feat: Add Continuous Delivery system with auto-commit and file watcher
Automatically commit and push changes to GitHub with zero manual intervention. Features: - File watcher mode: Auto-detects changes in real-time - Timer mode: Commits at regular intervals (default 5 minutes) - Smart exclusions: Ignores temp files, sessions, cache, db files - Retry logic: Auto-retries failed pushes - Change summaries: Detailed commit messages with file lists Components: - auto-deploy.ps1: Core CD engine with file watcher - start-cd.ps1: Easy-to-use wrapper with commands - .cd-config.json: Configuration file - CONTINUOUS_DELIVERY_GUIDE.md: Complete documentation Usage: .\start-cd.ps1 watch # Start file watcher (recommended) .\start-cd.ps1 start # Timer mode (every 5 min) .\start-cd.ps1 once # One-time commit Also removed db_data/ from git tracking (now in .gitignore). Database runtime files should never be committed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
362
auto-deploy.ps1
Normal file
362
auto-deploy.ps1
Normal file
@@ -0,0 +1,362 @@
|
||||
# Continuous Delivery Script for EasyStream
|
||||
# Automatically commits and pushes changes to GitHub
|
||||
|
||||
param(
|
||||
[int]$IntervalSeconds = 300, # Default: 5 minutes
|
||||
[string]$Branch = "dev",
|
||||
[string]$CommitPrefix = "auto:",
|
||||
[switch]$WatchMode,
|
||||
[switch]$Verbose
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
$RepoPath = $PSScriptRoot
|
||||
|
||||
# Color output functions
|
||||
function Write-Success { param($msg) Write-Host "✓ $msg" -ForegroundColor Green }
|
||||
function Write-Info { param($msg) Write-Host "ℹ $msg" -ForegroundColor Cyan }
|
||||
function Write-Warning { param($msg) Write-Host "⚠ $msg" -ForegroundColor Yellow }
|
||||
function Write-Error { param($msg) Write-Host "✗ $msg" -ForegroundColor Red }
|
||||
|
||||
# Load configuration
|
||||
$configPath = Join-Path $RepoPath ".cd-config.json"
|
||||
if (Test-Path $configPath) {
|
||||
$config = Get-Content $configPath | ConvertFrom-Json
|
||||
if ($config.intervalSeconds) { $IntervalSeconds = $config.intervalSeconds }
|
||||
if ($config.branch) { $Branch = $config.branch }
|
||||
if ($config.commitPrefix) { $CommitPrefix = $config.commitPrefix }
|
||||
if ($config.excludePatterns) { $excludePatterns = $config.excludePatterns }
|
||||
} else {
|
||||
$excludePatterns = @(
|
||||
"f_data/data_sessions/*",
|
||||
"f_data/data_cache/_c_tpl/*",
|
||||
".setup_complete",
|
||||
"*.log",
|
||||
"db_data/*",
|
||||
"node_modules/*",
|
||||
"vendor/*"
|
||||
)
|
||||
}
|
||||
|
||||
function Test-GitRepo {
|
||||
Push-Location $RepoPath
|
||||
try {
|
||||
$null = git rev-parse --git-dir 2>&1
|
||||
return $?
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Get-GitStatus {
|
||||
Push-Location $RepoPath
|
||||
try {
|
||||
$status = git status --porcelain 2>&1
|
||||
return $status
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ChangeSummary {
|
||||
Push-Location $RepoPath
|
||||
try {
|
||||
$modified = @(git diff --name-only).Count
|
||||
$staged = @(git diff --cached --name-only).Count
|
||||
$untracked = @(git ls-files --others --exclude-standard).Count
|
||||
|
||||
return @{
|
||||
Modified = $modified
|
||||
Staged = $staged
|
||||
Untracked = $untracked
|
||||
Total = $modified + $staged + $untracked
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Update-GitIgnore {
|
||||
$gitignorePath = Join-Path $RepoPath ".gitignore"
|
||||
|
||||
$ignoreContent = @"
|
||||
# Temporary session files
|
||||
f_data/data_sessions/sess_*
|
||||
# Cache files
|
||||
f_data/data_cache/_c_tpl/*
|
||||
# Setup marker
|
||||
.setup_complete
|
||||
# Database runtime files
|
||||
db_data/*
|
||||
# Logs
|
||||
*.log
|
||||
# Dependencies
|
||||
node_modules/
|
||||
vendor/
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
"@
|
||||
|
||||
if (-not (Test-Path $gitignorePath)) {
|
||||
$ignoreContent | Out-File -FilePath $gitignorePath -Encoding UTF8
|
||||
Write-Success "Created .gitignore"
|
||||
} else {
|
||||
# Append if patterns are missing
|
||||
$existing = Get-Content $gitignorePath -Raw
|
||||
if ($existing -notmatch "f_data/data_sessions/sess_\*") {
|
||||
"`n# Auto-generated exclusions`n$ignoreContent" | Add-Content -Path $gitignorePath
|
||||
Write-Success "Updated .gitignore"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-AutoCommit {
|
||||
param([string]$message)
|
||||
|
||||
Push-Location $RepoPath
|
||||
try {
|
||||
Write-Info "Checking for changes..."
|
||||
|
||||
$changes = Get-ChangeSummary
|
||||
|
||||
if ($changes.Total -eq 0) {
|
||||
if ($Verbose) { Write-Info "No changes detected" }
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Info "Found $($changes.Total) changed files (Modified: $($changes.Modified), Staged: $($changes.Staged), Untracked: $($changes.Untracked))"
|
||||
|
||||
# Stage all changes
|
||||
Write-Info "Staging changes..."
|
||||
git add -A 2>&1 | Out-Null
|
||||
|
||||
# Generate commit message if not provided
|
||||
if (-not $message) {
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$message = "${CommitPrefix} Update at $timestamp"
|
||||
|
||||
# Add file change summary
|
||||
$fileList = @(git diff --cached --name-only | Select-Object -First 5)
|
||||
if ($fileList.Count -gt 0) {
|
||||
$message += "`n`nChanged files:"
|
||||
foreach ($file in $fileList) {
|
||||
$message += "`n- $file"
|
||||
}
|
||||
if ($changes.Total -gt 5) {
|
||||
$message += "`n- ... and $($changes.Total - 5) more files"
|
||||
}
|
||||
}
|
||||
|
||||
$message += "`n`n🤖 Generated with Claude Code Continuous Delivery`n`nCo-Authored-By: Claude <noreply@anthropic.com>"
|
||||
}
|
||||
|
||||
# Create commit
|
||||
Write-Info "Creating commit..."
|
||||
$commitResult = git commit -m $message 2>&1
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Committed changes"
|
||||
return $true
|
||||
} else {
|
||||
Write-Warning "Commit failed or nothing to commit"
|
||||
if ($Verbose) { Write-Host $commitResult }
|
||||
return $false
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-AutoPush {
|
||||
Push-Location $RepoPath
|
||||
try {
|
||||
Write-Info "Pushing to origin/$Branch..."
|
||||
|
||||
# Check if we're ahead of remote
|
||||
$ahead = git rev-list --count "origin/$Branch..$Branch" 2>&1
|
||||
|
||||
if ($ahead -match "^\d+$" -and [int]$ahead -gt 0) {
|
||||
Write-Info "Local is $ahead commit(s) ahead of remote"
|
||||
|
||||
# Push with retry logic
|
||||
$maxRetries = 3
|
||||
$retryCount = 0
|
||||
|
||||
while ($retryCount -lt $maxRetries) {
|
||||
$pushResult = git push origin $Branch 2>&1
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Successfully pushed to GitHub"
|
||||
return $true
|
||||
} else {
|
||||
$retryCount++
|
||||
Write-Warning "Push attempt $retryCount failed"
|
||||
if ($Verbose) { Write-Host $pushResult }
|
||||
|
||||
if ($retryCount -lt $maxRetries) {
|
||||
Write-Info "Retrying in 5 seconds..."
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Error "Failed to push after $maxRetries attempts"
|
||||
return $false
|
||||
} else {
|
||||
if ($Verbose) { Write-Info "Already up to date with remote" }
|
||||
return $false
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Start-ContinuousDelivery {
|
||||
Write-Info "========================================="
|
||||
Write-Info "EasyStream Continuous Delivery Started"
|
||||
Write-Info "========================================="
|
||||
Write-Info "Repository: $RepoPath"
|
||||
Write-Info "Branch: $Branch"
|
||||
Write-Info "Interval: $IntervalSeconds seconds"
|
||||
Write-Info "========================================="
|
||||
Write-Info ""
|
||||
|
||||
# Verify git repository
|
||||
if (-not (Test-GitRepo)) {
|
||||
Write-Error "Not a git repository: $RepoPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Update .gitignore
|
||||
Update-GitIgnore
|
||||
|
||||
$iteration = 0
|
||||
|
||||
while ($true) {
|
||||
$iteration++
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
Write-Host "`n[$timestamp] Check #$iteration" -ForegroundColor Magenta
|
||||
|
||||
try {
|
||||
# Commit changes
|
||||
$committed = Invoke-AutoCommit
|
||||
|
||||
# Push if there was a commit
|
||||
if ($committed) {
|
||||
Start-Sleep -Seconds 2
|
||||
Invoke-AutoPush
|
||||
}
|
||||
|
||||
# Wait for next interval
|
||||
Write-Info "Next check in $IntervalSeconds seconds... (Press Ctrl+C to stop)"
|
||||
Start-Sleep -Seconds $IntervalSeconds
|
||||
|
||||
} catch {
|
||||
Write-Error "Error during CD cycle: $_"
|
||||
Write-Info "Continuing in 30 seconds..."
|
||||
Start-Sleep -Seconds 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Start-FileWatcher {
|
||||
Write-Info "========================================="
|
||||
Write-Info "EasyStream File Watcher Started"
|
||||
Write-Info "========================================="
|
||||
Write-Info "Watching: $RepoPath"
|
||||
Write-Info "Branch: $Branch"
|
||||
Write-Info "Debounce: 30 seconds after last change"
|
||||
Write-Info "========================================="
|
||||
Write-Info ""
|
||||
|
||||
# Verify git repository
|
||||
if (-not (Test-GitRepo)) {
|
||||
Write-Error "Not a git repository: $RepoPath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Update .gitignore
|
||||
Update-GitIgnore
|
||||
|
||||
# Create file system watcher
|
||||
$watcher = New-Object System.IO.FileSystemWatcher
|
||||
$watcher.Path = $RepoPath
|
||||
$watcher.IncludeSubdirectories = $true
|
||||
$watcher.EnableRaisingEvents = $true
|
||||
|
||||
# Exclude patterns
|
||||
$watcher.Filter = "*.*"
|
||||
$watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor
|
||||
[System.IO.NotifyFilters]::DirectoryName -bor
|
||||
[System.IO.NotifyFilters]::LastWrite
|
||||
|
||||
$global:lastChangeTime = Get-Date
|
||||
$global:changedFiles = @{}
|
||||
|
||||
$onChange = {
|
||||
param($sender, $e)
|
||||
|
||||
# Skip excluded patterns
|
||||
$relativePath = $e.FullPath.Replace($RepoPath, "").TrimStart('\', '/')
|
||||
$exclude = $false
|
||||
foreach ($pattern in $excludePatterns) {
|
||||
if ($relativePath -like $pattern) {
|
||||
$exclude = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $exclude) {
|
||||
$global:lastChangeTime = Get-Date
|
||||
$global:changedFiles[$e.FullPath] = $e.ChangeType
|
||||
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] " -NoNewline -ForegroundColor Gray
|
||||
Write-Host "$($e.ChangeType): " -NoNewline -ForegroundColor Yellow
|
||||
Write-Host $relativePath -ForegroundColor White
|
||||
}
|
||||
}
|
||||
|
||||
# Register events
|
||||
Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $onChange | Out-Null
|
||||
Register-ObjectEvent -InputObject $watcher -EventName Created -Action $onChange | Out-Null
|
||||
Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action $onChange | Out-Null
|
||||
Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action $onChange | Out-Null
|
||||
|
||||
Write-Success "File watcher active. Monitoring for changes..."
|
||||
Write-Info "Changes will auto-commit 30 seconds after last modification"
|
||||
Write-Info ""
|
||||
|
||||
try {
|
||||
while ($true) {
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
# Check if enough time has passed since last change
|
||||
$timeSinceLastChange = (Get-Date) - $global:lastChangeTime
|
||||
|
||||
if ($global:changedFiles.Count -gt 0 -and $timeSinceLastChange.TotalSeconds -ge 30) {
|
||||
Write-Info "`nDebounce period elapsed. Processing $($global:changedFiles.Count) changes..."
|
||||
|
||||
$committed = Invoke-AutoCommit
|
||||
if ($committed) {
|
||||
Start-Sleep -Seconds 2
|
||||
Invoke-AutoPush
|
||||
}
|
||||
|
||||
# Reset
|
||||
$global:changedFiles = @{}
|
||||
Write-Info "`nContinuing to watch for changes...`n"
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$watcher.Dispose()
|
||||
Get-EventSubscriber | Unregister-Event
|
||||
}
|
||||
}
|
||||
|
||||
# Main execution
|
||||
if ($WatchMode) {
|
||||
Start-FileWatcher
|
||||
} else {
|
||||
Start-ContinuousDelivery
|
||||
}
|
||||
Reference in New Issue
Block a user