Merge pull request #2563 from mastertyko/fix/writeIntegrationBranch-workflow-template-guard

fix(gsd): guard writeIntegrationBranch against workflow-template branches
This commit is contained in:
TÂCHES 2026-03-25 22:15:10 -06:00 committed by GitHub
commit 3b6d9024f5
2 changed files with 62 additions and 0 deletions

View file

@ -246,6 +246,15 @@ export function readIntegrationBranch(basePath: string, milestoneId: string): st
/** Regex matching GSD quick-task branches: gsd/quick/<num>-<slug> */
export const QUICK_BRANCH_RE = /^gsd\/quick\//;
/**
* Matches all GSD workflow-template branches: gsd/<templateId>/<slug>.
*
* Template IDs are lowercase alphanumeric with hyphens (e.g. hotfix, bugfix,
* small-feature, dep-upgrade). The negative lookahead excludes milestone
* branches (gsd/M001/... or gsd/M001-abc123/...) which use SLICE_BRANCH_RE.
*/
export const WORKFLOW_BRANCH_RE = /^gsd\/(?!M\d)[\w-]+\//;
export function writeIntegrationBranch(
basePath: string,
milestoneId: string,
@ -257,6 +266,10 @@ export function writeIntegrationBranch(
// to their origin branch on completion. Recording one as the integration
// target causes milestone merges to land on the wrong branch (#1293).
if (QUICK_BRANCH_RE.test(branch)) return;
// Don't record workflow-template branches (hotfix, bugfix, spike, etc.) —
// same root cause as quick-task branches (#2498). All templates create
// gsd/<templateId>/<slug> branches that are ephemeral.
if (WORKFLOW_BRANCH_RE.test(branch)) return;
// Validate
if (!VALID_BRANCH_NAME.test(branch)) return;
// Skip if already recorded with the same branch (idempotent across restarts).

View file

@ -868,6 +868,55 @@ describe('git-service', async () => {
rmSync(repo, { recursive: true, force: true });
});
// ─── writeIntegrationBranch: rejects workflow-template branches (#2498) ─
test('Integration branch: rejects workflow-template branches', () => {
const repo = initBranchTestRepo();
// All 8 registered workflow templates should be rejected
writeIntegrationBranch(repo, "M001", "gsd/hotfix/fix-login");
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "hotfix branch is not recorded");
writeIntegrationBranch(repo, "M001", "gsd/bugfix/null-pointer");
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "bugfix branch is not recorded");
writeIntegrationBranch(repo, "M001", "gsd/small-feature/add-button");
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "small-feature branch is not recorded");
writeIntegrationBranch(repo, "M001", "gsd/refactor/rename-module");
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "refactor branch is not recorded");
writeIntegrationBranch(repo, "M001", "gsd/spike/evaluate-lib");
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "spike branch is not recorded");
writeIntegrationBranch(repo, "M001", "gsd/security-audit/owasp-scan");
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "security-audit branch is not recorded");
writeIntegrationBranch(repo, "M001", "gsd/dep-upgrade/bump-react");
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "dep-upgrade branch is not recorded");
writeIntegrationBranch(repo, "M001", "gsd/full-project/new-app");
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "full-project branch is not recorded");
rmSync(repo, { recursive: true, force: true });
});
// ─── writeIntegrationBranch: still records legitimate branches ────────
test('Integration branch: records non-ephemeral gsd branches', () => {
const repo = initBranchTestRepo();
// A normal feature branch should still be recorded
writeIntegrationBranch(repo, "M001", "feature/new-thing");
assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), "feature/new-thing", "normal branches are recorded");
// The main branch should be recorded
writeIntegrationBranch(repo, "M002", "main");
assert.deepStrictEqual(readIntegrationBranch(repo, "M002"), "main", "main branch is recorded");
rmSync(repo, { recursive: true, force: true });
});
// ─── writeIntegrationBranch: rejects invalid branch names ─────────────
test('Integration branch: rejects invalid names', () => {