Repository navigation
720 lines (693 loc) · 32.9 KB
/
Copy pathsquad-ci.yml
File metadata and controls
720 lines (693 loc) · 32.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
name: Squad CI
on:
pull_request:
# squad2-* / stack-* are stacked-PR bases: run the same gates on each layer's own head.
branches: [dev, main, "squad2-*", "stack-*"]
types: [opened, synchronize, reopened, edited]
push:
branches: [dev]
permissions:
contents: read
pull-requests: read
# Prevent parallel runs from competing for resources
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Path filter for conditional job execution ──────────────────────────
changes:
runs-on: ubuntu-latest
timeout-minutes: 3
outputs:
docs: ${{ steps.filter.outputs.docs }}
code: ${{ steps.filter.outputs.code }}
workflows: ${{ steps.filter.outputs.workflows }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
fetch-depth: 0
- name: Detect changed paths
id: filter
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
CHANGED=$(git diff --name-only "$BASE"..."$HEAD")
else
# On push, compare against parent commit
CHANGED=$(git diff --name-only HEAD~1...HEAD 2>/dev/null || echo "docs/")
fi
if echo "$CHANGED" | grep -qE '^(docs/|README\.md|\.markdownlint|\.cspell|cspell\.json)'; then
echo "docs=true" >> "$GITHUB_OUTPUT"
else
echo "docs=false" >> "$GITHUB_OUTPUT"
fi
if echo "$CHANGED" | grep -qvE '^(docs/|README\.md|\.markdownlint|\.cspell|cspell\.json)'; then
echo "code=true" >> "$GITHUB_OUTPUT"
else
echo "code=false" >> "$GITHUB_OUTPUT"
fi
if echo "$CHANGED" | grep -qE '^\.github/workflows/'; then
echo "workflows=true" >> "$GITHUB_OUTPUT"
else
echo "workflows=false" >> "$GITHUB_OUTPUT"
fi
docs-quality:
needs: changes
if: "!cancelled() && (github.event_name == 'push' || needs.changes.outputs.docs == 'true')"
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22.23.4'
cache: 'npm'
# Docs-only pull requests skip the `test` job, so the docs site location guard runs here
# as well. The suite uses Node built-ins only, so it needs no npm install.
- name: Docs feature and site location tests
run: node --test docs/tests/astro-features.test.mjs
- name: Install docs tools
run: |
success=false
for i in 1 2 3; do
if npm install --no-save markdownlint-cli2 cspell; then success=true; break; fi
echo "Retry $i/3 — npm install failed, retrying in 5s..."; sleep 5
done
[ "$success" = true ] || { echo "::error::npm install failed after 3 attempts"; exit 1; }
- name: Lint docs markdown
run: npx markdownlint-cli2
- name: Spell check docs
run: npx cspell --no-progress --dot "docs/src/content/**/*.md" "README.md"
test:
needs: changes
# Fail-open: run test if changes job failed (don't let path filter break testing)
if: >-
always() && !cancelled()
&& (github.event_name == 'push'
|| needs.changes.result != 'success'
|| needs.changes.outputs.code == 'true')
runs-on: ubuntu-latest
# Setup alone (npm installs + Playwright browser download) has taken 10-12 min on slow
# runners, cancelling the test run mid-way; the suite itself needs ~6 min.
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22.23.4'
cache: 'npm'
- name: Fix stale lockfile entries
run: node scripts/release/workspace-lockfile.mjs fix
- name: Install dependencies
run: |
success=false
for i in 1 2 3; do
if npm install; then success=true; break; fi
echo "Retry $i/3 — npm install failed, retrying in 5s..."; sleep 5
done
[ "$success" = true ] || { echo "::error::npm install failed after 3 attempts"; exit 1; }
- name: Install docs dependencies
run: |
success=false
for i in 1 2 3; do
if npm ci; then success=true; break; fi
echo "Retry $i/3 — npm ci failed, retrying in 5s..."; sleep 5
done
[ "$success" = true ] || { echo "::error::npm ci failed after 3 attempts"; exit 1; }
working-directory: docs
- name: Resolve Playwright version
id: playwright
run: echo "version=$(node -p "require('playwright/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}
- name: Install Playwright browsers
# Browser download is the slow part; with a cache hit only the OS deps are installed.
run: |
if [ "${{ steps.playwright-cache.outputs.cache-hit }}" = "true" ]; then
npx playwright install-deps chromium
else
npx playwright install chromium --with-deps
fi
- name: "🔒 Source tree canary check"
run: |
MISSING=0
for f in \
"packages/squad-sdk/src/index.ts" \
"packages/squad-cli/src/cli/index.ts" \
"packages/squad-sdk/package.json" \
"packages/squad-cli/package.json"; do
if [ ! -f "$f" ]; then
echo "::error::MISSING critical file: $f"
MISSING=$((MISSING + 1))
fi
done
if [ $MISSING -gt 0 ]; then
echo "::error::$MISSING critical source files missing — possible accidental deletion"
exit 1
fi
echo "✅ All critical source files present"
- name: "🔒 Large deletion guard"
if: github.event_name == 'pull_request'
run: |
DELETED=$(git diff --diff-filter=D --name-only origin/${{ github.base_ref }}...HEAD | wc -l)
echo "Files deleted in this PR: $DELETED"
if [ "$DELETED" -gt 50 ]; then
echo "::error::This PR deletes $DELETED files (threshold: 50). If intentional, add the 'large-deletion-approved' label."
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name' 2>/dev/null || echo "")
if echo "$LABELS" | grep -q "large-deletion-approved"; then
echo "✅ Large deletion approved via label"
else
exit 1
fi
fi
env:
GH_TOKEN: ${{ github.token }}
- name: Install gh-aw extension
# Required so `gh aw --version` succeeds and the compiled-workflow gates in
# test/gh-aw-quality.test.ts can compile squad.md. Those gates FAIL CLOSED:
# if gh aw is absent they hard-fail with this exact install command, rather
# than skipping — a skipped gate is indistinguishable from no gate. This
# includes the shell input security contract gate (#1834) and the compile
# contract (#1732).
run: gh extension install --pin v0.91.5 github/gh-aw
env:
GH_TOKEN: ${{ github.token }}
- name: Build
run: npm run build
- name: Verify product/team isolation
run: npm run verify:team-isolation
- name: Type-check
run: npm run lint
- name: Lint source and tests
run: npm run lint:eslint
- name: Run tests
run: npm test
# Skip labels: skip-changelog, skip-exports-check, skip-samples-ci,
# skip-workspace-check, skip-version-check, skip-export-smoke, large-deletion-approved
# ── Changeset drift gate (#1273) ─────────────────────────────────────────
# Alarms when unreleased changeset fragments accumulate (count or age),
# i.e. the release flow stopped consuming them. Warn-only on PRs (the PR
# author isn't the one who can fix release cadence), hard fail on dev pushes.
changeset-drift:
name: Changeset Drift
if: vars.SQUAD_CHANGESET_DRIFT_CHECK != 'false'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
fetch-depth: 0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22.23.4'
- name: Check changeset drift
run: node scripts/check-changeset-drift.mjs --mode=${{ github.event_name == 'push' && 'fail' || 'warn' }}
# ── Shebang line-ending guard ───────────────────────────────────────────
# A CRLF shebang breaks POSIX execution and defeats bundler shebang stripping,
# which silently zeroes out any vitest suite importing the file (#1788). Linux
# runners check out LF, so this class is invisible here unless the committed
# blob itself is inspected -- which is what the script does.
shebang-eol:
name: Shebang EOL
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22.23.4'
- name: Check shebang line endings
run: node scripts/check-shebang-eol.mjs
# ── Consolidated policy gates ───────────────────────────────────────────
# Runs changelog, changelog-protection, workspace-integrity,
# prerelease-version-guard, publish-policy, publish-allowlist and scope-check on
# one runner. PR-diff gates (changelog, write protection, scope) are PR-only; the
# tree-level gates also run on every dev push through scripts/release/policy-gates.mjs,
# which the release path (squad-release-verify.yml) executes again at the exact
# release SHA with no label or variable bypass.
policy-gates:
name: Policy Gates
needs: changes
if: >-
!cancelled()
&& (github.event_name == 'push'
|| needs.changes.result != 'success'
|| needs.changes.outputs.code == 'true'
|| needs.changes.outputs.workflows == 'true')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
fetch-depth: 0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22.23.4'
- name: Fetch PR labels
id: labels
if: github.event_name == 'pull_request'
run: |
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name' 2>/dev/null || echo "")
echo "all<<EOF" >> "$GITHUB_OUTPUT"
echo "$LABELS" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "$LABELS" | grep -q "skip-changelog" && echo "skip_changelog=true" >> "$GITHUB_OUTPUT" || echo "skip_changelog=false" >> "$GITHUB_OUTPUT"
echo "$LABELS" | grep -q "skip-workspace-check" && echo "skip_workspace=true" >> "$GITHUB_OUTPUT" || echo "skip_workspace=false" >> "$GITHUB_OUTPUT"
echo "$LABELS" | grep -q "skip-version-check" && echo "skip_version=true" >> "$GITHUB_OUTPUT" || echo "skip_version=false" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
# ─── Changelog Gate ────────────────────────────────────────────────
- name: "Gate: Changelog"
if: >-
always()
&& github.event_name == 'pull_request'
&& steps.labels.outputs.skip_changelog != 'true'
&& vars.SQUAD_CHANGELOG_CHECK != 'false'
run: |
echo "## 📋 Changelog Gate" >> "$GITHUB_STEP_SUMMARY"
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
CHANGED=$(git diff --name-only "$BASE"..."$HEAD")
# Paths whose changes ship behavioral or scaffolding updates and therefore
# require a changeset (or CHANGELOG update). The top-level templates/ tree is
# the primary sync target of .squad-templates/ (see scripts/sync-templates.mjs) —
# an append-only superset whose entire tree is governed squad scaffolding, not
# unrelated content. Live .squad/ team state is deliberately excluded:
# re-casting a team must not require a product package changeset.
SDK_CLI_PATH_REGEX='^packages/squad-(sdk|cli)/src/|^packages/squad-(sdk|cli)/templates/|^\.squad-templates/|^templates/'
SDK_CLI_CHANGED=$(echo "$CHANGED" | grep -E "$SDK_CLI_PATH_REGEX" || true)
if [ -z "$SDK_CLI_CHANGED" ]; then
echo "✅ Not applicable (no SDK/CLI source or template changes)" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "SDK/CLI source or template files changed:"
echo "$SDK_CLI_CHANGED"
CHANGESET_ADDED=$(git diff --diff-filter=AM --name-only "$BASE"..."$HEAD" | grep -E '^\.changeset/[^/]+\.md$' | grep -vxF '.changeset/README.md' || true)
CHANGELOG_CHANGED=$(echo "$CHANGED" | grep -E '^CHANGELOG\.md$' || true)
if [ -n "$CHANGESET_ADDED" ]; then
echo "✅ Changeset file detected" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ -n "$CHANGELOG_CHANGED" ]; then
echo "✅ CHANGELOG.md updated" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "::error::No changeset or CHANGELOG.md update found, but SDK/CLI source or template files were changed."
echo "::error::Run 'npx changeset add' or edit CHANGELOG.md. Escape hatch: add 'skip-changelog' label."
echo "❌ No changeset or CHANGELOG.md update" >> "$GITHUB_STEP_SUMMARY"
exit 1
# ─── CHANGELOG Write Protection ────────────────────────────────────
- name: "Gate: CHANGELOG Write Protection"
if: always() && github.event_name == 'pull_request'
env:
APPROVED_AUTHORS: 'bradygaster tamirdresher github-actions[bot] copilot-swe-agent[bot]'
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
echo "## 🔒 CHANGELOG Write Protection" >> "$GITHUB_STEP_SUMMARY"
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '^CHANGELOG.md$' || true)
if [ -n "$CHANGED" ]; then
if echo "$APPROVED_AUTHORS" | tr ' ' '\n' | grep -qxF "$PR_AUTHOR"; then
echo "✅ $PR_AUTHOR is approved" >> "$GITHUB_STEP_SUMMARY"
else
echo "::error::$PR_AUTHOR is not approved to modify CHANGELOG.md directly. Use 'npx changeset add' instead."
echo "❌ $PR_AUTHOR is not approved" >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
else
echo "✅ CHANGELOG.md not modified" >> "$GITHUB_STEP_SUMMARY"
fi
# ─── Workspace Integrity ───────────────────────────────────────────
- name: "Gate: Workspace Integrity"
if: >-
always()
&& steps.labels.outputs.skip_workspace != 'true'
&& vars.SQUAD_WORKSPACE_CHECK != 'false'
run: |
echo "## 🔗 Workspace Integrity" >> "$GITHUB_STEP_SUMMARY"
node scripts/release/workspace-lockfile.mjs check
echo "✅ All workspace packages resolve to local links" >> "$GITHUB_STEP_SUMMARY"
# ─── Prerelease Version Guard ──────────────────────────────────────
- name: "Gate: Prerelease Version Guard"
if: >-
always()
&& steps.labels.outputs.skip_version != 'true'
&& vars.SQUAD_VERSION_CHECK != 'false'
&& true
run: |
echo "## 🏷️ Prerelease Version Guard" >> "$GITHUB_STEP_SUMMARY"
node scripts/release/check-prerelease-versions.mjs
echo "✅ All versions are release versions" >> "$GITHUB_STEP_SUMMARY"
# ─── Publish Policy ────────────────────────────────────────────────
- name: "Gate: Workspace-scoped npm publish"
if: always()
run: |
VIOLATIONS=0
for wf in .github/workflows/*.yml; do
BARE=$(awk '
/^[[:space:]]*#/ { next }
{
command = $0
sub(/^[[:space:]]*-[[:space:]]*/, "", command)
sub(/^run:[[:space:]]*/, "", command)
if (command ~ /(^|&&|\|\||;)[[:space:]]*((if|then)[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]+[[:space:]]+)*npm[[:space:]][^#;&|]*publish([[:space:]]|$)/ &&
command !~ /(^|[[:space:]])(-w|--workspace)(=|[[:space:]]|$)/ &&
command !~ /npm publish "\$PACKS_DIR\/bradygaster-squad-(sdk|cli)-\$\{VERSION\}\.tgz"/) {
print NR ":" $0
}
}
' "$wf")
if [ -n "$BARE" ]; then
echo "::error file=$wf::Unsanctioned npm publish found (requires -w/--workspace or an exact verified Squad tarball path):"
echo "$BARE"
VIOLATIONS=1
fi
done
if [ "$VIOLATIONS" -eq 1 ]; then
echo "::error::PUBLISH POLICY VIOLATION — npm publish must be workspace-scoped or use one of the two exact verified Squad tarball paths"
exit 1
fi
echo "✅ All npm publish commands are workspace-scoped or exact verified Squad tarball paths"
# ─── Release policy (shared with the release path; no bypass) ───────
- name: "Gate: Release policy (workspace integrity, prerelease guard, publish allowlist, changelog heading)"
if: always()
run: |
set -o pipefail
echo "## 🛡️ Release policy" >> "$GITHUB_STEP_SUMMARY"
node scripts/release/policy-gates.mjs | tee -a "$GITHUB_STEP_SUMMARY"
- name: "Gate: Lockfiles resolve only to registry.npmjs.org"
if: always()
run: node scripts/release/check-lockfile-registry.mjs
# ─── Scope Check (repo-health PRs only) ────────────────────────────
- name: "Gate: Repo-health scope boundary"
if: >-
always()
&& github.event_name == 'pull_request'
&& contains(github.event.pull_request.labels.*.name, 'repo-health')
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
CHANGED=$(git diff --name-only "$BASE"..."$HEAD" | grep -E '^packages/squad-(cli|sdk)/src/' || true)
if [ -n "$CHANGED" ]; then
echo "::error::SCOPE VIOLATION — repo-health PRs must not modify product source code."
echo "$CHANGED" | while read -r f; do echo " - $f"; done
echo "Use a 'fix' or 'feat' label instead for product source changes."
exit 1
fi
echo "✅ No product source files modified — scope boundary respected"
# ── SDK exports validation ──────────────────────────────────────────────
# Merged gate: validates exports map config AND built artifact resolution.
sdk-exports-validation:
needs: changes
if: >-
!cancelled()
&& (github.event_name != 'pull_request'
|| needs.changes.outputs.code == 'true'
|| needs.changes.result == 'failure')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
fetch-depth: 0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22.23.4'
- name: Check skip conditions
id: gate
run: |
SKIP_MAP="false"
SKIP_SMOKE="false"
if [ "${{ vars.SQUAD_EXPORTS_CHECK }}" = "false" ]; then SKIP_MAP="true"; fi
if [ "${{ vars.SQUAD_EXPORT_SMOKE }}" = "false" ]; then SKIP_SMOKE="true"; fi
if [ "${{ github.event_name }}" = "pull_request" ]; then
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name' 2>/dev/null || echo "")
else
LABELS=""
fi
echo "$LABELS" | grep -q "skip-exports-check" && SKIP_MAP="true"
echo "$LABELS" | grep -q "skip-export-smoke" && SKIP_SMOKE="true"
if [ "$SKIP_MAP" = "true" ] && [ "$SKIP_SMOKE" = "true" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
echo "skip_map=$SKIP_MAP" >> "$GITHUB_OUTPUT"
echo "skip_smoke=$SKIP_SMOKE" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
- name: Check for SDK changes
if: steps.gate.outputs.skip != 'true'
id: sdk
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
else
BASE="${{ github.event.before }}"
HEAD="${{ github.sha }}"
NULL_SHA="0000000000000000000000000000000000000000"
if [ -z "$BASE" ] || [ "$BASE" = "$NULL_SHA" ]; then
HEAD="$(git rev-parse HEAD)"
if git rev-parse HEAD~1 >/dev/null 2>&1; then
BASE="$(git rev-parse HEAD~1)"
else
BASE="$HEAD"
fi
fi
fi
SDK_CHANGED=$(git diff --name-only "$BASE"..."$HEAD" | grep -E '^packages/squad-sdk/(src/|package\.json)' || true)
if [ -z "$SDK_CHANGED" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No SDK changes detected — exports validation not applicable"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "SDK files changed:"
echo "$SDK_CHANGED"
fi
- name: Verify exports map matches barrel files
if: >-
steps.gate.outputs.skip != 'true'
&& steps.gate.outputs.skip_map != 'true'
&& steps.sdk.outputs.skip != 'true'
run: node scripts/check-exports-map.mjs
- name: Install and build SDK
if: >-
steps.gate.outputs.skip != 'true'
&& steps.gate.outputs.skip_smoke != 'true'
&& steps.sdk.outputs.skip != 'true'
run: |
npm ci --ignore-scripts
node packages/squad-cli/scripts/patch-esm-imports.mjs
npm run build -w packages/squad-sdk
- name: Smoke test all subpath exports
if: >-
steps.gate.outputs.skip != 'true'
&& steps.gate.outputs.skip_smoke != 'true'
&& steps.sdk.outputs.skip != 'true'
run: node scripts/release/smoke-sdk-exports.mjs
samples-build:
needs: changes
if: >-
!cancelled()
&& (github.event_name != 'pull_request'
|| needs.changes.outputs.code == 'true'
|| needs.changes.result == 'failure')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
with:
fetch-depth: 0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22.23.4'
cache: 'npm'
cache-dependency-path: |
package-lock.json
samples/**/package-lock.json
- name: Check skip conditions
id: gate
run: |
SKIP="false"
if [ "${{ vars.SQUAD_SAMPLES_CI }}" = "false" ]; then
SKIP="true"
echo "Samples build disabled via vars.SQUAD_SAMPLES_CI"
fi
LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}'
if echo "$LABELS" | grep -q "skip-samples-ci"; then
SKIP="true"
echo "Skipping (skip-samples-ci label)"
fi
echo "skip=$SKIP" >> "$GITHUB_OUTPUT"
- name: Check for SDK source changes
if: steps.gate.outputs.skip != 'true'
id: sdk
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
else
BASE="${{ github.event.before }}"
HEAD="${{ github.sha }}"
NULL_SHA="0000000000000000000000000000000000000000"
if [ -z "$BASE" ] || [ "$BASE" = "$NULL_SHA" ]; then
HEAD="$(git rev-parse HEAD)"
if git rev-parse HEAD~1 >/dev/null 2>&1; then
BASE="$(git rev-parse HEAD~1)"
else
BASE="$HEAD"
fi
fi
fi
SDK_CHANGED=$(git diff --name-only "$BASE"..."$HEAD" | grep -E '^packages/squad-sdk/src/' || true)
if [ -z "$SDK_CHANGED" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No SDK source changes — samples build not applicable"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Install root dependencies and build SDK
if: steps.gate.outputs.skip != 'true' && steps.sdk.outputs.skip != 'true'
run: |
npm ci --ignore-scripts
node packages/squad-cli/scripts/patch-esm-imports.mjs
npm run build -w packages/squad-sdk
- name: Build and test samples
if: steps.gate.outputs.skip != 'true' && steps.sdk.outputs.skip != 'true'
run: |
FAILED=0
PASSED=0
SKIPPED=0
for sample_dir in samples/*/; do
sample_dir="${sample_dir%/}"
sample=$(basename "$sample_dir")
if [ ! -f "$sample_dir/package.json" ]; then
SKIPPED=$((SKIPPED + 1))
continue
fi
HAS_BUILD=$(node -e "const p=require('./$sample_dir/package.json'); process.exit(p.scripts?.build ? 0 : 1)" 2>/dev/null && echo "true" || echo "false")
HAS_TEST=$(node -e "const p=require('./$sample_dir/package.json'); process.exit(p.scripts?.test ? 0 : 1)" 2>/dev/null && echo "true" || echo "false")
if [ "$HAS_BUILD" = "false" ] && [ "$HAS_TEST" = "false" ]; then
SKIPPED=$((SKIPPED + 1))
continue
fi
echo "[$sample] Installing..."
if ! (cd "$sample_dir" && npm install --ignore-scripts 2>&1); then
echo "::error::[$sample] npm install failed"
FAILED=$((FAILED + 1))
continue
fi
if [ "$HAS_BUILD" = "true" ]; then
echo "[$sample] Building..."
if ! (cd "$sample_dir" && npm run build 2>&1); then
echo "::error::[$sample] build failed"
FAILED=$((FAILED + 1))
continue
fi
fi
if [ "$HAS_TEST" = "true" ]; then
echo "[$sample] Testing..."
if ! (cd "$sample_dir" && npm test 2>&1); then
echo "::error::[$sample] test failed"
FAILED=$((FAILED + 1))
continue
fi
fi
PASSED=$((PASSED + 1))
done
echo "Samples: $PASSED passed, $FAILED failed, $SKIPPED skipped"
if [ "$FAILED" -gt 0 ]; then
echo "::error::$FAILED sample(s) failed build/test validation"
exit 1
fi
# ── gh-aw strict compile gate ────────────────────────────────────────────
# Compiles all eight Squad-authored workflow sources with --strict against a
# pinned compiler so the delivered stack is always proven compilable. All
# eight workflows, including squad-improvement-worker, squad-bootstrap, and squad-command-router, are distributed by the
# gh-aw-enlistment bootstrap skill/docs as part of the standard install --
# squad-improvement-worker stays dormant (approval-gated) until a maintainer
# authorizes a governance-scoped retrospective proposal, but it is not a
# separate opt-in add-on.
# Runs unconditionally on every PR / push so it can be used as a required
# status check (check name: "Squad CI / gh-aw strict compile").
#
# EXTERNAL ACTION REQUIRED after this PR merges to close the
# merge-with-failed-checks gap that allowed #1873 and #1874 to merge:
# 1. https://github.com/github/squad/settings/rules/12703724
# 2. Set enforcement → Active
# 3. Conditions → branch patterns: refs/heads/dev AND refs/heads/main
# 4. Rules → Required status checks → add:
# "Squad CI / gh-aw strict compile"
# "Squad CI / test"
# Rulesets are external GitHub configuration; they cannot be enforced from
# repo code.
gh-aw-compile:
name: gh-aw strict compile
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 #v6
with:
node-version: '22.23.4'
cache: npm
- name: Install verifier dependencies
run: npm ci --ignore-scripts --registry=https://registry.npmjs.org/
- name: Install gh-aw v0.91.5 (pinned)
run: |
gh extension install --force --pin v0.91.5 github/gh-aw
test "$(gh aw --version 2>&1 | awk '{print $NF}')" = v0.91.5
env:
GH_TOKEN: ${{ github.token }}
- name: Record compiler version
run: |
echo "### gh-aw compiler" >> "$GITHUB_STEP_SUMMARY"
gh aw --version | tee -a "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
- name: Verify canonical package manifest and generated digests
run: npm run gh-aw:integrity:check
- name: Verify a clean native package consumer
env:
SQUAD_SOURCE_REVISION: ${{ github.sha }}
run: |
set -euo pipefail
CONSUMER_DIR="$GITHUB_WORKSPACE/.gh-aw-consumer"
rm -rf "$CONSUMER_DIR"
trap 'rm -rf "$CONSUMER_DIR"' EXIT
mkdir -p "$CONSUMER_DIR"
git -C "$CONSUMER_DIR" init --quiet
git -C "$CONSUMER_DIR" remote add origin https://github.com/example/squad-consumer.git
(
cd "$CONSUMER_DIR"
mkdir -p .github/workflows
for workflow in squad squad-implement-worker squad-review squad-deps-worker squad-retro squad-improvement-worker squad-bootstrap squad-command-router; do
cp "$GITHUB_WORKSPACE/workflows/package/${workflow}.md" \
".github/workflows/${workflow}.md"
done
gh aw add --force "$GITHUB_WORKSPACE/workflows"
rm -f .github/skills/agentic-workflows/SKILL.md
node .github/workflows/shared/squad-install-verifier.mjs \
--write-local-test-ownership \
--source-revision "$SQUAD_SOURCE_REVISION"
node .github/workflows/shared/squad-install-verifier.mjs --materialize-runtime
test "$(find .github/workflows -maxdepth 1 -name 'squad*.md' | wc -l | tr -d ' ')" = 8
test "$(find .github/workflows/shared -type f ! -name 'squad-bootstrap-trigger-probe.json' | wc -l | tr -d ' ')" = 20
test -f .github/workflows/squad-agent-binding-check.yml
test "$(find .github/skills/gh-aw-enlistment -type f | wc -l | tr -d ' ')" = 1
node "$GITHUB_WORKSPACE/scripts/release/compile-pinned-actions.mjs"
gh aw compile --strict --approve --no-check-update
for workflow in squad squad-implement-worker squad-review squad-deps-worker squad-retro squad-improvement-worker squad-bootstrap squad-command-router; do
gh aw compile "$workflow" --strict --no-check-update
test -f ".github/workflows/${workflow}.lock.yml"
done
gh aw compile --strict --no-check-update
node .github/workflows/shared/squad-install-verifier.mjs \
--verify-install \
--source-revision "$SQUAD_SOURCE_REVISION" \
--strict-compile
)