Repository navigation
deps: Bump the minor-patch group across 1 directory with 21 updates #6019
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| ) |