From 25d734c8089c7188dc3a991ff8b31655b426cc66 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Thu, 1 Oct 2026 08:43:40 -0700 Subject: [PATCH] fix(gh-aw): create bootstrap candidate payload parent Part of #2128 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .changeset/bright-gh-aw-central-ops.md | 2 +- test/gh-aw-bootstrap-workflow.test.ts | 64 +++++++++++++++++++++++++ workflows/package/squad-bootstrap.md | 1 + workflows/squad-bootstrap.md | 1 + workflows/squad-workflows.manifest.json | 6 +-- 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/.changeset/bright-gh-aw-central-ops.md b/.changeset/bright-gh-aw-central-ops.md index 38591be23..2c558f44e 100644 --- a/.changeset/bright-gh-aw-central-ops.md +++ b/.changeset/bright-gh-aw-central-ops.md @@ -3,4 +3,4 @@ "@bradygaster/squad-sdk": patch --- -Require gh-aw v0.89.22 across the Squad workflow package, enlistment guidance, integrity contract, CI, and hosted E2E controller for central agentic ops compatibility. Validate isolated bootstrap candidates against the trusted checkout's Git history so materialization can create the Cast PR and research issue. +Require gh-aw v0.89.22 across the Squad workflow package, enlistment guidance, integrity contract, CI, and hosted E2E controller for central agentic ops compatibility. Validate isolated bootstrap candidates against the trusted checkout's Git history and create the candidate payload parent before writing so materialization can create the Cast PR and research issue. diff --git a/test/gh-aw-bootstrap-workflow.test.ts b/test/gh-aw-bootstrap-workflow.test.ts index 248416283..89690969a 100644 --- a/test/gh-aw-bootstrap-workflow.test.ts +++ b/test/gh-aw-bootstrap-workflow.test.ts @@ -437,6 +437,37 @@ function compileWorkflow(source = WORKFLOW): string { return readFileSync(join(workflowDir, 'squad-bootstrap.lock.yml'), 'utf8'); } +function materializeCandidatePayloadFromWorkflow( + source: string, + candidate: string, + candidatePayload: unknown, +): string { + const validateStart = source.indexOf('const validate = (candidatePayload, linkMode) => {'); + const validationStart = source.indexOf( + 'const errors = validatorModule.validateBootstrapPayload({', + validateStart, + ); + expect(validateStart).toBeGreaterThan(-1); + expect(validationStart).toBeGreaterThan(validateStart); + const validatePrelude = source.slice(validateStart, validationStart); + const candidatePayloadPath = join( + candidate, + '.github/workflows/squad-bootstrap-payload.json', + ); + const mkdirStatement = + 'mkdirSync(dirname(candidatePayloadPath), { recursive: true });'; + const writeStatement = + 'writeFileSync(candidatePayloadPath, `${JSON.stringify(candidatePayload)}\\n`);'; + const mkdirIndex = validatePrelude.indexOf(mkdirStatement); + const writeIndex = validatePrelude.indexOf(writeStatement); + expect(writeIndex).toBeGreaterThan(-1); + if (mkdirIndex >= 0 && mkdirIndex < writeIndex) { + mkdirSync(dirname(candidatePayloadPath), { recursive: true }); + } + writeFileSync(candidatePayloadPath, `${JSON.stringify(candidatePayload)}\n`); + return candidatePayloadPath; +} + afterAll(() => { for (const workspace of workspaces) rmSync(workspace, { recursive: true, force: true }); }); @@ -612,6 +643,7 @@ describe('automatic Squad bootstrap workflow', () => { recursive: true, filter: source => relative(fixture.root, source).split('/')[0] !== '.git', }); + expect(existsSync(join(candidate, '.git'))).toBe(false); const options = { root: candidate, payloadPath: join(candidate, 'payload.json'), @@ -634,6 +666,26 @@ describe('automatic Squad bootstrap workflow', () => { expect(resolvedResult.stdout).toBe('Squad bootstrap validation passed.\n'); }); + it('creates the missing candidate payload parent before materialization', () => { + const candidate = mkdtempSync(join(tmpdir(), 'gh-aw-bootstrap-candidate-')); + workspaces.push(candidate); + expect(existsSync(join(candidate, '.github/workflows'))).toBe(false); + + const payload = { schema_version: '1', repository: 'octo/example' }; + const payloadPath = materializeCandidatePayloadFromWorkflow(WORKFLOW, candidate, payload); + expect(readFileSync(payloadPath, 'utf8')).toBe(`${JSON.stringify(payload)}\n`); + + const mutatedCandidate = mkdtempSync(join(tmpdir(), 'gh-aw-bootstrap-candidate-')); + workspaces.push(mutatedCandidate); + const mutated = WORKFLOW.replace( + ' mkdirSync(dirname(candidatePayloadPath), { recursive: true });\n', + '', + ); + expect(() => + materializeCandidatePayloadFromWorkflow(mutated, mutatedCandidate, payload), + ).toThrow(/ENOENT/); + }); + it('transports a shared payload larger than the old single-string limit byte-identically', () => { const fixture = createFixture(); const targetBytes = 34_843; @@ -871,7 +923,19 @@ describe('automatic Squad bootstrap workflow', () => { expect(lock).not.toMatch(/"materialize-bootstrap":\{"inputs":\{"payload":/); expect(lock).toContain('reconstructBootstrapPayload(items[0])'); expect(lock).toContain("mkdtempSync(join(tmpdir(), 'squad-bootstrap-candidate-'))"); + expect(lock).toContain("relative(checkout, source).split('/')[0] !== '.git'"); expect(lock).toContain('gitRoot: checkout'); + const candidatePayloadMkdir = lock.indexOf( + 'mkdirSync(dirname(candidatePayloadPath), { recursive: true });', + ); + const candidatePayloadWrite = lock.indexOf( + 'writeFileSync(candidatePayloadPath, `${JSON.stringify(candidatePayload)}\\n`);', + ); + expect(candidatePayloadMkdir).toBeGreaterThan(-1); + expect(candidatePayloadWrite).toBeGreaterThan(candidatePayloadMkdir); + expect(lock.slice(candidatePayloadMkdir, candidatePayloadWrite).trim()).toBe( + 'mkdirSync(dirname(candidatePayloadPath), { recursive: true });', + ); expect(lock).toContain('Bootstrap payload path crosses a symbolic link'); expect(lock.indexOf("validate(payload, 'placeholder')")) .toBeLessThan(lock.indexOf('safeTarget(checkout, file.path)')); diff --git a/workflows/package/squad-bootstrap.md b/workflows/package/squad-bootstrap.md index 3efb5b0fc..7671602f3 100644 --- a/workflows/package/squad-bootstrap.md +++ b/workflows/package/squad-bootstrap.md @@ -313,6 +313,7 @@ safe-outputs: const validate = (candidatePayload, linkMode) => { const candidatePayloadPath = join(candidate, '.github/workflows/squad-bootstrap-payload.json'); + mkdirSync(dirname(candidatePayloadPath), { recursive: true }); writeFileSync(candidatePayloadPath, `${JSON.stringify(candidatePayload)}\n`); const errors = validatorModule.validateBootstrapPayload({ root: candidate, diff --git a/workflows/squad-bootstrap.md b/workflows/squad-bootstrap.md index 8143736be..871d854cb 100644 --- a/workflows/squad-bootstrap.md +++ b/workflows/squad-bootstrap.md @@ -304,6 +304,7 @@ safe-outputs: const validate = (candidatePayload, linkMode) => { const candidatePayloadPath = join(candidate, '.github/workflows/squad-bootstrap-payload.json'); + mkdirSync(dirname(candidatePayloadPath), { recursive: true }); writeFileSync(candidatePayloadPath, `${JSON.stringify(candidatePayload)}\n`); const errors = validatorModule.validateBootstrapPayload({ root: candidate, diff --git a/workflows/squad-workflows.manifest.json b/workflows/squad-workflows.manifest.json index a29fdcf8d..6e8599695 100644 --- a/workflows/squad-workflows.manifest.json +++ b/workflows/squad-workflows.manifest.json @@ -68,9 +68,9 @@ "source": "workflows/package/squad-bootstrap.md", "destination": ".github/workflows/squad-bootstrap.md", "lock": ".github/workflows/squad-bootstrap.lock.yml", - "source_sha256": "9cccdec152866720f99452b77fc0407b6a308e86e22f4091b50d844eb40b4fa7", - "lock_sha256": "96a02f7d5837e9753eacae9fbf99658fedf96f33f40ce164e5da9fbb8a283058", - "package_lock_sha256": "33bc16d47c98b27de5fb3a9931437a43d6e8dcde60b1b15ba29214b42256a235" + "source_sha256": "c2c64baf14c12065609a4cd3a966ff4ed8ff038087d4911e802a2737ae4ac7c9", + "lock_sha256": "3355eb5eac82d4c9f370ba0574d401e0da3cc3abf56e8957d3e9b749803a22cc", + "package_lock_sha256": "1707880aeaba3586519d5f56a0fee967e477b1f3966393634bd8e0a8cdb1075d" }, { "name": "squad-command-router",