Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 113 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,105 @@ 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 ! 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, (.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 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 "${paths[@]}"; 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 ${#entries[@]} 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
Expand All @@ -39,14 +135,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
Expand Down Expand Up @@ -79,7 +181,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
Expand Down Expand Up @@ -138,7 +243,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
Expand Down
18 changes: 15 additions & 3 deletions docs/local-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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)` |
Expand Down
Loading