From 1c42035e53faac0bfda9b5a5404e3537e35c6dfe Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Mon, 28 Sep 2026 17:39:19 -0700 Subject: [PATCH 1/2] fix(gh-aw): make review guard first-install safe Closes #2103 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/gh-aw-review-workflow.test.ts | 98 ++++++++++++++++++++++++- workflows/package/squad-review.md | 31 ++++++-- workflows/squad-review.md | 31 ++++++-- workflows/squad-workflows.manifest.json | 2 +- 4 files changed, 147 insertions(+), 15 deletions(-) diff --git a/test/gh-aw-review-workflow.test.ts b/test/gh-aw-review-workflow.test.ts index c83c44082..3bc06593a 100644 --- a/test/gh-aw-review-workflow.test.ts +++ b/test/gh-aw-review-workflow.test.ts @@ -2,7 +2,7 @@ import { afterAll, describe, expect, it } from 'vitest'; import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { execFileSync, spawnSync } from 'node:child_process'; import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { extractSafeOutputsConfigJson } from './helpers/gh-aw-lock.js'; import { parse } from 'yaml'; @@ -163,11 +163,75 @@ function assertCompiledGate(lock: string): void { expect(steps[guard].with.script).toContain('await guard.enforceReviewOutputs'); expect(steps[guard].if).toBeUndefined(); expect(steps[guard]['continue-on-error']).toBeUndefined(); - expect(steps[guard - 1].with.ref).toBe('${{ github.event.pull_request.base.sha || github.workflow_sha }}'); + const baseCheckout = steps.find((step: { name: string }) => + step.name === 'Checkout base commit for review guard'); + const workflowCheckout = steps.find((step: { name: string }) => + step.name === 'Checkout workflow commit for first-install review guard'); + expect(baseCheckout.with.ref).toBe('${{ github.event.pull_request.base.sha || github.workflow_sha }}'); + expect(workflowCheckout.with.ref).toBe('${{ github.workflow_sha }}'); + expect(steps[guard].with.script).toContain('existsSync(baseGuard) ? baseGuard : workflowGuard'); + const finalGate = review.steps.find((step: { name: string }) => + step.name === 'Enforce independent current-head verdict'); + expect(finalGate.with.script).toContain('existsSync(baseGuard) ? baseGuard : workflowGuard'); for (const step of review.steps) expect(step['continue-on-error']).toBeUndefined(); expect(review['continue-on-error']).toBeUndefined(); } +function safeOutputsGuardScript(lock: string): string { + const workflow = parse(lock); + const step = workflow.jobs.safe_outputs.steps.find((candidate: { name: string }) => + candidate.name === 'Bind review output to committed agent identities and current head'); + expect(step, 'compiled safe_outputs guard step is missing').toBeDefined(); + return step.with.script; +} + +async function executeCompiledGuardLoader( + script: string, + { + baseGuard, + workflowGuard, + head = 'a'.repeat(40), + }: { baseGuard?: string; workflowGuard: string; head?: string }, +): Promise { + const workspace = mkdtempSync(resolve(ROOT, '.squad-review-loader-')); + compileWorkspaces.push(workspace); + const output = resolve(workspace, 'loaded.txt'); + const guardPath = '.github/workflows/shared/squad-review-guard.mjs'; + if (baseGuard !== undefined) { + const path = resolve(workspace, '.squad-review-base', guardPath); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, baseGuard); + } + const workflowPath = resolve(workspace, '.squad-review-workflow', guardPath); + mkdirSync(dirname(workflowPath), { recursive: true }); + writeFileSync(workflowPath, workflowGuard); + + const previousWorkspace = process.env.GITHUB_WORKSPACE; + const previousOutput = process.env.SQUAD_TEST_GUARD_OUTPUT; + const previousHead = process.env.SQUAD_REVIEW_HEAD; + process.env.GITHUB_WORKSPACE = workspace; + process.env.SQUAD_TEST_GUARD_OUTPUT = output; + process.env.SQUAD_REVIEW_HEAD = head; + try { + const runner = resolve(workspace, 'runner.mjs'); + writeFileSync(runner, ` + import { createRequire } from 'node:module'; + const require = createRequire(import.meta.url); + const github = { request: async () => ({ data: {} }) }; + ${script} + `); + await import(`${pathToFileURL(runner).href}?run=${Date.now()}`); + return readFileSync(output, 'utf8'); + } finally { + if (previousWorkspace === undefined) delete process.env.GITHUB_WORKSPACE; + else process.env.GITHUB_WORKSPACE = previousWorkspace; + if (previousOutput === undefined) delete process.env.SQUAD_TEST_GUARD_OUTPUT; + else process.env.SQUAD_TEST_GUARD_OUTPUT = previousOutput; + if (previousHead === undefined) delete process.env.SQUAD_REVIEW_HEAD; + else process.env.SQUAD_REVIEW_HEAD = previousHead; + } +} + describe('gh-aw enforcing Squad reviewer', () => { it('routes /squad review to the isolated workflow and supports automatic PR events', () => { const routerDispatch = yamlBlock(ROUTER_FRONTMATTER, 'dispatch-workflow'); @@ -353,6 +417,36 @@ describe('gh-aw enforcing Squad reviewer', () => { expect(lock).toContain('GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }}'); }, 30000); + it('executes the compiled first-install fallback without weakening established base control', async () => { + const { lock } = compileReviewer(); + const script = safeOutputsGuardScript(lock); + const stub = (source: string) => ` + import { writeFileSync } from 'node:fs'; + export async function enforceReviewOutputs(env) { + writeFileSync(process.env.SQUAD_TEST_GUARD_OUTPUT, + JSON.stringify({ source: ${JSON.stringify(source)}, head: env.SQUAD_REVIEW_HEAD })); + } + `; + + for (const head of [ + 'edd6fbf0f84334ff086ed2bc8bbfed5503fdd56f', + '9a898b5182ca432b62bef26dbf1f9022dd1d256b', + ]) { + await expect(executeCompiledGuardLoader(script, { + workflowGuard: stub('workflow'), + head, + })).resolves.toBe(JSON.stringify({ source: 'workflow', head })); + } + await expect(executeCompiledGuardLoader(script, { + baseGuard: stub('base'), + workflowGuard: stub('workflow'), + })).resolves.toBe(JSON.stringify({ source: 'base', head: 'a'.repeat(40) })); + await expect(executeCompiledGuardLoader(script, { + baseGuard: 'this is not valid JavaScript', + workflowGuard: stub('workflow'), + })).rejects.toThrow(); + }, 30000); + it('kills mutations of every important authority and provenance gate', () => { const mutations = [ REVIEWER.replace('tools:\n bash:', 'tools:\n edit:\n bash:'), diff --git a/workflows/package/squad-review.md b/workflows/package/squad-review.md index 033d863df..512ec9b80 100644 --- a/workflows/package/squad-review.md +++ b/workflows/package/squad-review.md @@ -56,6 +56,12 @@ safe-outputs: ref: ${{ github.event.pull_request.base.sha || github.workflow_sha }} persist-credentials: false path: .squad-review-base + - name: Checkout workflow commit for first-install review guard + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + path: .squad-review-workflow - name: Bind review output to committed agent identities and current head uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 env: @@ -64,10 +70,13 @@ safe-outputs: SQUAD_REVIEW_HEAD: ${{ github.event.pull_request.head.sha || github.event.inputs.expected_head_sha }} with: script: | + const { existsSync } = require('node:fs'); const { pathToFileURL } = require('node:url'); - const guard = await import(pathToFileURL( - `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs` - ).href); + const baseGuard = + `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs`; + const workflowGuard = + `${process.env.GITHUB_WORKSPACE}/.squad-review-workflow/.github/workflows/shared/squad-review-guard.mjs`; + const guard = await import(pathToFileURL(existsSync(baseGuard) ? baseGuard : workflowGuard).href); await guard.enforceReviewOutputs(process.env, async (route, fields) => (await github.request(`GET /${route}`, fields)).data); add-comment: @@ -114,6 +123,13 @@ jobs: ref: ${{ github.event.pull_request.base.sha }} persist-credentials: false path: .squad-review-base + - name: Checkout workflow commit for first-install final verdict gate + if: github.event_name == 'pull_request' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + path: .squad-review-workflow - name: Enforce independent current-head verdict if: github.event_name == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 @@ -122,10 +138,13 @@ jobs: SQUAD_REVIEW_HEAD: ${{ github.event.pull_request.head.sha }} with: script: | + const { existsSync } = require('node:fs'); const { pathToFileURL } = require('node:url'); - const guard = await import(pathToFileURL( - `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs` - ).href); + const baseGuard = + `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs`; + const workflowGuard = + `${process.env.GITHUB_WORKSPACE}/.squad-review-workflow/.github/workflows/shared/squad-review-guard.mjs`; + const guard = await import(pathToFileURL(existsSync(baseGuard) ? baseGuard : workflowGuard).href); await guard.assertClearingReview(process.env, async (route, fields) => (await github.request(`GET /${route}`, fields)).data); --- diff --git a/workflows/squad-review.md b/workflows/squad-review.md index 78846f728..96a4b67ca 100644 --- a/workflows/squad-review.md +++ b/workflows/squad-review.md @@ -54,6 +54,12 @@ safe-outputs: ref: ${{ github.event.pull_request.base.sha || github.workflow_sha }} persist-credentials: false path: .squad-review-base + - name: Checkout workflow commit for first-install review guard + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + path: .squad-review-workflow - name: Bind review output to committed agent identities and current head uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: @@ -62,10 +68,13 @@ safe-outputs: SQUAD_REVIEW_HEAD: ${{ github.event.pull_request.head.sha || github.event.inputs.expected_head_sha }} with: script: | + const { existsSync } = require('node:fs'); const { pathToFileURL } = require('node:url'); - const guard = await import(pathToFileURL( - `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs` - ).href); + const baseGuard = + `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs`; + const workflowGuard = + `${process.env.GITHUB_WORKSPACE}/.squad-review-workflow/.github/workflows/shared/squad-review-guard.mjs`; + const guard = await import(pathToFileURL(existsSync(baseGuard) ? baseGuard : workflowGuard).href); await guard.enforceReviewOutputs(process.env, async (route, fields) => (await github.request(`GET /${route}`, fields)).data); add-comment: @@ -109,6 +118,13 @@ jobs: ref: ${{ github.event.pull_request.base.sha }} persist-credentials: false path: .squad-review-base + - name: Checkout workflow commit for first-install final verdict gate + if: github.event_name == 'pull_request' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.workflow_sha }} + persist-credentials: false + path: .squad-review-workflow - name: Enforce independent current-head verdict if: github.event_name == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -117,10 +133,13 @@ jobs: SQUAD_REVIEW_HEAD: ${{ github.event.pull_request.head.sha }} with: script: | + const { existsSync } = require('node:fs'); const { pathToFileURL } = require('node:url'); - const guard = await import(pathToFileURL( - `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs` - ).href); + const baseGuard = + `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs`; + const workflowGuard = + `${process.env.GITHUB_WORKSPACE}/.squad-review-workflow/.github/workflows/shared/squad-review-guard.mjs`; + const guard = await import(pathToFileURL(existsSync(baseGuard) ? baseGuard : workflowGuard).href); await guard.assertClearingReview(process.env, async (route, fields) => (await github.request(`GET /${route}`, fields)).data); --- diff --git a/workflows/squad-workflows.manifest.json b/workflows/squad-workflows.manifest.json index 66f4a6d09..3432b865a 100644 --- a/workflows/squad-workflows.manifest.json +++ b/workflows/squad-workflows.manifest.json @@ -28,7 +28,7 @@ "source": "workflows/package/squad-review.md", "destination": ".github/workflows/squad-review.md", "lock": ".github/workflows/squad-review.lock.yml", - "source_sha256": "d2adc1235555f96bce6abedf9e7ef665de1f33704da685f51b6abe45d62abbd2" + "source_sha256": "fa48adb2ff79d1afcd3499d48eefc37127f962f69f3efabcb08a9553ef065bae" }, { "name": "squad-deps-worker", From fa940f41dd1617524bec0260681d44b738826f25 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Mon, 28 Sep 2026 18:24:57 -0700 Subject: [PATCH 2/2] fix(gh-aw): fail closed on missing installed review guard Closes #2103 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/gh-aw-review-workflow.test.ts | 81 ++++++++++++++++++------- workflows/package/squad-review.md | 28 ++++++++- workflows/squad-review.md | 28 ++++++++- workflows/squad-workflows.manifest.json | 2 +- 4 files changed, 112 insertions(+), 27 deletions(-) diff --git a/test/gh-aw-review-workflow.test.ts b/test/gh-aw-review-workflow.test.ts index 3bc06593a..bbccd9b61 100644 --- a/test/gh-aw-review-workflow.test.ts +++ b/test/gh-aw-review-workflow.test.ts @@ -169,29 +169,40 @@ function assertCompiledGate(lock: string): void { step.name === 'Checkout workflow commit for first-install review guard'); expect(baseCheckout.with.ref).toBe('${{ github.event.pull_request.base.sha || github.workflow_sha }}'); expect(workflowCheckout.with.ref).toBe('${{ github.workflow_sha }}'); - expect(steps[guard].with.script).toContain('existsSync(baseGuard) ? baseGuard : workflowGuard'); + expect(steps[guard].with.script).toContain('if (existsSync(baseGuard))'); + expect(steps[guard].with.script).toContain('else if (!existsSync(baseManifest))'); + expect(steps[guard].with.script).toContain('missing from an established base installation'); const finalGate = review.steps.find((step: { name: string }) => step.name === 'Enforce independent current-head verdict'); - expect(finalGate.with.script).toContain('existsSync(baseGuard) ? baseGuard : workflowGuard'); + expect(finalGate.with.script).toContain('if (existsSync(baseGuard))'); + expect(finalGate.with.script).toContain('else if (!existsSync(baseManifest))'); + expect(finalGate.with.script).toContain('missing from an established base installation'); for (const step of review.steps) expect(step['continue-on-error']).toBeUndefined(); expect(review['continue-on-error']).toBeUndefined(); } -function safeOutputsGuardScript(lock: string): string { +function compiledGuardScripts(lock: string): Array<{ name: string; script: string }> { const workflow = parse(lock); - const step = workflow.jobs.safe_outputs.steps.find((candidate: { name: string }) => + const safeOutputsStep = workflow.jobs.safe_outputs.steps.find((candidate: { name: string }) => candidate.name === 'Bind review output to committed agent identities and current head'); - expect(step, 'compiled safe_outputs guard step is missing').toBeDefined(); - return step.with.script; + const finalGateStep = workflow.jobs.review.steps.find((candidate: { name: string }) => + candidate.name === 'Enforce independent current-head verdict'); + expect(safeOutputsStep, 'compiled safe_outputs guard step is missing').toBeDefined(); + expect(finalGateStep, 'compiled final verdict guard step is missing').toBeDefined(); + return [ + { name: 'safe outputs', script: safeOutputsStep.with.script }, + { name: 'final verdict', script: finalGateStep.with.script }, + ]; } async function executeCompiledGuardLoader( script: string, { baseGuard, + baseManifest = false, workflowGuard, head = 'a'.repeat(40), - }: { baseGuard?: string; workflowGuard: string; head?: string }, + }: { baseGuard?: string; baseManifest?: boolean; workflowGuard: string; head?: string }, ): Promise { const workspace = mkdtempSync(resolve(ROOT, '.squad-review-loader-')); compileWorkspaces.push(workspace); @@ -202,6 +213,15 @@ async function executeCompiledGuardLoader( mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, baseGuard); } + if (baseManifest) { + const path = resolve( + workspace, + '.squad-review-base', + '.github/aw/squad-workflows.manifest.json', + ); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, '{}\n'); + } const workflowPath = resolve(workspace, '.squad-review-workflow', guardPath); mkdirSync(dirname(workflowPath), { recursive: true }); writeFileSync(workflowPath, workflowGuard); @@ -419,32 +439,49 @@ describe('gh-aw enforcing Squad reviewer', () => { it('executes the compiled first-install fallback without weakening established base control', async () => { const { lock } = compileReviewer(); - const script = safeOutputsGuardScript(lock); const stub = (source: string) => ` import { writeFileSync } from 'node:fs'; export async function enforceReviewOutputs(env) { writeFileSync(process.env.SQUAD_TEST_GUARD_OUTPUT, - JSON.stringify({ source: ${JSON.stringify(source)}, head: env.SQUAD_REVIEW_HEAD })); + JSON.stringify({ source: ${JSON.stringify(source)}, operation: 'safe outputs', head: env.SQUAD_REVIEW_HEAD })); + } + export async function assertClearingReview(env) { + writeFileSync(process.env.SQUAD_TEST_GUARD_OUTPUT, + JSON.stringify({ source: ${JSON.stringify(source)}, operation: 'final verdict', head: env.SQUAD_REVIEW_HEAD })); } `; - for (const head of [ - 'edd6fbf0f84334ff086ed2bc8bbfed5503fdd56f', - '9a898b5182ca432b62bef26dbf1f9022dd1d256b', - ]) { + for (const { name, script } of compiledGuardScripts(lock)) { + const head = name === 'safe outputs' + ? 'edd6fbf0f84334ff086ed2bc8bbfed5503fdd56f' + : '9a898b5182ca432b62bef26dbf1f9022dd1d256b'; + await expect(executeCompiledGuardLoader(script, { + workflowGuard: stub('workflow'), + head, + })).resolves.toBe(JSON.stringify({ source: 'workflow', operation: name, head })); + + await expect(executeCompiledGuardLoader(script, { + baseGuard: stub('base'), + baseManifest: true, + workflowGuard: stub('workflow'), + head, + })).resolves.toBe(JSON.stringify({ source: 'base', operation: name, head })); + + await expect(executeCompiledGuardLoader(script, { + baseManifest: true, + workflowGuard: stub('workflow'), + head, + })).rejects.toThrow( + 'Squad Review guard is missing from an established base installation. Refusing workflow-source fallback.', + ); + await expect(executeCompiledGuardLoader(script, { + baseGuard: 'this is not valid JavaScript', + baseManifest: true, workflowGuard: stub('workflow'), head, - })).resolves.toBe(JSON.stringify({ source: 'workflow', head })); + })).rejects.toThrow(); } - await expect(executeCompiledGuardLoader(script, { - baseGuard: stub('base'), - workflowGuard: stub('workflow'), - })).resolves.toBe(JSON.stringify({ source: 'base', head: 'a'.repeat(40) })); - await expect(executeCompiledGuardLoader(script, { - baseGuard: 'this is not valid JavaScript', - workflowGuard: stub('workflow'), - })).rejects.toThrow(); }, 30000); it('kills mutations of every important authority and provenance gate', () => { diff --git a/workflows/package/squad-review.md b/workflows/package/squad-review.md index 512ec9b80..4abbe4c70 100644 --- a/workflows/package/squad-review.md +++ b/workflows/package/squad-review.md @@ -74,9 +74,21 @@ safe-outputs: const { pathToFileURL } = require('node:url'); const baseGuard = `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs`; + const baseManifest = + `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/aw/squad-workflows.manifest.json`; const workflowGuard = `${process.env.GITHUB_WORKSPACE}/.squad-review-workflow/.github/workflows/shared/squad-review-guard.mjs`; - const guard = await import(pathToFileURL(existsSync(baseGuard) ? baseGuard : workflowGuard).href); + let guardPath; + if (existsSync(baseGuard)) { + guardPath = baseGuard; + } else if (!existsSync(baseManifest)) { + guardPath = workflowGuard; + } else { + throw new Error( + 'Squad Review guard is missing from an established base installation. Refusing workflow-source fallback.' + ); + } + const guard = await import(pathToFileURL(guardPath).href); await guard.enforceReviewOutputs(process.env, async (route, fields) => (await github.request(`GET /${route}`, fields)).data); add-comment: @@ -142,9 +154,21 @@ jobs: const { pathToFileURL } = require('node:url'); const baseGuard = `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs`; + const baseManifest = + `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/aw/squad-workflows.manifest.json`; const workflowGuard = `${process.env.GITHUB_WORKSPACE}/.squad-review-workflow/.github/workflows/shared/squad-review-guard.mjs`; - const guard = await import(pathToFileURL(existsSync(baseGuard) ? baseGuard : workflowGuard).href); + let guardPath; + if (existsSync(baseGuard)) { + guardPath = baseGuard; + } else if (!existsSync(baseManifest)) { + guardPath = workflowGuard; + } else { + throw new Error( + 'Squad Review guard is missing from an established base installation. Refusing workflow-source fallback.' + ); + } + const guard = await import(pathToFileURL(guardPath).href); await guard.assertClearingReview(process.env, async (route, fields) => (await github.request(`GET /${route}`, fields)).data); --- diff --git a/workflows/squad-review.md b/workflows/squad-review.md index 96a4b67ca..a9ce2e8ee 100644 --- a/workflows/squad-review.md +++ b/workflows/squad-review.md @@ -72,9 +72,21 @@ safe-outputs: const { pathToFileURL } = require('node:url'); const baseGuard = `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs`; + const baseManifest = + `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/aw/squad-workflows.manifest.json`; const workflowGuard = `${process.env.GITHUB_WORKSPACE}/.squad-review-workflow/.github/workflows/shared/squad-review-guard.mjs`; - const guard = await import(pathToFileURL(existsSync(baseGuard) ? baseGuard : workflowGuard).href); + let guardPath; + if (existsSync(baseGuard)) { + guardPath = baseGuard; + } else if (!existsSync(baseManifest)) { + guardPath = workflowGuard; + } else { + throw new Error( + 'Squad Review guard is missing from an established base installation. Refusing workflow-source fallback.' + ); + } + const guard = await import(pathToFileURL(guardPath).href); await guard.enforceReviewOutputs(process.env, async (route, fields) => (await github.request(`GET /${route}`, fields)).data); add-comment: @@ -137,9 +149,21 @@ jobs: const { pathToFileURL } = require('node:url'); const baseGuard = `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/workflows/shared/squad-review-guard.mjs`; + const baseManifest = + `${process.env.GITHUB_WORKSPACE}/.squad-review-base/.github/aw/squad-workflows.manifest.json`; const workflowGuard = `${process.env.GITHUB_WORKSPACE}/.squad-review-workflow/.github/workflows/shared/squad-review-guard.mjs`; - const guard = await import(pathToFileURL(existsSync(baseGuard) ? baseGuard : workflowGuard).href); + let guardPath; + if (existsSync(baseGuard)) { + guardPath = baseGuard; + } else if (!existsSync(baseManifest)) { + guardPath = workflowGuard; + } else { + throw new Error( + 'Squad Review guard is missing from an established base installation. Refusing workflow-source fallback.' + ); + } + const guard = await import(pathToFileURL(guardPath).href); await guard.assertClearingReview(process.env, async (route, fields) => (await github.request(`GET /${route}`, fields)).data); --- diff --git a/workflows/squad-workflows.manifest.json b/workflows/squad-workflows.manifest.json index 3432b865a..2bb0c4e5d 100644 --- a/workflows/squad-workflows.manifest.json +++ b/workflows/squad-workflows.manifest.json @@ -28,7 +28,7 @@ "source": "workflows/package/squad-review.md", "destination": ".github/workflows/squad-review.md", "lock": ".github/workflows/squad-review.lock.yml", - "source_sha256": "fa48adb2ff79d1afcd3499d48eefc37127f962f69f3efabcb08a9553ef065bae" + "source_sha256": "cae3f9ca8a32ca76cfd873941fafe38319fa53b35fb3478c2338732b8b9f63fd" }, { "name": "squad-deps-worker",