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

@@ -529,6 +529,13 @@ func runDaemonLoop(interval time.Duration, autoCommit, autoPush, autoPull, local
log.Warn("repository mismatch ignored (BEADS_IGNORE_REPO_MISMATCH=1)")
}
// GH#1258: Warn at startup if sync-branch == current-branch (misconfiguration)
// This is a one-time warning - per-operation skipping is handled by shouldSkipDueToSameBranch()
// Skip check in local mode (no sync-branch is used)
if !localMode {
warnIfSyncBranchMisconfigured(ctx, store, log)
}
// Validate schema version matches daemon version
versionCtx := context.Background()
dbVersion, err := store.GetMetadata(versionCtx, "bd_version")

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()

View File

@@ -31,7 +31,12 @@ func syncBranchCommitAndPushWithOptions(ctx context.Context, store storage.Stora
if !hasGitRemote(ctx) {
return true, nil // Skip sync branch commit/push in local-only mode
}
// Guard: Skip if sync-branch == current-branch (GH#1258)
if shouldSkipDueToSameBranch(ctx, store, "sync-branch commit", log) {
return false, nil
}
// Get sync branch configuration (supports BEADS_SYNC_BRANCH override)
syncBranch, err := syncbranch.Get(ctx, store)
if err != nil {
@@ -252,7 +257,12 @@ func syncBranchPull(ctx context.Context, store storage.Storage, log daemonLogger
if !hasGitRemote(ctx) {
return true, nil // Skip sync branch pull in local-only mode
}
// Guard: Skip if sync-branch == current-branch (GH#1258)
if shouldSkipDueToSameBranch(ctx, store, "sync-branch pull", log) {
return false, nil
}
// Get sync branch configuration (supports BEADS_SYNC_BRANCH override)
syncBranch, err := syncbranch.Get(ctx, store)
if err != nil {

File diff suppressed because it is too large Load Diff