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:
SamiAhmed7777
2025-10-26 02:07:57 -07:00
parent d22b3e1c0d
commit 877b9347b6
207 changed files with 980 additions and 3658 deletions

362
auto-deploy.ps1 Normal file
View 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
}