fix(daemon): add sync-branch guard to daemon code paths (#1271)

* fix(daemon): skip export when sync-branch matches current

Prevent redundant export operations by checking if the daemon's sync
branch matches the current active branch.

Previously, the daemon would attempt to perform an export even when
already on the target branch. This logic now skips the export step in
such cases to avoid unnecessary overhead and potential conflicts.
Includes a new integration test to verify the guard logic.

* fix(daemon): prevent sync on guarded branches

Add checks to verify if a branch is guarded before performing automated
sync cycles, auto-imports, or branch-specific commit and pull operations.
This prevents the daemon from modifying protected branches or running
synchronization tasks where they are restricted.

Includes comprehensive integration tests to verify the guard logic
during sync-branch operations.

* fix(daemon): warn on sync branch misconfiguration at startup

The daemon now checks for sync branch name conflicts during its startup
loop. This provides early feedback if the sync branch is configured
in a way that might conflict with existing branches or other settings.

The warnIfSyncBranchMisconfigured function performs the validation
and logs a warning to the console. Integration tests verify that
the daemon correctly identifies and reports these misconfigurations
at initialization.
This commit is contained in:
Peter Chanthamynavong
2026-01-24 17:10:08 -08:00
committed by GitHub
parent b7d650bd8e
commit dbd505656d
4 changed files with 1162 additions and 2 deletions

View File

@@ -16,9 +16,48 @@ import (
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/syncbranch"
"github.com/steveyegge/beads/internal/types"
)
// warnIfSyncBranchMisconfigured logs a warning at daemon startup if sync-branch
// equals the current branch. This is a one-time warning to alert users about
// the misconfiguration. The daemon continues to start (warn only, don't block).
// Returns true if misconfigured (warning was logged), false otherwise.
// GH#1258: Prevents silent failure when sync-branch == current-branch.
func warnIfSyncBranchMisconfigured(ctx context.Context, store storage.Storage, log daemonLogger) bool {
syncBranch, err := syncbranch.Get(ctx, store)
if err != nil || syncBranch == "" {
return false // No sync branch configured, not misconfigured
}
if syncbranch.IsSyncBranchSameAsCurrent(ctx, syncBranch) {
log.Warn("sync-branch misconfiguration detected",
"sync_branch", syncBranch,
"message", "sync-branch is your current branch; daemon sync operations will be skipped; configure a dedicated sync branch (e.g., 'beads-sync') to enable sync")
return true
}
return false
}
// shouldSkipDueToSameBranch checks if operation should be skipped because
// sync-branch == current-branch. Returns true if should skip, logs reason.
// Uses fail-open pattern: if branch detection fails, allows operation to proceed.
func shouldSkipDueToSameBranch(ctx context.Context, store storage.Storage, operation string, log daemonLogger) bool {
syncBranch, err := syncbranch.Get(ctx, store)
if err != nil || syncBranch == "" {
return false // No sync branch configured, allow
}
if syncbranch.IsSyncBranchSameAsCurrent(ctx, syncBranch) {
log.log("Skipping %s: sync-branch '%s' is your current branch. Use a dedicated sync branch.", operation, syncBranch)
return true
}
return false
}
// exportToJSONLWithStore exports issues to JSONL using the provided store.
// If multi-repo mode is configured, routes issues to their respective JSONL files.
// Otherwise, exports to a single JSONL file.
@@ -437,6 +476,13 @@ func performExport(ctx context.Context, store storage.Storage, autoCommit, autoP
if skipGit {
mode = "local export"
}
// Guard: Skip if sync-branch == current-branch (GH#1258)
// Local-only mode (skipGit) doesn't use sync-branch, so skip the guard
if !skipGit && shouldSkipDueToSameBranch(exportCtx, store, mode, log) {
return
}
log.log("Starting %s...", mode)
jsonlPath := findJSONLPath()
@@ -587,6 +633,12 @@ func performAutoImport(ctx context.Context, store storage.Storage, skipGit bool,
mode = "local auto-import"
}
// Guard: Skip if sync-branch == current-branch (GH#1258)
// Local-only mode (skipGit) doesn't use sync-branch, so skip the guard
if !skipGit && shouldSkipDueToSameBranch(importCtx, store, mode, log) {
return
}
// Check backoff before attempting sync (skip for local mode)
if !skipGit {
jsonlPath := findJSONLPath()
@@ -725,6 +777,13 @@ func performSync(ctx context.Context, store storage.Storage, autoCommit, autoPus
if skipGit {
mode = "local sync cycle"
}
// Guard: Skip if sync-branch == current-branch (GH#1258)
// Local-only mode (skipGit) doesn't use sync-branch, so skip the guard
if !skipGit && shouldSkipDueToSameBranch(syncCtx, store, mode, log) {
return
}
log.log("Starting %s...", mode)
jsonlPath := findJSONLPath()