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
45 changes: 43 additions & 2 deletions .github/workflows/squad-workflow-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 66 to +70

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradygaster see the copilot comment

# 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: |
Expand Down
34 changes: 33 additions & 1 deletion test/template-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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([]);
Comment on lines +211 to +214
expect(files, '.github/agents/ must contain squad.agent.md').toContain(AGENT_MD_FILE);
});
});

Expand Down
Loading