diff --git a/CHANGELOG.md b/CHANGELOG.md index 036e8a2..ace2d17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Changelog +### v0.29.1 — Fix Git Lock File Stale After Interrupted Sync (2026-02-24) + +#### Fixed +- **Stale git lock file recovery** — Catalog sync now removes stale `.git/index.lock`, `.git/shallow.lock`, and `.git/HEAD.lock` files before running `git fetch`/`git reset`. Previously, if the container was killed mid-sync, the leftover lock file would block all subsequent syncs until manual intervention. + ### v0.29.0 — Encrypt Sensitive Values in app.yaml (2026-02-23) #### Added diff --git a/controller/README.md b/controller/README.md index a9fa644..237cfc8 100644 --- a/controller/README.md +++ b/controller/README.md @@ -652,6 +652,7 @@ Notification preferences (email, enabled events, cooldown hours) are: - Periodic `git fetch` + `git reset --hard` of the app catalog repo - Content-hash comparison prevents unnecessary file writes - Post-sync stack rescan detects new/changed apps immediately +- **Stale lock recovery**: automatically removes `.git/index.lock`, `.git/shallow.lock`, and `.git/HEAD.lock` before each fetch — prevents permanent sync failures after interrupted operations (e.g. container restart mid-sync) #### Planned Update Classifications diff --git a/controller/internal/sync/sync.go b/controller/internal/sync/sync.go index 35d54e3..848fce6 100644 --- a/controller/internal/sync/sync.go +++ b/controller/internal/sync/sync.go @@ -252,6 +252,9 @@ func (s *Syncer) gitCloneOrPull() error { return s.runGit(args...) } + // Remove stale git lock files left behind by interrupted operations + s.removeGitLockFiles() + // Pull s.logger.Printf("[SYNC] Pulling latest from %s (branch: %s)", s.cfg.Git.RepoURL, s.cfg.Git.Branch) if s.isDebug() { @@ -266,6 +269,23 @@ func (s *Syncer) gitCloneOrPull() error { return nil } +// removeGitLockFiles removes stale .git/*.lock files that may have been left +// behind if a previous git operation was interrupted (e.g. container restart). +// These lock files prevent all subsequent git operations from succeeding. +func (s *Syncer) removeGitLockFiles() { + gitDir := filepath.Join(s.cacheDir, ".git") + lockFiles := []string{"index.lock", "shallow.lock", "HEAD.lock"} + for _, name := range lockFiles { + lockPath := filepath.Join(gitDir, name) + if _, err := os.Stat(lockPath); err == nil { + s.logger.Printf("[SYNC] Removing stale lock file: %s", lockPath) + if err := os.Remove(lockPath); err != nil { + s.logger.Printf("[SYNC] Failed to remove lock file %s: %v", lockPath, err) + } + } + } +} + // buildRepoURL constructs the repo URL with optional auth credentials. func (s *Syncer) buildRepoURL() string { url := s.cfg.Git.RepoURL