From a72a13499e6bc22bb15dfee34fe7e43a6a800e7e Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Tue, 4 Aug 2026 11:01:46 -0700 Subject: [PATCH 1/2] fix(ci): skip DO-NOT-EDIT gh-aw generated workflows in lint; widen agents guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actionlint v1.7.12 is the current latest release — upgrading cannot fix this. The tool lags GitHub's evolving feature set on two specific constructs: • 'copilot-requests: write' — a real current Actions permission scope used by GitHub's own gh-aw toolchain. • 'concurrency.queue: max' — a newer Actions concurrency feature. The files triggering these errors are DO-NOT-EDIT compiler output from gh-aw's 'gh aw compile' toolchain (*.lock.yml) and gh-aw's code generators (e.g. agentics-maintenance.yml). Editing them is not acceptable — they are clobbered on each compile/generate cycle and are validated upstream by gh-aw's own toolchain. Fix 1 (.github/workflows/squad-workflow-lint.yml): Detect generated files by their header ('automatically generated' / 'DO NOT EDIT' within the first 5 lines) and skip them. Header-based detection automatically covers future gh-aw generated files without manual maintenance. Hand-authored workflows are always linted. Verified with a sabotage test: intentionally broken hand-authored workflow still fails the gate. Skipped files are echoed in CI logs for debuggability. Fix 2 (test/template-sync.test.ts): The '.github/agents/ negative guard' asserted strict directory equality, which over-reaches. 'gh aw init' legitimately installs its own agentic-workflows.md in that directory. Narrowed the guard to its actual intent: squad.agent.md must be present, no *unexpected* files allowed, with an explicit allowlist of known non-Squad agents. A truly random stray file still fails the test. Unblocks PRs #1587 and #1592 (does NOT close them). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/squad-workflow-lint.yml | 45 ++++++++++++++++++++++- test/template-sync.test.ts | 19 +++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/.github/workflows/squad-workflow-lint.yml b/.github/workflows/squad-workflow-lint.yml index 920651424..30c78d443 100644 --- a/.github/workflows/squad-workflow-lint.yml +++ b/.github/workflows/squad-workflow-lint.yml @@ -65,8 +65,49 @@ jobs: - name: Lint .github/workflows/*.yml run: | - echo "--- Linting .github/workflows/ ---" - actionlint .github/workflows/*.yml + # Why we skip machine-generated files: + # + # actionlint v1.7.12 is the current latest release and CANNOT be upgraded to fix + # these; the tool simply lags GitHub's evolving feature set. Two known gaps: + # + # • "copilot-requests: write" — a real, current GitHub Actions permission scope + # used by GitHub's own gh-aw (Agentic Workflows) toolchain. + # • "concurrency.queue: max" — a newer real Actions concurrency feature. + # + # The files that trigger these errors are DO-NOT-EDIT compiler output produced by + # gh-aw's `gh aw compile` toolchain (*.lock.yml files) or by gh-aw's own code + # generators (e.g. agentics-maintenance.yml). They carry a header identifying them + # as generated and are validated upstream by gh-aw's own toolchain. Editing them + # is NOT acceptable — they will be clobbered on the next compile/generate cycle. + # + # Detection strategy: skip any workflow whose first 5 lines contain "automatically + # generated" or "DO NOT EDIT". This is header-based rather than filename-based so + # it automatically covers future gh-aw generated files without manual maintenance. + # Hand-authored workflows never carry such a header, so they always get linted. + + echo "--- Linting .github/workflows/ (skipping DO-NOT-EDIT generated files) ---" + skipped=() + to_lint=() + for f in .github/workflows/*.yml; do + if head -5 "$f" | grep -qiE "(automatically generated|DO NOT EDIT)"; then + skipped+=("$f") + else + to_lint+=("$f") + fi + done + + if [ ${#skipped[@]} -gt 0 ]; then + echo "⏭ Skipped (DO-NOT-EDIT generated files — validated by upstream toolchain):" + for f in "${skipped[@]}"; do echo " $f"; done + fi + + if [ ${#to_lint[@]} -gt 0 ]; then + echo "🔍 Linting ${#to_lint[@]} hand-authored workflow(s):" + for f in "${to_lint[@]}"; do echo " $f"; done + actionlint "${to_lint[@]}" + else + echo "⚠️ No hand-authored workflows found to lint." + fi - name: Lint squad-cli template workflows run: | diff --git a/test/template-sync.test.ts b/test/template-sync.test.ts index 81d776367..447467045 100644 --- a/test/template-sync.test.ts +++ b/test/template-sync.test.ts @@ -175,12 +175,29 @@ describe('sync-templates.mjs script execution', () => { // 3. Negative guard — .github/agents/ should only have squad.agent.md // --------------------------------------------------------------------------- +// Files in .github/agents/ that are NOT produced by Squad's template sync but +// are legitimately present because other toolchains share this directory: +// +// • agentic-workflows.md — installed by `gh aw init` (GitHub Agentic Workflows +// dispatcher agent). Squad does not create or own this file; gh-aw does. +// +// Add entries here when a new well-known third-party agent is introduced. +// Any file NOT in this allowlist and NOT squad.agent.md will still fail the test. +const KNOWN_NON_SQUAD_AGENTS = new Set(['agentic-workflows.md']); + describe('.github/agents/ contains only squad.agent.md', () => { it('has no files beyond squad.agent.md from the sync', () => { const agentDir = resolve(ROOT, AGENT_MD_EXTRA_TARGET); expect(existsSync(agentDir), '.github/agents/ should exist').toBe(true); const files = readdirSync(agentDir); - expect(files).toEqual([AGENT_MD_FILE]); + const unexpected = files.filter( + f => f !== AGENT_MD_FILE && !KNOWN_NON_SQUAD_AGENTS.has(f), + ); + expect( + unexpected, + `unexpected file(s) in .github/agents/ that are neither squad.agent.md nor a known third-party agent: ${unexpected.join(', ')}`, + ).toEqual([]); + expect(files, '.github/agents/ must contain squad.agent.md').toContain(AGENT_MD_FILE); }); }); From 7055f5ebb4e158e38de6f70d1c7b7b5f7f730ed9 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Tue, 4 Aug 2026 11:37:13 -0700 Subject: [PATCH 2/2] fix(test): add section-1 beforeAll sync to guard against init-scaffolding race test/init-scaffolding.test.ts runs runInit() from a subdirectory of the repo root. In monorepo mode the init command detects the git root and places squad.agent.md at .github/agents/ in the REAL repo root, then calls stampVersion() to rewrite that file in place. This runs concurrently with template-sync.test.ts in a different Vitest worker. The file-level beforeAll sync in template-sync.test.ts is correct but leaves a race window: if init-scaffolding's stampVersion write lands between the file-level sync and section 1's readFileBytes calls, the byte comparison sees the stamped file (which differs from the canonical) and fails. Fix: add a describe-scoped beforeAll inside section 1 that re-runs the sync immediately before the byte comparisons start. This collapses the race window from 'entire beforeAll + test-collection time' to near-zero. The byte-for-byte assertion itself is unchanged; this only makes the setup more resilient. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/template-sync.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/template-sync.test.ts b/test/template-sync.test.ts index 447467045..911474b69 100644 --- a/test/template-sync.test.ts +++ b/test/template-sync.test.ts @@ -118,6 +118,21 @@ const CASTING_POLICY_LOCATIONS = [ // --------------------------------------------------------------------------- describe('dynamic template enumeration (all synced files)', () => { + // Re-sync immediately before byte comparisons to guard against a race + // condition with test/init-scaffolding.test.ts. That suite runs runInit() + // with a subdirectory of the repo root as cwd; in monorepo mode the init + // command places squad.agent.md at the real git root (.github/agents/) via + // stampVersion(), which races with the file-level beforeAll sync. A + // describe-scoped beforeAll runs synchronously, right before any test in + // this suite, minimising the window to near-zero. + beforeAll(() => { + execSync('node scripts/sync-templates.mjs', { + cwd: ROOT, + encoding: 'utf-8', + timeout: 60_000, + }); + }); + const sourceFiles = collectFiles(SOURCE_DIR); it('.squad-templates/ contains files to sync', () => {