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..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', () => { @@ -175,12 +190,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); }); });