Merge pull request #1279 from gsd-build/fix/early-branch-creation-1278

fix: create strategy branch before first commit, not at execute-phase
This commit is contained in:
Tom Boucher
2026-03-21 01:17:33 -04:00
committed by GitHub
2 changed files with 102 additions and 0 deletions

View File

@@ -247,6 +247,43 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
return;
}
// Ensure branching strategy branch exists before first commit (#1278).
// Pre-execution workflows (discuss, plan, research) commit artifacts but the branch
// was previously only created during execute-phase — too late.
if (config.branching_strategy && config.branching_strategy !== 'none') {
let branchName = null;
if (config.branching_strategy === 'phase') {
// Determine which phase we're committing for from the file paths
const phaseMatch = (files || []).join(' ').match(/(\d+)-/);
if (phaseMatch) {
const phaseNum = phaseMatch[1];
const phaseInfo = findPhaseInternal(cwd, phaseNum);
if (phaseInfo) {
branchName = config.phase_branch_template
.replace('{phase}', phaseInfo.phase_number)
.replace('{slug}', phaseInfo.phase_slug || 'phase');
}
}
} else if (config.branching_strategy === 'milestone') {
const milestone = getMilestoneInfo(cwd);
if (milestone && milestone.version) {
branchName = config.milestone_branch_template
.replace('{milestone}', milestone.version)
.replace('{slug}', generateSlugInternal(milestone.name) || 'milestone');
}
}
if (branchName) {
const currentBranch = execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
if (currentBranch.exitCode === 0 && currentBranch.stdout.trim() !== branchName) {
// Create branch if it doesn't exist, or switch to it if it does
const create = execGit(cwd, ['checkout', '-b', branchName]);
if (create.exitCode !== 0) {
execGit(cwd, ['checkout', branchName]);
}
}
}
}
// Stage files
const filesToStage = files && files.length > 0 ? files : ['.planning/'];
for (const file of filesToStage) {

View File

@@ -1185,6 +1185,71 @@ describe('commit command', () => {
const logCount = execSync('git log --oneline', { cwd: tmpDir, encoding: 'utf-8' }).trim().split('\n').length;
assert.strictEqual(logCount, 2, 'should have 2 commits (initial + amended)');
});
test('creates strategy branch before first commit when branching_strategy is milestone', () => {
// Configure milestone branching strategy
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({
commit_docs: true,
branching_strategy: 'milestone',
milestone_branch_template: 'gsd/{milestone}-{slug}',
})
);
// getMilestoneInfo reads ROADMAP.md for milestone version/name
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'## v1.0: Initial Release\n\n### Phase 1: Setup\n'
);
// Create a file to commit
fs.writeFileSync(path.join(tmpDir, '.planning', 'test-context.md'), '# Context\n');
const result = runGsdTools('commit "docs: add context" --files .planning/test-context.md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.committed, true, 'should have committed');
// Verify we're on the strategy branch
const { execFileSync } = require('child_process');
const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: tmpDir, encoding: 'utf-8' }).trim();
assert.strictEqual(branch, 'gsd/v1.0-initial-release', 'should be on milestone branch');
});
test('creates strategy branch before first commit when branching_strategy is phase', () => {
// Configure phase branching strategy
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({
commit_docs: true,
branching_strategy: 'phase',
phase_branch_template: 'gsd/phase-{phase}-{slug}',
})
);
// Create ROADMAP.md with a phase
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-setup'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n## Phase 1: Setup\nGoal: Initial setup\n'
);
// Create a context file for phase 1
fs.writeFileSync(path.join(tmpDir, '.planning', 'phases', '01-setup', '01-CONTEXT.md'), '# Context\n');
const result = runGsdTools(
'commit "docs(01): add context" --files .planning/phases/01-setup/01-CONTEXT.md',
tmpDir
);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.committed, true, 'should have committed');
// Verify we're on the strategy branch
const { execFileSync } = require('child_process');
const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: tmpDir, encoding: 'utf-8' }).trim();
assert.strictEqual(branch, 'gsd/phase-01-setup', 'should be on phase branch');
});
});
// ─────────────────────────────────────────────────────────────────────────────