Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/bright-gh-aw-central-ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
64 changes: 64 additions & 0 deletions test/gh-aw-bootstrap-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
Expand Down Expand Up @@ -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'),
Expand All @@ -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;
Expand Down Expand Up @@ -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)'));
Expand Down
1 change: 1 addition & 0 deletions workflows/package/squad-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions workflows/squad-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions workflows/squad-workflows.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading