From e343e32480055a87b10cbf53f0edf2cba437bfb6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 23:12:53 +0000 Subject: [PATCH 1/2] ci: trim the PR Verify matrix to Node 24 and skip heavy jobs on docs-only PRs Measured across 8 runs today: the 3-leg Verify matrix (~7 min/leg, dominated by pnpm test) was ~75% of a PR's ~28 runner-minutes, and the starved runner pool queued main pushes 20-27 minutes. PRs now run a single Verify leg on Node 24 (newest supported LTS); pushes to main and workflow_dispatch keep the full 22.19/24/26 matrix, so main coverage is unchanged. Docs-only PRs (docs/**, agent-patterns/**, .changeset/*.md, top-level *.md) skip the four heavy jobs via a fail-open API-based classifier; nested markdown stays code because compiled SKILL.md artifacts and package markdown affect npm pack audits. pnpm store caching, Chrome reuse, and PR-cancel concurrency were already in place and are untouched. --- .github/workflows/ci.yml | 88 +++++++++++++++++++++++++++++++++++++--- docs/local-ci.md | 18 ++++++-- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bda2f9dc6..34ae96958 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,75 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + # PR-only docs allowlist: docs/**, agent-patterns/**, .changeset/*.md, and + # top-level *.md. Nested markdown elsewhere is code because examples and + # packages contain compiled SKILL.md artifacts, and package markdown affects + # npm pack audits. Classification fails open so uncertain PRs run every heavy + # job; pushes to main never skip any job based on changed paths. + changes: + if: github.event_name == 'pull_request' + name: Detect changed paths + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + pull-requests: read + outputs: + docs_only: ${{ steps.classify.outputs.docs_only }} + steps: + - name: Classify changed files + id: classify + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + + files_file="$(mktemp)" + trap 'rm -f "$files_file"' EXIT + if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \ + --paginate --jq '.[].filename' > "$files_file"; then + echo "Could not list changed files; failing open so heavy jobs run." + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + mapfile -t files < "$files_file" + if (( ${#files[@]} == 0 )); then + echo "No changed files returned; failing open so heavy jobs run." + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + docs_only=true + for f in "${files[@]}"; do + case "$f" in + docs/*|agent-patterns/*) ;; + .changeset/*.md) ;; + */*) + echo "'$f' is nested outside the docs allowlist; heavy jobs will run." + docs_only=false + break + ;; + *.md) ;; + *) + echo "'$f' is not a docs-only path; heavy jobs will run." + docs_only=false + break + ;; + esac + done + + if [[ "$docs_only" == "true" ]]; then + echo "All ${#files[@]} changed files are docs-only; heavy jobs will be skipped." + fi + echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT" + # Builds and checks every public example through its own toolchain. examples-check: - if: github.event_name != 'schedule' + needs: changes + if: >- + ${{ !cancelled() && github.event_name != 'schedule' && + (github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }} name: Examples check (Node 22.19) runs-on: ubuntu-latest timeout-minutes: 25 @@ -39,14 +105,20 @@ jobs: - run: pnpm examples:check verify: - if: github.event_name != 'schedule' + needs: changes + if: >- + ${{ !cancelled() && github.event_name != 'schedule' && + (github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }} name: Verify (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest timeout-minutes: 45 + # Measured PR Verify cost was 3 legs × ~7 minutes, about 75% of PR + # runner-minutes. PRs run newest-LTS Node 24; every main push and manual + # dispatch still gates on the full supported matrix. strategy: fail-fast: false matrix: - node-version: ['22.19.0', '24', '26'] + node-version: ${{ github.event_name == 'pull_request' && fromJSON('["24"]') || fromJSON('["22.19.0","24","26"]') }} steps: - uses: actions/checkout@v7 - uses: pnpm/setup@v2 @@ -79,7 +151,10 @@ jobs: - run: pnpm test release-gates: - if: github.event_name != 'schedule' + needs: changes + if: >- + ${{ !cancelled() && github.event_name != 'schedule' && + (github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }} name: Release gates (Node 22.19) runs-on: ubuntu-latest timeout-minutes: 30 @@ -138,7 +213,10 @@ jobs: # (hook -> RSC worker -> shared kernel state -> MCP tool lowering) without # any real Claude/Codex host. Real native-host smokes stay skip-gated in # the manually dispatched native-host-smoke workflow on purpose. - if: github.event_name != 'schedule' + needs: changes + if: >- + ${{ !cancelled() && github.event_name != 'schedule' && + (github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }} name: RSC runtime micro-eval (Node 22.19) runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/docs/local-ci.md b/docs/local-ci.md index 5dba9ba47..c453bfc91 100644 --- a/docs/local-ci.md +++ b/docs/local-ci.md @@ -2,9 +2,13 @@ `pnpm check:local-ci` proves what the hosted CI gate proves, on the development machine, in one command — including the full Node matrix. It -exists because a hosted Verify leg takes ~13–16 minutes while a many-core -development machine can run all three legs plus the release gates -concurrently in less wall time. The local-merge workflow it enables: +exists because a hosted Verify leg takes ~7 minutes by current measurement. +PR CI runs only the Node 24 Verify leg, while pushes to `main` and +`workflow_dispatch` run the full 22.19/24/26 matrix. The local gate keeps all +three legs because local green is used to merge and must prove what the +post-merge `main` run will prove. A many-core development machine can run all +three legs plus the release gates concurrently. The local-merge workflow it +enables: 1. Run `pnpm check:local-ci` on the branch's HEAD commit. 2. If the gate is green, the branch is mergeable — merge it. @@ -17,6 +21,11 @@ Verify-equivalent leg on whatever Node is currently active, with the repo's normal local worker derivation. It skips the Node matrix and the examples/release/micro-eval gates, so it is a fast signal, not a merge gate. +Docs-only PRs skip the hosted Verify, examples, release-gates, and micro-eval +jobs. Docs-only means changes under `docs/` or `agent-patterns/`, changeset +markdown (`.changeset/*.md`), or top-level markdown. Nested markdown elsewhere +is treated as code. Pushes to `main` never use this skip. + ## What it runs Every leg is an isolated git worktree pinned to the HEAD commit (uncommitted @@ -35,6 +44,9 @@ still fails its own scan. Legs live under `.worktrees/local-ci/` (gitignored), are reused across runs for warm caches, and can be recreated with `--fresh`. +The three Verify legs below mirror the hosted `main`-push matrix. On PRs, +only `verify (24)` runs hosted. + | Local leg | Node | Steps | Mirrors hosted job | | --- | --- | --- | --- | | `verify-node22` | 22.19.x | `install`, `playwright install chrome`, `build`, `lint:package`, `typecheck`, `lint`, `test:unit`, `test:integration` | `verify (22.19.0)` | From f7e94b9131cd4eec76699101966f3ba6fa127e49 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 23:40:48 +0000 Subject: [PATCH 2/2] ci: classify rename sources and fail open on truncated PR file listings Review fixes: renamed files now contribute both their old and new paths to the docs-only classifier (a code-to-docs rename no longer skips Verify), and the listing is checked against the PR's authoritative changed_files count so a truncated files API response (3000-entry cap) fails open to running every heavy job. --- .github/workflows/ci.yml | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34ae96958..b25df0f93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,22 +47,52 @@ jobs: files_file="$(mktemp)" trap 'rm -f "$files_file"' EXIT + if ! changed_files="$( + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ + --jq '.changed_files' + )"; then + echo "Could not read the PR changed-files count; failing open so heavy jobs run." + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [[ ! "$changed_files" =~ ^[0-9]+$ ]]; then + echo "Invalid PR changed-files count '$changed_files'; failing open so heavy jobs run." + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \ - --paginate --jq '.[].filename' > "$files_file"; then + --paginate \ + --jq '.[] | [.filename, (.previous_filename // "")] | @tsv' \ + > "$files_file"; then echo "Could not list changed files; failing open so heavy jobs run." echo "docs_only=false" >> "$GITHUB_OUTPUT" exit 0 fi - mapfile -t files < "$files_file" - if (( ${#files[@]} == 0 )); then + mapfile -t entries < "$files_file" + if (( ${#entries[@]} == 0 )); then echo "No changed files returned; failing open so heavy jobs run." echo "docs_only=false" >> "$GITHUB_OUTPUT" exit 0 fi + if (( ${#entries[@]} != changed_files )); then + echo "Listed ${#entries[@]} of $changed_files changed files; failing open so heavy jobs run." + echo "docs_only=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + paths=() + for entry in "${entries[@]}"; do + IFS=$'\t' read -r filename previous_filename <<< "$entry" + paths+=("$filename") + if [[ -n "$previous_filename" ]]; then + paths+=("$previous_filename") + fi + done docs_only=true - for f in "${files[@]}"; do + for f in "${paths[@]}"; do case "$f" in docs/*|agent-patterns/*) ;; .changeset/*.md) ;; @@ -81,7 +111,7 @@ jobs: done if [[ "$docs_only" == "true" ]]; then - echo "All ${#files[@]} changed files are docs-only; heavy jobs will be skipped." + echo "All ${#entries[@]} changed files are docs-only; heavy jobs will be skipped." fi echo "docs_only=$docs_only" >> "$GITHUB_OUTPUT"