diff --git a/scripts/lib/workflow-bootstrap.mjs b/scripts/lib/workflow-bootstrap.mjs index a87d7760..05c0d38d 100644 --- a/scripts/lib/workflow-bootstrap.mjs +++ b/scripts/lib/workflow-bootstrap.mjs @@ -26,6 +26,13 @@ function normalizeHead(headSha) { return value; } +function normalizeBase(baseSha) { + if (baseSha === null || baseSha === undefined || baseSha === "") return null; + const value = String(baseSha).trim().toLowerCase(); + if (!HEAD_RE.test(value)) throw new Error("workflow_bootstrap_base_invalid"); + return value; +} + function stateRoot(override) { return resolve( override || process.env.GITHUB_DELIVERY_STATE_DIR || join(homedir(), ".github-delivery"), @@ -67,6 +74,7 @@ function assertCheckpointIdentity(snapshot, { repo, headSha }) { export function bootstrapLocalPrWorkflow({ repo, headSha, baseSha = null, stateDir } = {}) { const normalizedRepo = normalizeRepo(repo); const normalizedHead = normalizeHead(headSha); + const normalizedBase = normalizeBase(baseSha); const checkpointPath = localPrWorkflowCheckpointPath({ repo: normalizedRepo, headSha: normalizedHead, @@ -76,7 +84,7 @@ export function bootstrapLocalPrWorkflow({ repo, headSha, baseSha = null, stateD const initialSnapshot = createDeliveryWorkflowController({ workflow: profile.workflow, repo: normalizedRepo, - baseSha: baseSha ? String(baseSha) : null, + baseSha: normalizedBase, headSha: normalizedHead, graph: profile.graph, startPhase: profile.startPhase, @@ -101,9 +109,9 @@ export function bootstrapLocalPrWorkflow({ repo, headSha, baseSha = null, stateD headSha: normalizedHead, }); if ( - baseSha && + normalizedBase && storedSnapshot.baseSha && - String(storedSnapshot.baseSha).toLowerCase() !== String(baseSha).toLowerCase() + String(storedSnapshot.baseSha).toLowerCase() !== normalizedBase ) { throw new Error("workflow_bootstrap_checkpoint_base_mismatch"); } diff --git a/tests/unit/workflow-state-grounding.test.mjs b/tests/unit/workflow-state-grounding.test.mjs index f4c000d6..c3dedff3 100644 --- a/tests/unit/workflow-state-grounding.test.mjs +++ b/tests/unit/workflow-state-grounding.test.mjs @@ -124,6 +124,23 @@ test("show re-derives controller state instead of trusting raw checkpoint guidan } }); +test("local workflow bootstrap rejects abbreviated base SHA before checkpoint comparison", () => { + const stateDir = mkdtempSync(join(tmpdir(), "github-delivery-bootstrap-base-")); + try { + assert.throws( + () => bootstrapLocalPrWorkflow({ + repo: "acme/widgets", + headSha: HEAD, + baseSha: "a".repeat(9), + stateDir, + }), + /workflow_bootstrap_base_invalid/, + ); + } finally { + rmSync(stateDir, { recursive: true, force: true }); + } +}); + test("local workflow bootstrap upgrades reused raw checkpoint guidance", () => { const stateDir = mkdtempSync(join(tmpdir(), "github-delivery-bootstrap-grounding-")); try {