feat: Respect BEADS_SYNC_BRANCH environment variable

Fixes daemon and bd sync to honor BEADS_SYNC_BRANCH environment variable
as documented in PROTECTED_BRANCHES.md for CI/CD temporary overrides.

Changes:
- Updated internal/syncbranch.Get() to prioritize env var over DB config
- Both daemon sync and bd sync CLI now use syncbranch.Get()
- Added comprehensive tests for env var override behavior
- Validates branch names using git-style rules

This enables CI/CD workflows to override sync branch per-job without
mutating database config.

Based on PR #364 by Charles P. Cross <cpdata@users.noreply.github.com>
Co-authored-by: Charles P. Cross <cpdata@users.noreply.github.com>
This commit is contained in:
Steve Yegge
2025-11-22 18:17:19 -08:00
parent 72aa0d1097
commit 0dc8452c56
6 changed files with 220 additions and 69 deletions

View File

@@ -14,6 +14,7 @@ import (
"time"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/syncbranch"
"github.com/steveyegge/beads/internal/types"
)
@@ -200,6 +201,99 @@ func TestSyncBranchCommitAndPush_Success(t *testing.T) {
}
}
// TestSyncBranchCommitAndPush_EnvOverridesDB verifies that BEADS_SYNC_BRANCH
// takes precedence over the sync.branch database config for daemon commits.
func TestSyncBranchCommitAndPush_EnvOverridesDB(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
tmpDir := t.TempDir()
initTestGitRepo(t, tmpDir)
// Setup test store
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
dbPath := filepath.Join(beadsDir, "test.db")
store, err := sqlite.New(context.Background(), dbPath)
if err != nil {
t.Fatalf("Failed to create store: %v", err)
}
defer store.Close()
ctx := context.Background()
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
t.Fatalf("Failed to set prefix: %v", err)
}
// Configure DB sync.branch to one value
if err := store.SetConfig(ctx, "sync.branch", "db-branch"); err != nil {
t.Fatalf("Failed to set sync.branch: %v", err)
}
// Set BEADS_SYNC_BRANCH to a different value and ensure it takes precedence.
t.Setenv(syncbranch.EnvVar, "env-branch")
// Initial commit on main branch
oldWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get working directory: %v", err)
}
defer os.Chdir(oldWd)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change directory: %v", err)
}
initMainBranch(t, tmpDir)
// Create test issue and export JSONL
issue := &types.Issue{
Title: "Env override issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := store.CreateIssue(ctx, issue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := exportToJSONLWithStore(ctx, store, jsonlPath); err != nil {
t.Fatalf("Failed to export: %v", err)
}
log, _ := newTestSyncBranchLogger()
committed, err := syncBranchCommitAndPush(ctx, store, false, log)
if err != nil {
t.Fatalf("syncBranchCommitAndPush failed: %v", err)
}
if !committed {
t.Fatal("Expected committed=true with env override")
}
// Verify that the worktree and branch are created using the env branch.
worktreePath := filepath.Join(tmpDir, ".git", "beads-worktrees", "env-branch")
if _, err := os.Stat(worktreePath); os.IsNotExist(err) {
t.Fatalf("Env sync branch worktree not created at %s", worktreePath)
}
cmd := exec.Command("git", "branch", "--list", "env-branch")
cmd.Dir = tmpDir
output, err := cmd.Output()
if err != nil {
t.Fatalf("Failed to list branches: %v", err)
}
if !strings.Contains(string(output), "env-branch") {
t.Errorf("Env sync branch not created, branches: %s", string(output))
}
}
// TestSyncBranchCommitAndPush_NoChanges tests behavior when no changes to commit
func TestSyncBranchCommitAndPush_NoChanges(t *testing.T) {
if testing.Short() {