Skip to content

deps: Bump the minor-patch group across 1 directory with 21 updates #6019

deps: Bump the minor-patch group across 1 directory with 21 updates

deps: Bump the minor-patch group across 1 directory with 21 updates #6019

Workflow file for this run

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
)