From 759a53e63914b6dda793b170df58700362a7aa49 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 19:48:25 +0000 Subject: [PATCH 01/11] ci: Skip GitHub Actions runs PRs don't need - Renovate rebases only on conflicts. Strict branch protection made the default rebase every Renovate PR (rerunning all CI) on each master push. - Benchmarks, Bundle Size and CodeQL skip draft PRs and run once marked ready. - Path filters drop tests, typescript-tests, legacy type overlays and markdown under packages/; CodeQL only triggers on shipped source. - Bundle Size no longer runs on master pushes, where it can't report. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- .cursor/rules/ci-config.mdc | 3 +++ .github/renovate.json | 1 + .github/workflows/benchmark-react.yml | 7 +++++++ .github/workflows/benchmark-spread.yml | 6 +++++- .github/workflows/benchmark.yml | 8 +++++++- .github/workflows/bundle_size.yml | 18 ++++++++---------- .github/workflows/codeql-analysis.yml | 15 +++++++++++++-- .github/workflows/editor-types.yml | 8 ++++++++ 8 files changed, 52 insertions(+), 14 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index c3a1f356b2d0..f269b25212ba 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -30,6 +30,9 @@ alwaysApply: false - `site-preview.yml`/`site-release.yml` `paths` (`website/**`, `docs/{core,rest,graphql}/**`) must match `SITE_PATHS` in `website/scripts/vercel-ignore.sh`. - `benchmark-react.yml` caches Playwright browsers keyed on the resolved `playwright` version from `examples/benchmark-react`; bumping playwright invalidates the cache automatically. - Benchmark workflows (`benchmark.yml`, `benchmark-react.yml`) tune the host (CPU governor, swapoff) and pin CPUs with `taskset` — they must run directly on the runner, not in a `container:`. +- Report-style workflows (`benchmark*.yml`, `bundle_size.yml`, `codeql-analysis.yml`) skip draft PRs with a job-level `if: ${{ !github.event.pull_request.draft }}` and list `ready_for_review` in `pull_request.types`, so they run once a PR is marked ready. Correctness checks (`editor-types`, `skills`, `website`) still run on drafts. +- `paths` leave out what a workflow never reads, so test- or docs-only edits under `packages/` don't fan out: `__tests__/`, `typescript-tests/`, `src-*-types/` (legacy types) and `*.md`. CodeQL triggers only on shipped source (`packages/*/src/**`, `packages/*/*.mjs`). `bundle_size.yml` is PR-only: on push the action measures but has nowhere to report. +- Renovate (`.github/renovate.json`) uses `rebaseWhen: conflicted`: master's branch protection requires up-to-date branches, which makes the default rebase every Renovate PR (and rerun all of its CI) on each master push. Update the branch before merging a Renovate PR. ## Vercel docs site (`website/scripts/vercel-ignore.sh`) diff --git a/.github/renovate.json b/.github/renovate.json index 3d8bfb3530f9..cd44c6521870 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -4,6 +4,7 @@ "commitMessagePrefix": "pkg: ", "commitMessageTopic": "`{{depName}}`", "dependencyDashboardApproval": true, + "rebaseWhen": "conflicted", "rangeStrategy": "auto", "ignoreDeps": [ "whatwg-fetch", diff --git a/.github/workflows/benchmark-react.yml b/.github/workflows/benchmark-react.yml index 4c9638572d62..cfe4614beb66 100644 --- a/.github/workflows/benchmark-react.yml +++ b/.github/workflows/benchmark-react.yml @@ -2,6 +2,8 @@ name: Benchmark React on: pull_request: + # ready_for_review: drafts skip the job below, so run once marked ready + types: [opened, synchronize, reopened, ready_for_review] branches: - master paths: @@ -10,6 +12,8 @@ on: - 'packages/endpoint/src/schemas/**' - 'packages/normalizr/src/**' - 'examples/benchmark-react/**' + - '!packages/**/__tests__/**' + - '!examples/benchmark-react/**/*.md' - '.github/workflows/benchmark-react.yml' push: branches: @@ -20,6 +24,8 @@ on: - 'packages/endpoint/src/schemas/**' - 'packages/normalizr/src/**' - 'examples/benchmark-react/**' + - '!packages/**/__tests__/**' + - '!examples/benchmark-react/**/*.md' - '.github/workflows/benchmark-react.yml' concurrency: @@ -32,6 +38,7 @@ permissions: jobs: benchmark-react: + if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/benchmark-spread.yml b/.github/workflows/benchmark-spread.yml index ff250e4f25e4..1983ef348d2d 100644 --- a/.github/workflows/benchmark-spread.yml +++ b/.github/workflows/benchmark-spread.yml @@ -7,11 +7,14 @@ name: Benchmark Spread on: pull_request: + # ready_for_review: drafts skip the job below, so run once marked ready + types: [opened, synchronize, reopened, ready_for_review] branches: - master paths: - 'packages/core/src/state/**' - 'packages/normalizr/src/normalize/**' + - '!packages/**/__tests__/**' - 'packages/endpoint/src/schemas/EntityMixin.ts' - 'examples/benchmark/spread.js' - 'examples/benchmark/spread-scenarios.js' @@ -23,6 +26,7 @@ on: paths: - 'packages/core/src/state/**' - 'packages/normalizr/src/normalize/**' + - '!packages/**/__tests__/**' - 'packages/endpoint/src/schemas/EntityMixin.ts' - 'examples/benchmark/spread.js' - 'examples/benchmark/spread-scenarios.js' @@ -38,7 +42,7 @@ concurrency: jobs: benchmark-spread: - + if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 19411426e69c..d32d541650c2 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -2,6 +2,8 @@ name: Benchmark on: pull_request: + # ready_for_review: drafts skip the job below, so run once marked ready + types: [opened, synchronize, reopened, ready_for_review] branches: - master paths: @@ -9,6 +11,8 @@ on: - 'packages/endpoint/src/schemas/**' - 'packages/core/src/**' - 'examples/benchmark/**' + - '!packages/**/__tests__/**' + - '!examples/benchmark/**/*.md' - '.github/workflows/benchmark.yml' push: branches: @@ -18,6 +22,8 @@ on: - 'packages/endpoint/src/schemas/**' - 'packages/core/src/**' - 'examples/benchmark/**' + - '!packages/**/__tests__/**' + - '!examples/benchmark/**/*.md' - '.github/workflows/benchmark.yml' permissions: contents: write @@ -29,7 +35,7 @@ concurrency: jobs: benchmark: - + if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/bundle_size.yml b/.github/workflows/bundle_size.yml index b731b8093c2f..e8581969d671 100644 --- a/.github/workflows/bundle_size.yml +++ b/.github/workflows/bundle_size.yml @@ -1,19 +1,17 @@ name: Bundle Size on: + # PR-only: on push the action measures but has nowhere to report pull_request: - # The branches below must be a subset of the branches above + # ready_for_review: drafts skip the job below, so run once marked ready + types: [opened, synchronize, reopened, ready_for_review] branches: [ master ] paths: - 'packages/**' - - 'yarn.lock' - - 'examples/test-bundlesize/**' - - '.github/workflows/bundle_size.yml' - push: - branches: - - master - paths: - - 'packages/**' + - '!packages/**/__tests__/**' + - '!packages/*/typescript-tests/**' + - '!packages/*/src-*-types/**' + - '!packages/**/*.md' - 'yarn.lock' - 'examples/test-bundlesize/**' - '.github/workflows/bundle_size.yml' @@ -24,7 +22,7 @@ concurrency: jobs: build: - + if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ca308aa719df..af678a7ffa17 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,13 +14,23 @@ name: "CodeQL" on: push: branches: [ master ] + # Shipped source only: manifests, docs and tests can't add alerts that matter paths: - - 'packages/**' + - 'packages/*/src/**' + - 'packages/*/*.mjs' + - '!packages/**/__tests__/**' + - '.github/workflows/codeql-analysis.yml' pull_request: + # ready_for_review: drafts skip the job below, so run once marked ready + types: [opened, synchronize, reopened, ready_for_review] # The branches below must be a subset of the branches above branches: [ master ] + # Shipped source only: manifests, docs and tests can't add alerts that matter paths: - - 'packages/**' + - 'packages/*/src/**' + - 'packages/*/*.mjs' + - '!packages/**/__tests__/**' + - '.github/workflows/codeql-analysis.yml' schedule: - cron: '25 22 * * 5' @@ -38,6 +48,7 @@ jobs: contents: read # for actions/checkout to fetch code security-events: write # for github/codeql-action/autobuild to send a status report name: Analyze + if: ${{ !github.event.pull_request.draft }} runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/editor-types.yml b/.github/workflows/editor-types.yml index b1934a98303f..a50624633313 100644 --- a/.github/workflows/editor-types.yml +++ b/.github/workflows/editor-types.yml @@ -7,6 +7,10 @@ on: # Inputs of scripts/copywebsitetypes.sh paths: - 'packages/**' + - '!packages/**/__tests__/**' + - '!packages/*/typescript-tests/**' + - '!packages/*/src-*-types/**' + - '!packages/**/*.md' - 'scripts/copywebsitetypes.sh' - 'scripts/strip-dts-comments.mjs' - 'scripts/globals.ts' @@ -22,6 +26,10 @@ on: - master paths: - 'packages/**' + - '!packages/**/__tests__/**' + - '!packages/*/typescript-tests/**' + - '!packages/*/src-*-types/**' + - '!packages/**/*.md' - 'scripts/copywebsitetypes.sh' - 'scripts/strip-dts-comments.mjs' - 'scripts/globals.ts' From e50f1a3a08b9d401958a19229f797f034c26bd84 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 19:53:48 +0000 Subject: [PATCH 02/11] ci: Simplify workflow trigger cleanups - Skip Bundle Size for packages the size test doesn't bundle - CodeQL matches node.mjs instead of every root .mjs - Drop repeated draft comments (documented in ci-config.mdc) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- .cursor/rules/ci-config.mdc | 5 +++-- .github/workflows/benchmark-react.yml | 1 - .github/workflows/benchmark-spread.yml | 5 ++--- .github/workflows/benchmark.yml | 1 - .github/workflows/bundle_size.yml | 7 +++++-- .github/workflows/codeql-analysis.yml | 8 +++----- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index f269b25212ba..96f2aadfa3fc 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -31,8 +31,9 @@ alwaysApply: false - `benchmark-react.yml` caches Playwright browsers keyed on the resolved `playwright` version from `examples/benchmark-react`; bumping playwright invalidates the cache automatically. - Benchmark workflows (`benchmark.yml`, `benchmark-react.yml`) tune the host (CPU governor, swapoff) and pin CPUs with `taskset` — they must run directly on the runner, not in a `container:`. - Report-style workflows (`benchmark*.yml`, `bundle_size.yml`, `codeql-analysis.yml`) skip draft PRs with a job-level `if: ${{ !github.event.pull_request.draft }}` and list `ready_for_review` in `pull_request.types`, so they run once a PR is marked ready. Correctness checks (`editor-types`, `skills`, `website`) still run on drafts. -- `paths` leave out what a workflow never reads, so test- or docs-only edits under `packages/` don't fan out: `__tests__/`, `typescript-tests/`, `src-*-types/` (legacy types) and `*.md`. CodeQL triggers only on shipped source (`packages/*/src/**`, `packages/*/*.mjs`). `bundle_size.yml` is PR-only: on push the action measures but has nowhere to report. -- Renovate (`.github/renovate.json`) uses `rebaseWhen: conflicted`: master's branch protection requires up-to-date branches, which makes the default rebase every Renovate PR (and rerun all of its CI) on each master push. Update the branch before merging a Renovate PR. +- `paths` leave out what a workflow never reads, so test- or docs-only edits under `packages/` don't fan out: `__tests__/`, `typescript-tests/`, `src-*-types/` (legacy types) and `*.md`. `bundle_size.yml` also skips packages `examples/test-bundlesize` doesn't bundle (graphql, test, vue). +- CodeQL triggers only on shipped source (`packages/*/src/**`, `packages/*/node.mjs`). `bundle_size.yml` is PR-only: on push the action measures but has nowhere to report. +- Renovate (`.github/renovate.json`) uses `rebaseWhen: conflicted`: master's branch protection requires up-to-date branches, which makes the default rebase every Renovate PR (and rerun all of its CI) on each master push. Update the branch before merging a Renovate PR; if that update isn't merged, Renovate treats the branch as edited and stops rebasing it until its PR's rebase checkbox is ticked. ## Vercel docs site (`website/scripts/vercel-ignore.sh`) diff --git a/.github/workflows/benchmark-react.yml b/.github/workflows/benchmark-react.yml index cfe4614beb66..9abf385f5f05 100644 --- a/.github/workflows/benchmark-react.yml +++ b/.github/workflows/benchmark-react.yml @@ -2,7 +2,6 @@ name: Benchmark React on: pull_request: - # ready_for_review: drafts skip the job below, so run once marked ready types: [opened, synchronize, reopened, ready_for_review] branches: - master diff --git a/.github/workflows/benchmark-spread.yml b/.github/workflows/benchmark-spread.yml index 1983ef348d2d..b1c709fc9e85 100644 --- a/.github/workflows/benchmark-spread.yml +++ b/.github/workflows/benchmark-spread.yml @@ -7,15 +7,14 @@ name: Benchmark Spread on: pull_request: - # ready_for_review: drafts skip the job below, so run once marked ready types: [opened, synchronize, reopened, ready_for_review] branches: - master paths: - 'packages/core/src/state/**' - 'packages/normalizr/src/normalize/**' - - '!packages/**/__tests__/**' - 'packages/endpoint/src/schemas/EntityMixin.ts' + - '!packages/**/__tests__/**' - 'examples/benchmark/spread.js' - 'examples/benchmark/spread-scenarios.js' - 'examples/benchmark/schemas.js' @@ -26,8 +25,8 @@ on: paths: - 'packages/core/src/state/**' - 'packages/normalizr/src/normalize/**' - - '!packages/**/__tests__/**' - 'packages/endpoint/src/schemas/EntityMixin.ts' + - '!packages/**/__tests__/**' - 'examples/benchmark/spread.js' - 'examples/benchmark/spread-scenarios.js' - 'examples/benchmark/schemas.js' diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index d32d541650c2..6080820bc347 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -2,7 +2,6 @@ name: Benchmark on: pull_request: - # ready_for_review: drafts skip the job below, so run once marked ready types: [opened, synchronize, reopened, ready_for_review] branches: - master diff --git a/.github/workflows/bundle_size.yml b/.github/workflows/bundle_size.yml index e8581969d671..e7897c065c72 100644 --- a/.github/workflows/bundle_size.yml +++ b/.github/workflows/bundle_size.yml @@ -3,7 +3,6 @@ name: Bundle Size on: # PR-only: on push the action measures but has nowhere to report pull_request: - # ready_for_review: drafts skip the job below, so run once marked ready types: [opened, synchronize, reopened, ready_for_review] branches: [ master ] paths: @@ -12,12 +11,16 @@ on: - '!packages/*/typescript-tests/**' - '!packages/*/src-*-types/**' - '!packages/**/*.md' + # not bundled by examples/test-bundlesize + - '!packages/graphql/**' + - '!packages/test/**' + - '!packages/vue/**' - 'yarn.lock' - 'examples/test-bundlesize/**' - '.github/workflows/bundle_size.yml' concurrency: - group: bundle-size-${{ github.head_ref || github.ref }} + group: bundle-size-${{ github.head_ref }} cancel-in-progress: true jobs: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index af678a7ffa17..31c3829e409b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -11,24 +11,22 @@ # name: "CodeQL" +# Shipped source only: manifests, docs and tests can't add alerts that matter on: push: branches: [ master ] - # Shipped source only: manifests, docs and tests can't add alerts that matter paths: - 'packages/*/src/**' - - 'packages/*/*.mjs' + - 'packages/*/node.mjs' - '!packages/**/__tests__/**' - '.github/workflows/codeql-analysis.yml' pull_request: - # ready_for_review: drafts skip the job below, so run once marked ready types: [opened, synchronize, reopened, ready_for_review] # The branches below must be a subset of the branches above branches: [ master ] - # Shipped source only: manifests, docs and tests can't add alerts that matter paths: - 'packages/*/src/**' - - 'packages/*/*.mjs' + - 'packages/*/node.mjs' - '!packages/**/__tests__/**' - '.github/workflows/codeql-analysis.yml' schedule: From 6b2ba4f65ceafd70534df6de98668381a5775517 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 20:08:44 +0000 Subject: [PATCH 03/11] ci: Never cancel GitHub Actions runs on master Cancelled push runs (website, skills, site deploy) marked master commits red. PR runs still cancel superseded ones; push runs get their own concurrency group, and an older site deploy skips itself when a newer push changed the site. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- .cursor/rules/ci-config.mdc | 1 + .github/workflows/codeql-analysis.yml | 4 +++- .github/workflows/editor-types.yml | 4 +++- .github/workflows/site-preview.yml | 4 +++- .github/workflows/site-release.yml | 23 +++++++++++++++++++---- .github/workflows/skills.yml | 4 +++- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index a434b2ce6fee..a1b448f56f74 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -32,6 +32,7 @@ alwaysApply: false - `site-preview.yml`/`site-release.yml` `paths` (`website/**`, `docs/{core,rest,graphql}/**`) must match `SITE_PATHS` in `website/scripts/vercel-ignore.sh`. - `benchmark-react.yml` caches Playwright browsers keyed on the resolved `playwright` version from `examples/benchmark-react`; bumping playwright invalidates the cache automatically. - Benchmark workflows (`benchmark.yml`, `benchmark-react.yml`) tune the host (CPU governor, swapoff) and pin CPUs with `taskset` — they must run directly on the runner, not in a `container:`. +- Never cancel a run on master: a cancelled check marks the commit red, which hurts npm search scoring. PR runs cancel superseded ones (`group: -${{ github.head_ref || github.run_id }}`, `cancel-in-progress: true`); push runs get a unique group. `site-release.yml` runs aren't grouped either, so an older run skips its deploy when a newer push changed the site (its own run deploys). `release.yml` and the benchmark push groups still queue (no `cancel-in-progress`), since they publish or write `gh-pages`. - Report-style workflows (`benchmark*.yml`, `bundle_size.yml`, `codeql-analysis.yml`) skip draft PRs with a job-level `if: ${{ !github.event.pull_request.draft }}` and list `ready_for_review` in `pull_request.types`, so they run once a PR is marked ready. Correctness checks (`editor-types`, `skills`, `website`) still run on drafts. - `paths` leave out what a workflow never reads, so test- or docs-only edits under `packages/` don't fan out: `__tests__/`, `typescript-tests/`, `src-*-types/` (legacy types) and `*.md`. `bundle_size.yml` also skips packages `examples/test-bundlesize` doesn't bundle (graphql, test, vue). - CodeQL triggers only on shipped source (`packages/*/src/**`, `packages/*/node.mjs`). `bundle_size.yml` is PR-only: on push the action measures but has nowhere to report. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 31c3829e409b..965531d64d3d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,8 +35,10 @@ on: permissions: contents: read +# PRs cancel superseded runs. Push runs get their own group, so master never +# shows a cancelled (red) run. concurrency: - group: codeql-${{ github.head_ref || github.ref }} + group: codeql-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: diff --git a/.github/workflows/editor-types.yml b/.github/workflows/editor-types.yml index a50624633313..8a83469f5c13 100644 --- a/.github/workflows/editor-types.yml +++ b/.github/workflows/editor-types.yml @@ -41,8 +41,10 @@ on: - 'tsconfig*.json' - '.github/workflows/editor-types.yml' +# PRs cancel superseded runs. Push runs get their own group, so master never +# shows a cancelled (red) run. concurrency: - group: editor-types-${{ github.head_ref || github.ref }} + group: editor-types-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: diff --git a/.github/workflows/site-preview.yml b/.github/workflows/site-preview.yml index 3aaf10c6e148..e85e32c0cca8 100644 --- a/.github/workflows/site-preview.yml +++ b/.github/workflows/site-preview.yml @@ -21,8 +21,10 @@ on: - 'docs/graphql/**' - '.github/workflows/site-preview.yml' +# PRs cancel superseded runs. Push runs get their own group, so master never +# shows a cancelled (red) run. concurrency: - group: site-preview-${{ github.head_ref || github.ref }} + group: site-preview-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: diff --git a/.github/workflows/site-release.yml b/.github/workflows/site-release.yml index 651a5dd5c242..eb8119a91857 100644 --- a/.github/workflows/site-release.yml +++ b/.github/workflows/site-release.yml @@ -15,10 +15,6 @@ on: - 'docs/graphql/**' - '.github/workflows/site-release.yml' -concurrency: - group: site-release-${{ github.ref }} - cancel-in-progress: true - jobs: deploy: runs-on: ubuntu-latest @@ -27,12 +23,31 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 800 + # Runs aren't cancelled (a cancelled run marks master red), so an older + # run skips when a newer push changed the site: that push's run deploys. + # Paths match `on.paths`. + - name: Check for a newer site change + id: latest + run: | + # Deploys unless it can tell a newer site change landed (fail open) + rc=0 + git fetch -q origin "$GITHUB_REF" && + git diff --quiet "$GITHUB_SHA" FETCH_HEAD -- website docs/core docs/rest docs/graphql || rc=$? + if [ "$rc" = 1 ]; then + echo "::notice::Skipping: $(git rev-parse --short FETCH_HEAD) changed the site again and deploys it" + else + echo "deploy=true" >> "$GITHUB_OUTPUT" + fi - name: Install Vercel CLI + if: steps.latest.outputs.deploy run: npm install --global vercel@latest - name: Pull Vercel Environment Information + if: steps.latest.outputs.deploy run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - name: Build Project Artifacts + if: steps.latest.outputs.deploy run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy Project Artifacts to Vercel + if: steps.latest.outputs.deploy continue-on-error: true run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/skills.yml b/.github/workflows/skills.yml index 2f23ab57ab8e..a486315ea501 100644 --- a/.github/workflows/skills.yml +++ b/.github/workflows/skills.yml @@ -25,8 +25,10 @@ on: - '.github/workflows/skills.yml' - '.gitattributes' +# PRs cancel superseded runs. Push runs get their own group, so master never +# shows a cancelled (red) run. concurrency: - group: skills-${{ github.head_ref || github.ref }} + group: skills-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: From 8b86287d0fc1c5db17588492031963b2efa9d0a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 20:14:59 +0000 Subject: [PATCH 04/11] ci: Run Bundle Size and benchmarks only for dependency bumps they measure A reusable gate diffs yarn.lock and the workspace manifests: bumps that can't reach the measured workspaces or the build tooling (test, lint, React Native, website deps) skip the job. Benchmarks now also run for relevant bumps (react, babel, their own deps), which they ignored before. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- .cursor/rules/ci-config.mdc | 1 + .github/workflows/benchmark-react.yml | 28 +++- .github/workflows/benchmark-spread.yml | 28 +++- .github/workflows/benchmark.yml | 27 ++- .github/workflows/bundle_size.yml | 26 ++- .github/workflows/dependency-gate.yml | 40 +++++ scripts/ci-deps-relevant.mjs | 222 +++++++++++++++++++++++++ 7 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/dependency-gate.yml create mode 100644 scripts/ci-deps-relevant.mjs diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index a1b448f56f74..4e4263a6ca00 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -36,6 +36,7 @@ alwaysApply: false - Report-style workflows (`benchmark*.yml`, `bundle_size.yml`, `codeql-analysis.yml`) skip draft PRs with a job-level `if: ${{ !github.event.pull_request.draft }}` and list `ready_for_review` in `pull_request.types`, so they run once a PR is marked ready. Correctness checks (`editor-types`, `skills`, `website`) still run on drafts. - `paths` leave out what a workflow never reads, so test- or docs-only edits under `packages/` don't fan out: `__tests__/`, `typescript-tests/`, `src-*-types/` (legacy types) and `*.md`. `bundle_size.yml` also skips packages `examples/test-bundlesize` doesn't bundle (graphql, test, vue). - CodeQL triggers only on shipped source (`packages/*/src/**`, `packages/*/node.mjs`). `bundle_size.yml` is PR-only: on push the action measures but has nowhere to report. +- Bundle Size and the benchmarks list `yarn.lock` in `paths` but gate their main job on the reusable `dependency-gate.yml` (`scripts/ci-deps-relevant.mjs`). It runs the job when a non-manifest file in the workflow's paths changed, a manifest of the measured workspaces (or a workspace package they ship with) changed, or the yarn.lock resolutions reachable from their dependencies or the babel/browserslist/core-js build tooling changed. Bumps of test, lint, React Native or website tooling skip it; anything it can't classify runs. The gate's `with.paths` must mirror the workflow's `paths` (minus `yarn.lock`). - Renovate (`.github/renovate.json`) uses `rebaseWhen: conflicted`: master's branch protection requires up-to-date branches, which makes the default rebase every Renovate PR (and rerun all of its CI) on each master push. Update the branch before merging a Renovate PR; if that update isn't merged, Renovate treats the branch as edited and stops rebasing it until its PR's rebase checkbox is ticked. ## Vercel docs site (`website/scripts/vercel-ignore.sh`) diff --git a/.github/workflows/benchmark-react.yml b/.github/workflows/benchmark-react.yml index 9abf385f5f05..12d4c99da577 100644 --- a/.github/workflows/benchmark-react.yml +++ b/.github/workflows/benchmark-react.yml @@ -13,6 +13,9 @@ on: - 'examples/benchmark-react/**' - '!packages/**/__tests__/**' - '!examples/benchmark-react/**/*.md' + - 'yarn.lock' + - 'scripts/ci-deps-relevant.mjs' + - '.github/workflows/dependency-gate.yml' - '.github/workflows/benchmark-react.yml' push: branches: @@ -25,6 +28,9 @@ on: - 'examples/benchmark-react/**' - '!packages/**/__tests__/**' - '!examples/benchmark-react/**/*.md' + - 'yarn.lock' + - 'scripts/ci-deps-relevant.mjs' + - '.github/workflows/dependency-gate.yml' - '.github/workflows/benchmark-react.yml' concurrency: @@ -36,8 +42,28 @@ permissions: pull-requests: write jobs: - benchmark-react: + # Skips dependency bumps that can't change what this measures + changes: if: ${{ !github.event.pull_request.draft }} + uses: ./.github/workflows/dependency-gate.yml + with: + workspaces: examples/benchmark-react + # `paths` above, one per line + paths: | + packages/react/src/** + packages/core/src/** + packages/endpoint/src/schemas/** + packages/normalizr/src/** + examples/benchmark-react/** + !packages/**/__tests__/** + !examples/benchmark-react/**/*.md + scripts/ci-deps-relevant.mjs + .github/workflows/dependency-gate.yml + .github/workflows/benchmark-react.yml + + benchmark-react: + needs: changes + if: ${{ needs.changes.outputs.relevant == 'true' }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/benchmark-spread.yml b/.github/workflows/benchmark-spread.yml index b1c709fc9e85..c6bcd638428d 100644 --- a/.github/workflows/benchmark-spread.yml +++ b/.github/workflows/benchmark-spread.yml @@ -18,6 +18,9 @@ on: - 'examples/benchmark/spread.js' - 'examples/benchmark/spread-scenarios.js' - 'examples/benchmark/schemas.js' + - 'yarn.lock' + - 'scripts/ci-deps-relevant.mjs' + - '.github/workflows/dependency-gate.yml' - '.github/workflows/benchmark-spread.yml' push: branches: @@ -30,6 +33,9 @@ on: - 'examples/benchmark/spread.js' - 'examples/benchmark/spread-scenarios.js' - 'examples/benchmark/schemas.js' + - 'yarn.lock' + - 'scripts/ci-deps-relevant.mjs' + - '.github/workflows/dependency-gate.yml' - '.github/workflows/benchmark-spread.yml' permissions: contents: write @@ -40,8 +46,28 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - benchmark-spread: + # Skips dependency bumps that can't change what this measures + changes: if: ${{ !github.event.pull_request.draft }} + uses: ./.github/workflows/dependency-gate.yml + with: + workspaces: examples/benchmark + # `paths` above, one per line + paths: | + packages/core/src/state/** + packages/normalizr/src/normalize/** + packages/endpoint/src/schemas/EntityMixin.ts + !packages/**/__tests__/** + examples/benchmark/spread.js + examples/benchmark/spread-scenarios.js + examples/benchmark/schemas.js + scripts/ci-deps-relevant.mjs + .github/workflows/dependency-gate.yml + .github/workflows/benchmark-spread.yml + + benchmark-spread: + needs: changes + if: ${{ needs.changes.outputs.relevant == 'true' }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 6080820bc347..f6ff1d84f070 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -12,6 +12,9 @@ on: - 'examples/benchmark/**' - '!packages/**/__tests__/**' - '!examples/benchmark/**/*.md' + - 'yarn.lock' + - 'scripts/ci-deps-relevant.mjs' + - '.github/workflows/dependency-gate.yml' - '.github/workflows/benchmark.yml' push: branches: @@ -23,6 +26,9 @@ on: - 'examples/benchmark/**' - '!packages/**/__tests__/**' - '!examples/benchmark/**/*.md' + - 'yarn.lock' + - 'scripts/ci-deps-relevant.mjs' + - '.github/workflows/dependency-gate.yml' - '.github/workflows/benchmark.yml' permissions: contents: write @@ -33,8 +39,27 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - benchmark: + # Skips dependency bumps that can't change what this measures + changes: if: ${{ !github.event.pull_request.draft }} + uses: ./.github/workflows/dependency-gate.yml + with: + workspaces: examples/benchmark + # `paths` above, one per line + paths: | + packages/normalizr/src/** + packages/endpoint/src/schemas/** + packages/core/src/** + examples/benchmark/** + !packages/**/__tests__/** + !examples/benchmark/**/*.md + scripts/ci-deps-relevant.mjs + .github/workflows/dependency-gate.yml + .github/workflows/benchmark.yml + + benchmark: + needs: changes + if: ${{ needs.changes.outputs.relevant == 'true' }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/bundle_size.yml b/.github/workflows/bundle_size.yml index e7897c065c72..d5316bcc2ced 100644 --- a/.github/workflows/bundle_size.yml +++ b/.github/workflows/bundle_size.yml @@ -17,6 +17,8 @@ on: - '!packages/vue/**' - 'yarn.lock' - 'examples/test-bundlesize/**' + - 'scripts/ci-deps-relevant.mjs' + - '.github/workflows/dependency-gate.yml' - '.github/workflows/bundle_size.yml' concurrency: @@ -24,8 +26,30 @@ concurrency: cancel-in-progress: true jobs: - build: + # Skips dependency bumps that can't change what this measures + changes: if: ${{ !github.event.pull_request.draft }} + uses: ./.github/workflows/dependency-gate.yml + with: + workspaces: examples/test-bundlesize + # `paths` above, one per line + paths: | + packages/** + !packages/**/__tests__/** + !packages/*/typescript-tests/** + !packages/*/src-*-types/** + !packages/**/*.md + !packages/graphql/** + !packages/test/** + !packages/vue/** + examples/test-bundlesize/** + scripts/ci-deps-relevant.mjs + .github/workflows/dependency-gate.yml + .github/workflows/bundle_size.yml + + build: + needs: changes + if: ${{ needs.changes.outputs.relevant == 'true' }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/dependency-gate.yml b/.github/workflows/dependency-gate.yml new file mode 100644 index 000000000000..873c072765de --- /dev/null +++ b/.github/workflows/dependency-gate.yml @@ -0,0 +1,40 @@ +name: dependency gate +# Reusable: tells a workflow whether its change can affect what it measures, +# so dependency bumps of unrelated tooling skip it. See scripts/ci-deps-relevant.mjs. +on: + workflow_call: + inputs: + workspaces: + description: Comma-separated workspaces the job builds and runs + required: true + type: string + paths: + description: The caller's `paths` filter, one glob per line + required: true + type: string + outputs: + relevant: + value: ${{ jobs.check.outputs.relevant }} + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + outputs: + relevant: ${{ steps.deps.outputs.relevant }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 1 + - id: deps + env: + BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + WORKSPACES: ${{ inputs.workspaces }} + PATHS: ${{ inputs.paths }} + run: | + # A missing base makes the script fail open + git fetch -q --depth=1 origin "$BASE" || true + mapfile -t paths <<< "$PATHS" + node scripts/ci-deps-relevant.mjs "$BASE" "$WORKSPACES" "${paths[@]}" diff --git a/scripts/ci-deps-relevant.mjs b/scripts/ci-deps-relevant.mjs new file mode 100644 index 000000000000..76d2c9392749 --- /dev/null +++ b/scripts/ci-deps-relevant.mjs @@ -0,0 +1,222 @@ +#!/usr/bin/env node +// Decides whether a change can affect what a CI job measures, so dependency +// bumps that only touch unrelated tooling (tests, lint, React Native, website) +// skip it. Fails open: anything it can't classify counts as relevant. +// +// Usage: node scripts/ci-deps-relevant.mjs [,...] [path...] +// : what the job builds and runs (e.g. examples/benchmark). +// [path...]: the workflow's `paths` filter (GitHub globs, `!` excludes). +// Relevant when, between and HEAD: +// - a file matching [path...] (any file, if none given) changed, other than +// package.json and lockfiles, or +// - the root package.json changed outside devDependencies/resolutions, or +// - a listed workspace's package.json changed, or a package.json field +// other than devDependencies/version of a workspace package it depends on +// (packageManager is ignored: the yarn version doesn't change output), or +// - the yarn.lock resolutions reachable from those workspaces' dependencies +// (or from the build tooling below) changed. +// Prints the decision and writes `relevant=true|false` to $GITHUB_OUTPUT. +import { execFileSync } from 'node:child_process'; +import { appendFileSync } from 'node:fs'; + +// Root build tooling every package build uses (babel.config.js, browserslist) +const BUILD_TOOLS = + /^(@babel\/|core-js|browserslist$|caniuse-lite$|@anansi\/(babel-preset|browserslist-config)$)/; +// Type-only packages can't change built output or runtime behavior +const IGNORED = /^(@types\/|typescript$|@typescript\/)/; +const DEP_FIELDS = [ + 'dependencies', + 'peerDependencies', + 'optionalDependencies', + 'devDependencies', +]; + +const git = (...args) => + execFileSync('git', args, { + encoding: 'utf8', + maxBuffer: 1 << 28, + stdio: ['ignore', 'pipe', 'ignore'], + }); +const show = (ref, file) => { + try { + return git('show', `${ref}:${file}`); + } catch { + return undefined; + } +}; +const json = (ref, file) => { + const text = show(ref, file); + return text === undefined ? undefined : JSON.parse(text); +}; +const omit = (obj, keys) => + obj && + Object.fromEntries(Object.entries(obj).filter(([k]) => !keys.includes(k))); +const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); +// '@scope/name@npm:1.0.0' -> '@scope/name' +const nameOf = descriptor => descriptor.slice(0, descriptor.indexOf('@', 1)); + +function decide(base, workspaceDirs, paths) { + git('rev-parse', '--verify', `${base}^{commit}`); + // GitHub `paths` globs as git pathspecs, minus manifests and lockfiles + const pathspecs = (paths.length ? paths : ['**']).map(p => + p.startsWith('!') ? `:(exclude,glob)${p.slice(1)}` : `:(glob)${p}`, + ); + const other = git( + 'diff', + '--name-only', + '--no-renames', + base, + 'HEAD', + '--', + ...pathspecs, + ':(exclude)yarn.lock', + ':(exclude,glob)**/package.json', + ':(exclude,glob)**/package-lock.json', + ) + .split('\n') + .filter(Boolean); + if (other.length) return `files changed: ${other.slice(0, 5).join(', ')}`; + + // Root scripts (e.g. build:benchmark); yarn.lock covers its devDependencies + // and resolutions + const root = json('HEAD', 'package.json'); + const rootOmitted = [ + 'devDependencies', + 'resolutions', + 'version', + 'packageManager', + ]; + if ( + !same( + omit(json(base, 'package.json'), rootOmitted), + omit(root, rootOmitted), + ) + ) + return 'package.json changed'; + + // Workspace packages by name, so `workspace:*` deps can be followed + const rootPatterns = root.workspaces; + const workspaces = new Map(); + for (const file of git( + 'ls-files', + '--', + ...rootPatterns.map(p => `${p}/package.json`), + ) + .split('\n') + .filter(Boolean)) { + workspaces.set(json('HEAD', file).name, file); + } + + // Seed names: everything the listed workspaces use, plus the shipped + // dependencies of workspace packages they pull in + const seeds = new Set(); + const visited = new Set(); + const queue = []; + const addDeps = (manifest, fields) => { + for (const field of fields) + for (const name of Object.keys(manifest?.[field] ?? {})) queue.push(name); + }; + for (const dir of workspaceDirs) { + const file = `${dir}/package.json`; + const head = json('HEAD', file); + if ( + !same( + omit(json(base, file), ['packageManager']), + omit(head, ['packageManager']), + ) + ) + return `${file} changed`; + addDeps(head, DEP_FIELDS); + } + while (queue.length) { + const name = queue.pop(); + if (visited.has(name) || IGNORED.test(name)) continue; + visited.add(name); + const file = workspaces.get(name); + if (!file) { + seeds.add(name); + continue; + } + const head = json('HEAD', file); + const omitted = ['devDependencies', 'version', 'packageManager']; + if (!same(omit(json(base, file), omitted), omit(head, omitted))) + return `${file} changed`; + addDeps(head, ['dependencies', 'optionalDependencies']); + } + + const before = reachable(parseLock(show(base, 'yarn.lock')), seeds); + const after = reachable(parseLock(show('HEAD', 'yarn.lock')), seeds); + const diff = [...after] + .filter(r => !before.has(r)) + .concat([...before].filter(r => !after.has(r))); + if (diff.length) + return `resolved dependencies changed: ${diff.slice(0, 5).join(', ')}`; +} + +// yarn.lock (berry) -> descriptor and name lookups for each resolution's deps +function parseLock(text) { + const byDescriptor = new Map(); + const byName = new Map(); + const deps = new Map(); + for (const block of text.split('\n\n')) { + const lines = block.split('\n'); + const header = lines.findIndex(l => /^"?[^\s#].*:$/.test(l)); + const resolution = block.match(/^ {2}resolution: "(.+)"$/m)?.[1]; + if (header < 0 || !resolution) continue; + for (const d of lines[header].slice(0, -1).split(', ')) + byDescriptor.set(d.replace(/^"|"$/g, ''), resolution); + const name = nameOf(resolution); + byName.set(name, [...(byName.get(name) ?? []), resolution]); + const own = []; + let inDeps = false; + for (const line of lines.slice(header + 1)) { + if (/^ {2}\S/.test(line)) inDeps = line === ' dependencies:'; + else if (inDeps) { + const m = line.match(/^ {4}"?([^":]+)"?: "?([^"]+)"?$/); + if (m) own.push([m[1], m[2]]); + } + } + deps.set(resolution, own); + } + return { byDescriptor, byName, deps }; +} + +function reachable({ byDescriptor, byName, deps }, seeds) { + const seen = new Set(); + const stack = []; + for (const [name, resolutions] of byName) + if (seeds.has(name) || BUILD_TOOLS.test(name)) stack.push(...resolutions); + while (stack.length) { + const res = stack.pop(); + if ( + seen.has(res) || + IGNORED.test(nameOf(res)) || + res.includes('@workspace:') + ) + continue; + seen.add(res); + for (const [name, range] of deps.get(res) ?? []) { + const target = byDescriptor.get(`${name}@${range}`); + // Unmatched ranges (patches, workspaces) follow every version of the name + stack.push(...(target ? [target] : (byName.get(name) ?? []))); + } + } + return seen; +} + +const [base, workspaces = '', ...rest] = process.argv.slice(2); +const paths = rest.filter(Boolean); +let reason; +try { + reason = decide(base, workspaces.split(',').filter(Boolean), paths); +} catch (e) { + reason = `could not decide, running anyway (${e.message.split('\n')[0]})`; +} +const relevant = reason !== undefined; +console.log( + relevant ? + `relevant: ${reason}` + : 'not relevant: no change reaches these workspaces', +); +if (process.env.GITHUB_OUTPUT) + appendFileSync(process.env.GITHUB_OUTPUT, `relevant=${relevant}\n`); From d4bc4063f1f9c291a53df720bf728e1a3767d513 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 20:18:23 +0000 Subject: [PATCH 05/11] ci: Queue release and site deploy runs instead of cancelling them Dropping site-release's group let two deploys overlap, so an older one could finish last. A shared group with queue: max runs them one at a time in push order without cancelling pending runs; Release and Beta Release get the same so a third quick push no longer cancels one. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- .cursor/rules/ci-config.mdc | 2 +- .github/workflows/beta-release.yml | 5 ++++- .github/workflows/release.yml | 5 ++++- .github/workflows/site-release.yml | 10 ++++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 4e4263a6ca00..44d456ecff44 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -32,7 +32,7 @@ alwaysApply: false - `site-preview.yml`/`site-release.yml` `paths` (`website/**`, `docs/{core,rest,graphql}/**`) must match `SITE_PATHS` in `website/scripts/vercel-ignore.sh`. - `benchmark-react.yml` caches Playwright browsers keyed on the resolved `playwright` version from `examples/benchmark-react`; bumping playwright invalidates the cache automatically. - Benchmark workflows (`benchmark.yml`, `benchmark-react.yml`) tune the host (CPU governor, swapoff) and pin CPUs with `taskset` — they must run directly on the runner, not in a `container:`. -- Never cancel a run on master: a cancelled check marks the commit red, which hurts npm search scoring. PR runs cancel superseded ones (`group: -${{ github.head_ref || github.run_id }}`, `cancel-in-progress: true`); push runs get a unique group. `site-release.yml` runs aren't grouped either, so an older run skips its deploy when a newer push changed the site (its own run deploys). `release.yml` and the benchmark push groups still queue (no `cancel-in-progress`), since they publish or write `gh-pages`. +- Never cancel a run on master: a cancelled check marks the commit red, which hurts npm search scoring. PR runs cancel superseded ones (`group: -${{ github.head_ref || github.run_id }}`, `cancel-in-progress: true`); push runs get a unique group. Runs that must not overlap (`release.yml`, `beta-release.yml`, `site-release.yml`) use a shared group with `queue: max`, which keeps up to 100 pending runs in order instead of cancelling all but one (GitHub rejects it alongside `cancel-in-progress: true`; actionlint 1.7.12 doesn't know the key yet). `site-release.yml` skips its deploy when a newer queued push changed the site. The benchmark push groups still use the default single pending slot. - Report-style workflows (`benchmark*.yml`, `bundle_size.yml`, `codeql-analysis.yml`) skip draft PRs with a job-level `if: ${{ !github.event.pull_request.draft }}` and list `ready_for_review` in `pull_request.types`, so they run once a PR is marked ready. Correctness checks (`editor-types`, `skills`, `website`) still run on drafts. - `paths` leave out what a workflow never reads, so test- or docs-only edits under `packages/` don't fan out: `__tests__/`, `typescript-tests/`, `src-*-types/` (legacy types) and `*.md`. `bundle_size.yml` also skips packages `examples/test-bundlesize` doesn't bundle (graphql, test, vue). - CodeQL triggers only on shipped source (`packages/*/src/**`, `packages/*/node.mjs`). `bundle_size.yml` is PR-only: on push the action measures but has nowhere to report. diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 84eb3573cca3..9b8b79785ccc 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -12,7 +12,10 @@ on: default: 'beta' type: string -concurrency: ${{ github.workflow }}-${{ github.ref }} +# Queue every run instead of cancelling pending ones (red on the branch) +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + queue: max jobs: beta-release: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e19bd2e50bce..74b6149eb580 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,10 @@ on: branches: - master -concurrency: ${{ github.workflow }}-${{ github.ref }} +# Queue every run instead of cancelling pending ones (red on the branch) +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + queue: max jobs: release: diff --git a/.github/workflows/site-release.yml b/.github/workflows/site-release.yml index eb8119a91857..7a5f7f839724 100644 --- a/.github/workflows/site-release.yml +++ b/.github/workflows/site-release.yml @@ -15,6 +15,12 @@ on: - 'docs/graphql/**' - '.github/workflows/site-release.yml' +# One deploy at a time, in push order. `queue: max` keeps every pending run +# instead of cancelling it (a cancelled run marks master red). +concurrency: + group: site-release-${{ github.ref }} + queue: max + jobs: deploy: runs-on: ubuntu-latest @@ -23,8 +29,8 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 800 - # Runs aren't cancelled (a cancelled run marks master red), so an older - # run skips when a newer push changed the site: that push's run deploys. + # Queued runs aren't cancelled, so an older one skips when a newer push + # changed the site: that push's run, queued behind it, deploys. # Paths match `on.paths`. - name: Check for a newer site change id: latest From c7259f9b39a0e90b67e2dbbb5a6a1f83520613ca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 20:21:07 +0000 Subject: [PATCH 06/11] ci: Deploy the branch tip from queued site releases A queued run that skipped because a newer push changed the site left production stale if that push had no run ([skip ci]). Deploying the branch tip covers it and still never goes backwards. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- .cursor/rules/ci-config.mdc | 2 +- .github/workflows/site-release.yml | 24 +++++------------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 44d456ecff44..7f303623baae 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -32,7 +32,7 @@ alwaysApply: false - `site-preview.yml`/`site-release.yml` `paths` (`website/**`, `docs/{core,rest,graphql}/**`) must match `SITE_PATHS` in `website/scripts/vercel-ignore.sh`. - `benchmark-react.yml` caches Playwright browsers keyed on the resolved `playwright` version from `examples/benchmark-react`; bumping playwright invalidates the cache automatically. - Benchmark workflows (`benchmark.yml`, `benchmark-react.yml`) tune the host (CPU governor, swapoff) and pin CPUs with `taskset` — they must run directly on the runner, not in a `container:`. -- Never cancel a run on master: a cancelled check marks the commit red, which hurts npm search scoring. PR runs cancel superseded ones (`group: -${{ github.head_ref || github.run_id }}`, `cancel-in-progress: true`); push runs get a unique group. Runs that must not overlap (`release.yml`, `beta-release.yml`, `site-release.yml`) use a shared group with `queue: max`, which keeps up to 100 pending runs in order instead of cancelling all but one (GitHub rejects it alongside `cancel-in-progress: true`; actionlint 1.7.12 doesn't know the key yet). `site-release.yml` skips its deploy when a newer queued push changed the site. The benchmark push groups still use the default single pending slot. +- Never cancel a run on master: a cancelled check marks the commit red, which hurts npm search scoring. PR runs cancel superseded ones (`group: -${{ github.head_ref || github.run_id }}`, `cancel-in-progress: true`); push runs get a unique group. Runs that must not overlap (`release.yml`, `beta-release.yml`, `site-release.yml`) use a shared group with `queue: max`, which keeps up to 100 pending runs in order instead of cancelling all but one (GitHub rejects it alongside `cancel-in-progress: true`; actionlint 1.7.12 doesn't know the key yet). `site-release.yml` deploys the branch tip, since a queued run can start after newer commits landed. The benchmark push groups still use the default single pending slot. - Report-style workflows (`benchmark*.yml`, `bundle_size.yml`, `codeql-analysis.yml`) skip draft PRs with a job-level `if: ${{ !github.event.pull_request.draft }}` and list `ready_for_review` in `pull_request.types`, so they run once a PR is marked ready. Correctness checks (`editor-types`, `skills`, `website`) still run on drafts. - `paths` leave out what a workflow never reads, so test- or docs-only edits under `packages/` don't fan out: `__tests__/`, `typescript-tests/`, `src-*-types/` (legacy types) and `*.md`. `bundle_size.yml` also skips packages `examples/test-bundlesize` doesn't bundle (graphql, test, vue). - CodeQL triggers only on shipped source (`packages/*/src/**`, `packages/*/node.mjs`). `bundle_size.yml` is PR-only: on push the action measures but has nowhere to report. diff --git a/.github/workflows/site-release.yml b/.github/workflows/site-release.yml index 7a5f7f839724..4a2c7a532b19 100644 --- a/.github/workflows/site-release.yml +++ b/.github/workflows/site-release.yml @@ -29,31 +29,17 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 800 - # Queued runs aren't cancelled, so an older one skips when a newer push - # changed the site: that push's run, queued behind it, deploys. - # Paths match `on.paths`. - - name: Check for a newer site change - id: latest - run: | - # Deploys unless it can tell a newer site change landed (fail open) - rc=0 - git fetch -q origin "$GITHUB_REF" && - git diff --quiet "$GITHUB_SHA" FETCH_HEAD -- website docs/core docs/rest docs/graphql || rc=$? - if [ "$rc" = 1 ]; then - echo "::notice::Skipping: $(git rev-parse --short FETCH_HEAD) changed the site again and deploys it" - else - echo "deploy=true" >> "$GITHUB_OUTPUT" - fi + # Runs queue in push order, so a run can start after newer commits + # landed. Deploy the branch tip so production never goes backwards, even + # if the newer push has no run of its own ([skip ci]). + - name: Use the branch tip + run: git fetch -q origin "$GITHUB_REF" && git checkout -q FETCH_HEAD || echo "::warning::Could not fetch $GITHUB_REF; deploying $GITHUB_SHA" - name: Install Vercel CLI - if: steps.latest.outputs.deploy run: npm install --global vercel@latest - name: Pull Vercel Environment Information - if: steps.latest.outputs.deploy run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - name: Build Project Artifacts - if: steps.latest.outputs.deploy run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy Project Artifacts to Vercel - if: steps.latest.outputs.deploy continue-on-error: true run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file From 2201d6e23507f658e57c8d51c1111a7a747d7c20 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 20:24:38 +0000 Subject: [PATCH 07/11] ci: Read the dependency gate's paths from the calling workflow Hand-copied path lists could drift and silently skip a real change. The gate now reads the caller's own on..paths (github.workflow_ref names the caller), and babel-plugin-module-resolver, which babel.config.js requires directly, is a build tool. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- .cursor/rules/ci-config.mdc | 2 +- .github/workflows/benchmark-react.yml | 12 ------------ .github/workflows/benchmark-spread.yml | 12 ------------ .github/workflows/benchmark.yml | 11 ----------- .github/workflows/bundle_size.yml | 14 -------------- .github/workflows/dependency-gate.yml | 10 ++++------ scripts/ci-deps-relevant.mjs | 5 +++-- 7 files changed, 8 insertions(+), 58 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 7f303623baae..372dae61cff5 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -36,7 +36,7 @@ alwaysApply: false - Report-style workflows (`benchmark*.yml`, `bundle_size.yml`, `codeql-analysis.yml`) skip draft PRs with a job-level `if: ${{ !github.event.pull_request.draft }}` and list `ready_for_review` in `pull_request.types`, so they run once a PR is marked ready. Correctness checks (`editor-types`, `skills`, `website`) still run on drafts. - `paths` leave out what a workflow never reads, so test- or docs-only edits under `packages/` don't fan out: `__tests__/`, `typescript-tests/`, `src-*-types/` (legacy types) and `*.md`. `bundle_size.yml` also skips packages `examples/test-bundlesize` doesn't bundle (graphql, test, vue). - CodeQL triggers only on shipped source (`packages/*/src/**`, `packages/*/node.mjs`). `bundle_size.yml` is PR-only: on push the action measures but has nowhere to report. -- Bundle Size and the benchmarks list `yarn.lock` in `paths` but gate their main job on the reusable `dependency-gate.yml` (`scripts/ci-deps-relevant.mjs`). It runs the job when a non-manifest file in the workflow's paths changed, a manifest of the measured workspaces (or a workspace package they ship with) changed, or the yarn.lock resolutions reachable from their dependencies or the babel/browserslist/core-js build tooling changed. Bumps of test, lint, React Native or website tooling skip it; anything it can't classify runs. The gate's `with.paths` must mirror the workflow's `paths` (minus `yarn.lock`). +- Bundle Size and the benchmarks list `yarn.lock` in `paths` but gate their main job on the reusable `dependency-gate.yml` (`scripts/ci-deps-relevant.mjs`). It runs the job when a non-manifest file in the workflow's paths changed, a manifest of the measured workspaces (or a workspace package they ship with) changed, or the yarn.lock resolutions reachable from their dependencies or the babel/browserslist/core-js build tooling changed. Bumps of test, lint, React Native or website tooling skip it; anything it can't classify runs. The gate reads the caller's `on..paths` itself (via `github.workflow_ref` and `yq`), so there's one list. Root `devDependencies` aren't walked: add a new root build dependency to `BUILD_TOOLS`. - Renovate (`.github/renovate.json`) uses `rebaseWhen: conflicted`: master's branch protection requires up-to-date branches, which makes the default rebase every Renovate PR (and rerun all of its CI) on each master push. Update the branch before merging a Renovate PR; if that update isn't merged, Renovate treats the branch as edited and stops rebasing it until its PR's rebase checkbox is ticked. ## Vercel docs site (`website/scripts/vercel-ignore.sh`) diff --git a/.github/workflows/benchmark-react.yml b/.github/workflows/benchmark-react.yml index 12d4c99da577..8483962fb449 100644 --- a/.github/workflows/benchmark-react.yml +++ b/.github/workflows/benchmark-react.yml @@ -48,18 +48,6 @@ jobs: uses: ./.github/workflows/dependency-gate.yml with: workspaces: examples/benchmark-react - # `paths` above, one per line - paths: | - packages/react/src/** - packages/core/src/** - packages/endpoint/src/schemas/** - packages/normalizr/src/** - examples/benchmark-react/** - !packages/**/__tests__/** - !examples/benchmark-react/**/*.md - scripts/ci-deps-relevant.mjs - .github/workflows/dependency-gate.yml - .github/workflows/benchmark-react.yml benchmark-react: needs: changes diff --git a/.github/workflows/benchmark-spread.yml b/.github/workflows/benchmark-spread.yml index c6bcd638428d..ce350be1c29c 100644 --- a/.github/workflows/benchmark-spread.yml +++ b/.github/workflows/benchmark-spread.yml @@ -52,18 +52,6 @@ jobs: uses: ./.github/workflows/dependency-gate.yml with: workspaces: examples/benchmark - # `paths` above, one per line - paths: | - packages/core/src/state/** - packages/normalizr/src/normalize/** - packages/endpoint/src/schemas/EntityMixin.ts - !packages/**/__tests__/** - examples/benchmark/spread.js - examples/benchmark/spread-scenarios.js - examples/benchmark/schemas.js - scripts/ci-deps-relevant.mjs - .github/workflows/dependency-gate.yml - .github/workflows/benchmark-spread.yml benchmark-spread: needs: changes diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index f6ff1d84f070..096a0e05971b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -45,17 +45,6 @@ jobs: uses: ./.github/workflows/dependency-gate.yml with: workspaces: examples/benchmark - # `paths` above, one per line - paths: | - packages/normalizr/src/** - packages/endpoint/src/schemas/** - packages/core/src/** - examples/benchmark/** - !packages/**/__tests__/** - !examples/benchmark/**/*.md - scripts/ci-deps-relevant.mjs - .github/workflows/dependency-gate.yml - .github/workflows/benchmark.yml benchmark: needs: changes diff --git a/.github/workflows/bundle_size.yml b/.github/workflows/bundle_size.yml index d5316bcc2ced..b25bebc8f37d 100644 --- a/.github/workflows/bundle_size.yml +++ b/.github/workflows/bundle_size.yml @@ -32,20 +32,6 @@ jobs: uses: ./.github/workflows/dependency-gate.yml with: workspaces: examples/test-bundlesize - # `paths` above, one per line - paths: | - packages/** - !packages/**/__tests__/** - !packages/*/typescript-tests/** - !packages/*/src-*-types/** - !packages/**/*.md - !packages/graphql/** - !packages/test/** - !packages/vue/** - examples/test-bundlesize/** - scripts/ci-deps-relevant.mjs - .github/workflows/dependency-gate.yml - .github/workflows/bundle_size.yml build: needs: changes diff --git a/.github/workflows/dependency-gate.yml b/.github/workflows/dependency-gate.yml index 873c072765de..6c19689279f4 100644 --- a/.github/workflows/dependency-gate.yml +++ b/.github/workflows/dependency-gate.yml @@ -8,10 +8,6 @@ on: description: Comma-separated workspaces the job builds and runs required: true type: string - paths: - description: The caller's `paths` filter, one glob per line - required: true - type: string outputs: relevant: value: ${{ jobs.check.outputs.relevant }} @@ -32,9 +28,11 @@ jobs: env: BASE: ${{ github.event.pull_request.base.sha || github.event.before }} WORKSPACES: ${{ inputs.workspaces }} - PATHS: ${{ inputs.paths }} run: | # A missing base makes the script fail open git fetch -q --depth=1 origin "$BASE" || true - mapfile -t paths <<< "$PATHS" + # The caller's own `paths` filter (github.workflow_ref names the + # caller). Unreadable means none: any changed file counts. + workflow="${GITHUB_WORKFLOW_REF#*/*/}" + mapfile -t paths < <(yq -r ".on.${GITHUB_EVENT_NAME}.paths[]" "${workflow%@*}" || true) node scripts/ci-deps-relevant.mjs "$BASE" "$WORKSPACES" "${paths[@]}" diff --git a/scripts/ci-deps-relevant.mjs b/scripts/ci-deps-relevant.mjs index 76d2c9392749..a3cf5f581bc4 100644 --- a/scripts/ci-deps-relevant.mjs +++ b/scripts/ci-deps-relevant.mjs @@ -19,9 +19,10 @@ import { execFileSync } from 'node:child_process'; import { appendFileSync } from 'node:fs'; -// Root build tooling every package build uses (babel.config.js, browserslist) +// Root build tooling every package build uses (babel.config.js, browserslist). +// Add new root build dependencies here: root devDependencies aren't walked. const BUILD_TOOLS = - /^(@babel\/|core-js|browserslist$|caniuse-lite$|@anansi\/(babel-preset|browserslist-config)$)/; + /^(@babel\/|core-js|browserslist$|caniuse-lite$|babel-plugin-module-resolver$|@anansi\/(babel-preset|browserslist-config)$)/; // Type-only packages can't change built output or runtime behavior const IGNORED = /^(@types\/|typescript$|@typescript\/)/; const DEP_FIELDS = [ From b948f67764e6bb21add0289d0418766efe0fc142 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 20:25:46 +0000 Subject: [PATCH 08/11] ci: Drop the GitHub Actions production docs deploy Vercel's Git integration already deploys every master push to production (gated by vercel-ignore.sh), so site-release.yml deployed the same site a second time. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- .cursor/rules/ci-config.mdc | 5 ++-- .github/workflows/site-release.yml | 45 ------------------------------ website/scripts/vercel-ignore.sh | 2 +- 3 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 .github/workflows/site-release.yml diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 372dae61cff5..3ec61c9c9f03 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -29,10 +29,11 @@ alwaysApply: false - `skills.yml` `paths` must cover every input of `website/framework-docs/skillReferences.mjs` (docs, skill manifests, the generator and its deps). - `editor-types.yml` reruns `yarn copy:websitetypes` and fails if `website/src/components/Playground/editor-types` changes. It needs the `website` workspace (for deps like `bignumber.js`), which CircleCI's `setup` drops. Its `paths` must cover every input of `scripts/copywebsitetypes.sh`. - `site-preview.yml` runs one `build` job (one install for the typecheck and the build). It builds the site directly (no Vercel CLI, only the packages it imports via `ci:build:website`, `VERCEL_ENV=preview` to include drafts), restores Docusaurus' webpack cache (only master pushes save it, so PRs share one entry), and fails on any `[WARNING]`/`[ERROR]` line. Broken links are `warn` in `docusaurus.config.ts` so this check catches them without failing Vercel deploys. -- `site-preview.yml`/`site-release.yml` `paths` (`website/**`, `docs/{core,rest,graphql}/**`) must match `SITE_PATHS` in `website/scripts/vercel-ignore.sh`. +- Production docs deploys come from Vercel's Git integration only (gated by `vercel-ignore.sh`); there is no Actions deploy workflow. +- `site-preview.yml` `paths` (`website/**`, `docs/{core,rest,graphql}/**`) must match `SITE_PATHS` in `website/scripts/vercel-ignore.sh`. - `benchmark-react.yml` caches Playwright browsers keyed on the resolved `playwright` version from `examples/benchmark-react`; bumping playwright invalidates the cache automatically. - Benchmark workflows (`benchmark.yml`, `benchmark-react.yml`) tune the host (CPU governor, swapoff) and pin CPUs with `taskset` — they must run directly on the runner, not in a `container:`. -- Never cancel a run on master: a cancelled check marks the commit red, which hurts npm search scoring. PR runs cancel superseded ones (`group: -${{ github.head_ref || github.run_id }}`, `cancel-in-progress: true`); push runs get a unique group. Runs that must not overlap (`release.yml`, `beta-release.yml`, `site-release.yml`) use a shared group with `queue: max`, which keeps up to 100 pending runs in order instead of cancelling all but one (GitHub rejects it alongside `cancel-in-progress: true`; actionlint 1.7.12 doesn't know the key yet). `site-release.yml` deploys the branch tip, since a queued run can start after newer commits landed. The benchmark push groups still use the default single pending slot. +- Never cancel a run on master: a cancelled check marks the commit red, which hurts npm search scoring. PR runs cancel superseded ones (`group: -${{ github.head_ref || github.run_id }}`, `cancel-in-progress: true`); push runs get a unique group. Runs that must not overlap (`release.yml`, `beta-release.yml`) use a shared group with `queue: max`, which keeps up to 100 pending runs in order instead of cancelling all but one (GitHub rejects it alongside `cancel-in-progress: true`; actionlint 1.7.12 doesn't know the key yet). The benchmark push groups still use the default single pending slot. - Report-style workflows (`benchmark*.yml`, `bundle_size.yml`, `codeql-analysis.yml`) skip draft PRs with a job-level `if: ${{ !github.event.pull_request.draft }}` and list `ready_for_review` in `pull_request.types`, so they run once a PR is marked ready. Correctness checks (`editor-types`, `skills`, `website`) still run on drafts. - `paths` leave out what a workflow never reads, so test- or docs-only edits under `packages/` don't fan out: `__tests__/`, `typescript-tests/`, `src-*-types/` (legacy types) and `*.md`. `bundle_size.yml` also skips packages `examples/test-bundlesize` doesn't bundle (graphql, test, vue). - CodeQL triggers only on shipped source (`packages/*/src/**`, `packages/*/node.mjs`). `bundle_size.yml` is PR-only: on push the action measures but has nowhere to report. diff --git a/.github/workflows/site-release.yml b/.github/workflows/site-release.yml deleted file mode 100644 index 4a2c7a532b19..000000000000 --- a/.github/workflows/site-release.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Vercel Release Deployment -env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} -on: - push: - branches: - - master - - rest-hooks-site - # Published site only. Keep in sync with website/scripts/vercel-ignore.sh. - paths: - - 'website/**' - - 'docs/core/**' - - 'docs/rest/**' - - 'docs/graphql/**' - - '.github/workflows/site-release.yml' - -# One deploy at a time, in push order. `queue: max` keeps every pending run -# instead of cancelling it (a cancelled run marks master red). -concurrency: - group: site-release-${{ github.ref }} - queue: max - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 800 - # Runs queue in push order, so a run can start after newer commits - # landed. Deploy the branch tip so production never goes backwards, even - # if the newer push has no run of its own ([skip ci]). - - name: Use the branch tip - run: git fetch -q origin "$GITHUB_REF" && git checkout -q FETCH_HEAD || echo "::warning::Could not fetch $GITHUB_REF; deploying $GITHUB_SHA" - - name: Install Vercel CLI - run: npm install --global vercel@latest - - name: Pull Vercel Environment Information - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - - name: Build Project Artifacts - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - - name: Deploy Project Artifacts to Vercel - continue-on-error: true - run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file diff --git a/website/scripts/vercel-ignore.sh b/website/scripts/vercel-ignore.sh index 34b493ac3f2c..b200e414f812 100755 --- a/website/scripts/vercel-ignore.sh +++ b/website/scripts/vercel-ignore.sh @@ -12,7 +12,7 @@ set -u # The published site: the Docusaurus app plus the doc trees it compiles. -# Keep in sync with the `paths` of site-preview.yml and site-release.yml. +# Keep in sync with the `paths` of site-preview.yml. SITE_PATHS=( website docs/core docs/rest docs/graphql ':(exclude)website/CHANGELOG.md' From a90ce992621a87ffdeb1f0d09f3d634bb6096c99 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 20:31:10 +0000 Subject: [PATCH 09/11] Deepen Vercel's shallow clone for docs last-updated dates Without the Actions deploy (fetch-depth 800), Vercel's ~10 commit clone dated every untouched page to the clone boundary. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- .cursor/rules/ci-config.mdc | 2 +- website/docusaurus.config.ts | 2 ++ website/scripts/deepenGitHistory.cjs | 38 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 website/scripts/deepenGitHistory.cjs diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 3ec61c9c9f03..ee55396f3216 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -29,7 +29,7 @@ alwaysApply: false - `skills.yml` `paths` must cover every input of `website/framework-docs/skillReferences.mjs` (docs, skill manifests, the generator and its deps). - `editor-types.yml` reruns `yarn copy:websitetypes` and fails if `website/src/components/Playground/editor-types` changes. It needs the `website` workspace (for deps like `bignumber.js`), which CircleCI's `setup` drops. Its `paths` must cover every input of `scripts/copywebsitetypes.sh`. - `site-preview.yml` runs one `build` job (one install for the typecheck and the build). It builds the site directly (no Vercel CLI, only the packages it imports via `ci:build:website`, `VERCEL_ENV=preview` to include drafts), restores Docusaurus' webpack cache (only master pushes save it, so PRs share one entry), and fails on any `[WARNING]`/`[ERROR]` line. Broken links are `warn` in `docusaurus.config.ts` so this check catches them without failing Vercel deploys. -- Production docs deploys come from Vercel's Git integration only (gated by `vercel-ignore.sh`); there is no Actions deploy workflow. +- Production docs deploys come from Vercel's Git integration only (gated by `vercel-ignore.sh`); there is no Actions deploy workflow. Vercel clones ~10 commits deep, so `website/scripts/deepenGitHistory.cjs` (called from `docusaurus.config.ts`) fetches 800 more for the "Last updated" dates. - `site-preview.yml` `paths` (`website/**`, `docs/{core,rest,graphql}/**`) must match `SITE_PATHS` in `website/scripts/vercel-ignore.sh`. - `benchmark-react.yml` caches Playwright browsers keyed on the resolved `playwright` version from `examples/benchmark-react`; bumping playwright invalidates the cache automatically. - Benchmark workflows (`benchmark.yml`, `benchmark-react.yml`) tune the host (CPU governor, swapoff) and pin CPUs with `taskset` — they must run directly on the runner, not in a `container:`. diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 29166f0498f4..61a6820aa1bb 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -12,6 +12,8 @@ import versions from './versions.json'; // Keep Monaco CDN preload hashes in sync with the installed monaco-editor package. const require = createRequire(path.join(__dirname, 'package.json')); require('./scripts/generateMonacoPreloads.cjs').ensureMonacoPreloadManifest(); +// Real history for "Last updated" dates (Vercel clones shallow) +require('./scripts/deepenGitHistory.cjs').deepenGitHistory(); //const versionsRest = require('./rest_versions.json'); diff --git a/website/scripts/deepenGitHistory.cjs b/website/scripts/deepenGitHistory.cjs new file mode 100644 index 000000000000..d1cff010485e --- /dev/null +++ b/website/scripts/deepenGitHistory.cjs @@ -0,0 +1,38 @@ +'use strict'; + +/* eslint-disable no-undef */ + +/** + * Vercel builds from a ~10 commit shallow clone, so Docusaurus would date every + * page not touched in those commits to the oldest commit in the clone + * ("Last updated"). Fetches more history first. Fails open: a failed fetch + * only leaves the dates as they were. + * + * Invoked from docusaurus.config.ts. Does nothing outside Vercel builds. + */ + +const { execFileSync } = require('child_process'); + +const DEPTH = '800'; + +function deepenGitHistory() { + const ref = process.env.VERCEL_GIT_COMMIT_REF; + if (!process.env.VERCEL || !ref) return; + const git = args => + execFileSync('git', args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + timeout: 60_000, + }).trim(); + try { + if (git(['rev-parse', '--is-shallow-repository']) !== 'true') return; + git(['fetch', '-q', '--no-tags', `--deepen=${DEPTH}`, 'origin', ref]); + console.log( + `Deepened git history by ${DEPTH} commits for last-update dates`, + ); + } catch { + console.warn('Could not deepen git history; last-update dates may be off'); + } +} + +module.exports = { deepenGitHistory }; From 47a5f37177dcbf093bf5e3d6136f1896b9f42ebe Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 22:09:06 +0000 Subject: [PATCH 10/11] Fetch docs history by repo URL: Vercel's clone has no origin remote Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- website/scripts/deepenGitHistory.cjs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/website/scripts/deepenGitHistory.cjs b/website/scripts/deepenGitHistory.cjs index d1cff010485e..2ff7e0fde169 100644 --- a/website/scripts/deepenGitHistory.cjs +++ b/website/scripts/deepenGitHistory.cjs @@ -16,8 +16,13 @@ const { execFileSync } = require('child_process'); const DEPTH = '800'; function deepenGitHistory() { - const ref = process.env.VERCEL_GIT_COMMIT_REF; - if (!process.env.VERCEL || !ref) return; + const { + VERCEL, + VERCEL_GIT_COMMIT_REF: ref, + VERCEL_GIT_REPO_OWNER: owner, + VERCEL_GIT_REPO_SLUG: slug, + } = process.env; + if (!VERCEL || !ref || !owner || !slug) return; const git = args => execFileSync('git', args, { encoding: 'utf8', @@ -26,12 +31,16 @@ function deepenGitHistory() { }).trim(); try { if (git(['rev-parse', '--is-shallow-repository']) !== 'true') return; - git(['fetch', '-q', '--no-tags', `--deepen=${DEPTH}`, 'origin', ref]); + // Vercel's clone has no `origin` remote, so fetch the (public) repo by URL + const url = `https://github.com/${owner}/${slug}.git`; + git(['fetch', '-q', '--no-tags', `--deepen=${DEPTH}`, url, ref]); console.log( `Deepened git history by ${DEPTH} commits for last-update dates`, ); - } catch { - console.warn('Could not deepen git history; last-update dates may be off'); + } catch (e) { + console.warn( + `Could not deepen git history; last-update dates may be off (${e.message.split('\n')[0]})`, + ); } } From 292a67e396c4c377698a2bad2d9ec66c9a1f836f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 22:10:02 +0000 Subject: [PATCH 11/11] Log the docs history deepen outcome: new depth, skip reason or git stderr Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0162EbzKf3kYr427Qe51L2jN --- website/scripts/deepenGitHistory.cjs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/website/scripts/deepenGitHistory.cjs b/website/scripts/deepenGitHistory.cjs index 2ff7e0fde169..93397ac82014 100644 --- a/website/scripts/deepenGitHistory.cjs +++ b/website/scripts/deepenGitHistory.cjs @@ -22,24 +22,27 @@ function deepenGitHistory() { VERCEL_GIT_REPO_OWNER: owner, VERCEL_GIT_REPO_SLUG: slug, } = process.env; - if (!VERCEL || !ref || !owner || !slug) return; + if (!VERCEL) return; + const log = msg => console.log(`deepenGitHistory: ${msg}`); + if (!ref || !owner || !slug) return log('skipped, Vercel git env missing'); const git = args => execFileSync('git', args, { encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], + stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000, }).trim(); + const commits = () => git(['rev-list', '--count', 'HEAD']); try { - if (git(['rev-parse', '--is-shallow-repository']) !== 'true') return; + if (git(['rev-parse', '--is-shallow-repository']) !== 'true') + return log(`skipped, clone not shallow (${commits()} commits)`); + const before = commits(); // Vercel's clone has no `origin` remote, so fetch the (public) repo by URL const url = `https://github.com/${owner}/${slug}.git`; git(['fetch', '-q', '--no-tags', `--deepen=${DEPTH}`, url, ref]); - console.log( - `Deepened git history by ${DEPTH} commits for last-update dates`, - ); + log(`history deepened from ${before} to ${commits()} commits`); } catch (e) { - console.warn( - `Could not deepen git history; last-update dates may be off (${e.message.split('\n')[0]})`, + log( + `FAILED, last-update dates will be wrong (exit ${e.status ?? e.signal}): ${String(e.stderr || e.message).trim()}`, ); } }