From 126c8243a7885b07d4dc3de4170e31062746ed9b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 4 Oct 2026 03:40:16 +0000 Subject: [PATCH 1/7] internal(ci): Build the docs site only when its inputs change Vercel preview builds used git diff HEAD^ HEAD. Merging master into a package or CI pull request makes that the incoming master tree, so site commits already on master started a full preview build. Previews also ignored docs/core, docs/rest, and docs/graphql, which are the published pages. The ignore script builds for website changes (not the changelog or unit tests) and those doc trees. Preview branches compare the branch's own changes, so an upstream merge no longer counts. GitHub Actions site workflows use the same paths. Co-authored-by: Nathaniel Tucker --- .cursor/rules/ci-config.mdc | 8 ++ .github/workflows/site-preview.yml | 7 +- .github/workflows/site-release.yml | 5 +- website/scripts/vercel-ignore.sh | 175 ++++++++++++++++++++++++++ website/scripts/vercel-ignore.test.sh | 173 +++++++++++++++++++++++++ website/vercel.json | 2 +- 6 files changed, 367 insertions(+), 3 deletions(-) create mode 100755 website/scripts/vercel-ignore.sh create mode 100755 website/scripts/vercel-ignore.test.sh diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index a67967962284..865b525955b2 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -19,5 +19,13 @@ alwaysApply: false ## GitHub Actions (`.github/workflows/`) - Workflows install only needed workspaces via `./scripts/ci-install.sh [extra-workspace ...]`. +- Docs site builds (`site-preview.yml`, `site-release.yml`) run only for the published site: `website/**`, `docs/core/**`, `docs/rest/**`, `docs/graphql/**`, and the workflow file itself. Keep that list in sync with `website/scripts/vercel-ignore.sh`. Package, example, CI, changeset, and other docs changes (including `docs/ROADMAP.md`) do not start those workflows. `site-preview` builds on the runner and does not deploy; the Vercel Git integration publishes the preview. - `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:`. + +## Vercel docs site (`website/scripts/vercel-ignore.sh`) + +- `website/vercel.json` `ignoreCommand` runs that script from the `website/` root. Exit 0 skips the build; exit 1 builds. Vercel treats any other status as build, so the script exits 0 only when it can see that the published site is unchanged, and builds if the diff cannot be computed. +- Same paths as the workflows, minus files that are not published: `website/CHANGELOG.md`, `website/**/__tests__/**`, and `website/**/*.test.*`. `gh-pages*` branches always skip. +- Preview branches compare the pull request's own changes (last successful preview, or merge-base with `master`). Do not use `git diff HEAD^ HEAD` there: merging `master` into a package or CI PR makes that diff the incoming master tree and starts a full preview build. Production (`master`, `rest-hooks-site`, or `VERCEL_ENV=production`) compares against the last successful deploy, or `HEAD^` for a squash merge. +- A skipped build still creates a Vercel deployment long enough to run this script, then cancels it. `git.deploymentEnabled: false` does not stick when the project dashboard overrides it. diff --git a/.github/workflows/site-preview.yml b/.github/workflows/site-preview.yml index a45fb423020d..074af3caeb42 100644 --- a/.github/workflows/site-preview.yml +++ b/.github/workflows/site-preview.yml @@ -6,9 +6,12 @@ on: pull_request: branches: - master + # Published site only. Keep in sync with website/scripts/vercel-ignore.sh. paths: - 'website/**' - - 'docs/**' + - 'docs/core/**' + - 'docs/rest/**' + - 'docs/graphql/**' - '.github/workflows/site-preview.yml' concurrency: @@ -23,6 +26,8 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 1 + - name: Test Vercel ignore decision + run: bash website/scripts/vercel-ignore.test.sh - uses: actions/setup-node@v6 with: node-version: '26' diff --git a/.github/workflows/site-release.yml b/.github/workflows/site-release.yml index 9192847e87d2..651a5dd5c242 100644 --- a/.github/workflows/site-release.yml +++ b/.github/workflows/site-release.yml @@ -7,9 +7,12 @@ on: branches: - master - rest-hooks-site + # Published site only. Keep in sync with website/scripts/vercel-ignore.sh. paths: - 'website/**' - - 'docs/**' + - 'docs/core/**' + - 'docs/rest/**' + - 'docs/graphql/**' - '.github/workflows/site-release.yml' concurrency: diff --git a/website/scripts/vercel-ignore.sh b/website/scripts/vercel-ignore.sh new file mode 100755 index 000000000000..b162cc895235 --- /dev/null +++ b/website/scripts/vercel-ignore.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# Decide whether Vercel should build the docs site. +# +# Exit 0 skips the build. Exit 1 builds. Vercel treats every non-zero status +# as "build", so this script exits 0 only when it can see that the published +# site is unchanged. If git history is missing, it builds (fail open). +# +# The published site is website/ (Docusaurus app, blog, pages, static) plus +# the doc trees Docusaurus compiles: docs/core, docs/rest, docs/graphql. +# Package, example, CI, changeset, and other docs changes do not build. +# +# Preview branches must not use `git diff HEAD^ HEAD`. Merging master into a +# pull request makes that diff the incoming master tree, so a site commit +# already on master starts a full preview build of an unrelated PR. + +set -u + +if [[ "${VERCEL_GIT_COMMIT_REF:-}" == gh-pages* ]]; then + echo "vercel-ignore: skip — gh-pages branch" + exit 0 +fi + +cd "$(git rev-parse --show-toplevel)" || { + echo "vercel-ignore: build — cannot find repo root" + exit 1 +} + +# 0 when this path can change the published site. +is_site_file() { + local f="$1" + case "$f" in + website/*) ;; + docs/core | docs/core/*) ;; + docs/rest | docs/rest/*) ;; + docs/graphql | docs/graphql/*) ;; + *) return 1 ;; + esac + case "$f" in + website/CHANGELOG.md) return 1 ;; + esac + if [[ "$f" == website/* && "$f" == *"/__tests__/"* ]]; then + return 1 + fi + if [[ "$f" == website/* ]]; then + case "$f" in + *.test.ts | *.test.tsx | *.test.js | *.test.jsx | *.test.mjs | *.test.cjs | *.test.sh) + return 1 + ;; + esac + fi + return 0 +} + +# Prints site paths changed between $1 and $2. +# Returns 0 if any, 1 if none, 2 if the diff could not be computed. +site_diff() { + local base="$1" head="$2" f found=0 tmp + if ! git cat-file -e "${base}^{commit}" >/dev/null 2>&1; then + return 2 + fi + if ! git cat-file -e "${head}^{commit}" >/dev/null 2>&1; then + return 2 + fi + tmp="$(mktemp)" + if ! git diff --name-only --no-renames "$base" "$head" >"$tmp"; then + rm -f "$tmp" + return 2 + fi + while IFS= read -r f; do + if [ -n "$f" ] && is_site_file "$f"; then + printf '%s\n' "$f" + found=1 + fi + done <"$tmp" + rm -f "$tmp" + [ "$found" -eq 1 ] +} + +build() { + echo "vercel-ignore: build — $*" + exit 1 +} + +skip() { + echo "vercel-ignore: skip — $*" + exit 0 +} + +decide() { + local files rc + files="$(site_diff "$1" "$2")" + rc=$? + case "$rc" in + 0) build "$3: ${files//$'\n'/, }" ;; + 1) skip "$3" ;; + *) build "could not diff $1..$2" ;; + esac +} + +is_ancestor() { + [ -n "${1:-}" ] && git merge-base --is-ancestor "$1" "$2" >/dev/null 2>&1 +} + +# Production deploys the branch's own history (squash merges are one commit). +# Previews compare the branch's changes, not commits merged in from upstream. +production_ref() { + case "${VERCEL_GIT_COMMIT_REF:-}" in + master | rest-hooks-site) return 0 ;; + esac + [ "${VERCEL_ENV:-}" = "production" ] +} + +ensure_master() { + if git rev-parse --verify -q origin/master >/dev/null 2>&1; then + echo origin/master + return 0 + fi + # A local master branch can be stale. Fetch the remote first, and only then + # fall back to it. Bound the fetch so a hung network call cannot hold a + # Vercel build machine. + if command -v timeout >/dev/null 2>&1; then + timeout 15 git fetch --no-tags --depth=80 origin master:refs/remotes/origin/master >/dev/null 2>&1 || true + else + git fetch --no-tags --depth=80 origin master:refs/remotes/origin/master >/dev/null 2>&1 || true + fi + if git rev-parse --verify -q origin/master >/dev/null 2>&1; then + echo origin/master + return 0 + fi + if git rev-parse --verify -q master >/dev/null 2>&1; then + echo master + return 0 + fi + return 1 +} + +prev="${VERCEL_GIT_PREVIOUS_SHA:-}" + +if production_ref; then + if is_ancestor "$prev" HEAD; then + decide "$prev" HEAD "production changes since ${prev:0:12}" + elif git rev-parse --verify -q 'HEAD^' >/dev/null 2>&1; then + decide 'HEAD^' HEAD "production changes in $(git rev-parse --short HEAD)" + else + build "production commit has no parent" + fi +fi + +# Merge commit on a preview branch: parents are (branch tip, upstream). +# Diff against the upstream parent so master's files are not "our" changes. +# If this branch already deployed and the branch tip has no new site files +# since then, merging upstream does not need another preview. +if git rev-parse --verify -q 'HEAD^2' >/dev/null 2>&1; then + if is_ancestor "$prev" 'HEAD^1'; then + decide "$prev" 'HEAD^1' "preview changes since ${prev:0:12} (merge)" + else + decide 'HEAD^2' HEAD "preview changes vs upstream" + fi +fi + +if is_ancestor "$prev" HEAD; then + decide "$prev" HEAD "preview changes since ${prev:0:12}" +fi + +if master="$(ensure_master)"; then + if base="$(git merge-base HEAD "$master" 2>/dev/null)" && [ -n "$base" ]; then + decide "$base" HEAD "preview changes vs ${master}" + fi +fi + +if git rev-parse --verify -q 'HEAD^' >/dev/null 2>&1; then + decide 'HEAD^' HEAD "preview changes in $(git rev-parse --short HEAD)" +fi + +build "no base to compare" diff --git a/website/scripts/vercel-ignore.test.sh b/website/scripts/vercel-ignore.test.sh new file mode 100755 index 000000000000..4e1f69cb7900 --- /dev/null +++ b/website/scripts/vercel-ignore.test.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# Exercises website/scripts/vercel-ignore.sh against a throwaway repo. +set -euo pipefail + +root="$(cd "$(dirname "$0")/../.." && pwd)" +script="$root/website/scripts/vercel-ignore.sh" +repo="$(mktemp -d)" +trap 'rm -rf "$repo"' EXIT + +git -C "$repo" init -b master >/dev/null +git -C "$repo" config user.email "vercel-ignore-test@example.com" +git -C "$repo" config user.name "vercel-ignore-test" +git -C "$repo" config commit.gpgsign false +git -C "$repo" config core.fsmonitor false +git -C "$repo" config core.untrackedcache false + +commit() { + local msg="$1" + shift + local path + for path in "$@"; do + mkdir -p "$repo/$(dirname "$path")" + printf '%s\n' "$msg" >>"$repo/$path" + git -C "$repo" add -- "$path" + done + git -C "$repo" commit -m "$msg" >/dev/null +} + +run_ignore() { + local ref="$1" + local prev="${2-}" + ( + cd "$repo" + VERCEL_GIT_COMMIT_REF="$ref" \ + VERCEL_GIT_PREVIOUS_SHA="$prev" \ + VERCEL_ENV="${3:-}" \ + bash "$script" + ) +} + +expect_skip() { + local name="$1" + shift + local out rc + set +e + out="$(run_ignore "$@" 2>&1)" + rc=$? + set -e + if [ "$rc" -ne 0 ]; then + printf 'FAIL %s: expected skip (0), got %s\n%s\n' "$name" "$rc" "$out" >&2 + exit 1 + fi + printf 'ok %s\n' "$name" +} + +expect_build() { + local name="$1" + shift + local out rc + set +e + out="$(run_ignore "$@" 2>&1)" + rc=$? + set -e + if [ "$rc" -ne 1 ]; then + printf 'FAIL %s: expected build (1), got %s\n%s\n' "$name" "$rc" "$out" >&2 + exit 1 + fi + printf 'ok %s\n' "$name" +} + +commit "init" README.md + +# --- production (master): one squash commit --- +commit "pkg" packages/core/src/index.ts +expect_skip "master package-only" master + +commit "docs page" docs/core/api/Controller.md +expect_build "master docs/core" master + +commit "roadmap" docs/ROADMAP.md +expect_skip "master docs/ROADMAP.md" master + +commit "prettier" docs/.prettierrc +expect_skip "master docs/.prettierrc" master + +commit "blog" website/blog/2026-10-04-note.md +expect_build "master website blog" master + +commit "changelog" website/CHANGELOG.md +expect_skip "master website changelog" master + +commit "unit test" website/src/components/Playground/__tests__/transformCode.test.ts +expect_skip "master website unit test" master + +commit "colocated test" website/src/components/Playground/transformCode.test.ts website/scripts/vercel-ignore.test.sh +expect_skip "master colocated website tests" master + +commit "ci" .circleci/config.yml .github/workflows/benchmark.yml +expect_skip "master CI-only" master + +# Last successful production deploy was before a package commit. Still skip. +pkg_sha="$(git -C "$repo" rev-parse HEAD)" +commit "another pkg" packages/rest/src/index.ts +expect_skip "master package since previous deploy" master "$pkg_sha" + +# --- preview branch, linear --- +git -C "$repo" checkout -b feature >/dev/null 2>&1 +commit "feature pkg" packages/vue/src/index.ts .changeset/preview.md +expect_skip "preview package and changeset" feature + +commit "feature roadmap" docs/ROADMAP.md +expect_skip "preview docs that are not published" feature + +commit "feature page" docs/rest/api/Entity.md +expect_build "preview docs/rest" feature + +commit "feature ci follow-up" .github/workflows/site-preview.yml +# Whole branch still contains docs/rest, and there is no previous deploy, +# so the site change must still build. +expect_build "preview follow-up after unpublished site change" feature + +page_sha="$(git -C "$repo" rev-parse HEAD)" +commit "feature pkg follow-up" packages/core/src/other.ts +expect_skip "preview package follow-up after site deploy" feature "$page_sha" + +commit "playground source" website/src/components/Playground/transformCode.ts +expect_build "preview playground source since last deploy" feature "$page_sha" + +# Multi-commit branch with no prior deploy: the site edit is not the tip. +git -C "$repo" checkout master >/dev/null 2>&1 +git -C "$repo" checkout -b stacked >/dev/null 2>&1 +commit "stacked site" website/src/pages/index.js +commit "stacked pkg" packages/endpoint/src/index.ts +expect_build "preview multi-commit site change not at tip" stacked + +# --- merge master into a package PR (the credit-burn case) --- +git -C "$repo" checkout master >/dev/null 2>&1 +commit "master site moves" website/src/pages/index.js docs/core/concepts/overview.md +master_sha="$(git -C "$repo" rev-parse HEAD)" + +git -C "$repo" checkout -b pkg-pr >/dev/null 2>&1 +# Branch point is the commit BEFORE "master site moves" only if we reset. +# Recreate the PR from the parent of that master commit so master is ahead. +git -C "$repo" reset --hard HEAD^ >/dev/null +commit "pr packages" packages/core/src/set.ts .circleci/config.yml +git -C "$repo" merge --no-edit "$master_sha" >/dev/null +expect_skip "preview merge of master into package PR" pkg-pr + +# Site PR already deployed, then merges a master that also changed the site. +git -C "$repo" checkout master >/dev/null 2>&1 +git -C "$repo" checkout -b site-pr >/dev/null 2>&1 +git -C "$repo" reset --hard HEAD^ >/dev/null +commit "pr website" website/docusaurus.config.ts +deployed="$(git -C "$repo" rev-parse HEAD)" +git -C "$repo" merge --no-edit "$master_sha" >/dev/null +expect_skip "preview merge after the site commit already deployed" site-pr "$deployed" + +# Site PR that has never deployed, merged with master: still build. +git -C "$repo" checkout -B site-pr-fresh "$deployed" >/dev/null 2>&1 +git -C "$repo" merge --no-edit "$master_sha" >/dev/null +expect_build "preview merge of a site PR with no prior deploy" site-pr-fresh + +# gh-pages branches never build, even if website files differ. +expect_skip "gh-pages branch" gh-pages-bench + +# VERCEL_ENV=production uses this branch's history, not the diff against master. +# A site commit still builds; a later package-only commit does not. +git -C "$repo" checkout master >/dev/null 2>&1 +expect_build "production env site tip" other-branch "" production +commit "prod pkg" packages/normalizr/src/index.ts +expect_skip "production env package tip" other-branch "" production + +echo "all vercel-ignore cases passed" diff --git a/website/vercel.json b/website/vercel.json index ee2a9de83d7e..b79686e64824 100644 --- a/website/vercel.json +++ b/website/vercel.json @@ -1,4 +1,4 @@ { "cleanUrls": true, - "ignoreCommand": "case \"$VERCEL_GIT_COMMIT_REF\" in gh-pages*) exit 0;; esac; if [ \"$VERCEL_GIT_COMMIT_REF\" = \"master\" ]; then git diff HEAD^ HEAD --quiet -- . ../docs/; else git diff HEAD^ HEAD --quiet -- .; fi" + "ignoreCommand": "bash scripts/vercel-ignore.sh" } From acf34c6eeef29a64e595400720b9702d9f5399a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 09:06:37 +0000 Subject: [PATCH 2/7] internal(ci): Simplify Vercel ignore script Replace the per-file shell matcher with git pathspec excludes, collapse the duplicated test helpers into one `expect`, and add test cases for docs/graphql and non-test files under __tests__. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BamoUG3kFNUhtM7qYaGBgk --- .cursor/rules/ci-config.mdc | 7 +- website/scripts/vercel-ignore.sh | 175 +++++++------------------- website/scripts/vercel-ignore.test.sh | 109 +++++++--------- 3 files changed, 89 insertions(+), 202 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 865b525955b2..bcdd73da779a 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -25,7 +25,6 @@ alwaysApply: false ## Vercel docs site (`website/scripts/vercel-ignore.sh`) -- `website/vercel.json` `ignoreCommand` runs that script from the `website/` root. Exit 0 skips the build; exit 1 builds. Vercel treats any other status as build, so the script exits 0 only when it can see that the published site is unchanged, and builds if the diff cannot be computed. -- Same paths as the workflows, minus files that are not published: `website/CHANGELOG.md`, `website/**/__tests__/**`, and `website/**/*.test.*`. `gh-pages*` branches always skip. -- Preview branches compare the pull request's own changes (last successful preview, or merge-base with `master`). Do not use `git diff HEAD^ HEAD` there: merging `master` into a package or CI PR makes that diff the incoming master tree and starts a full preview build. Production (`master`, `rest-hooks-site`, or `VERCEL_ENV=production`) compares against the last successful deploy, or `HEAD^` for a squash merge. -- A skipped build still creates a Vercel deployment long enough to run this script, then cancels it. `git.deploymentEnabled: false` does not stick when the project dashboard overrides it. +- `website/vercel.json` `ignoreCommand` runs it: exit 0 skips, exit 1 builds. It skips only when it can see the published site is unchanged, so any git failure builds. `SITE_PATHS` matches the workflow `paths` minus unpublished files (`website/CHANGELOG.md`, `website/**/__tests__/**`, `website/**/*.test.*`); `gh-pages*` branches always skip. `website/scripts/vercel-ignore.test.sh` covers each rule. +- Previews diff the PR's own changes (since the last preview, against a merge commit's upstream parent, or from the merge-base with `master`). Never `git diff HEAD^ HEAD` there: after merging `master` into a package PR that diff is master's site changes. Production (`master`, `rest-hooks-site`, `VERCEL_ENV=production`) diffs since the last deploy, else `HEAD^`. +- A skipped build still creates a canceled Vercel deployment; `git.deploymentEnabled: false` does not stick while the project dashboard overrides it. diff --git a/website/scripts/vercel-ignore.sh b/website/scripts/vercel-ignore.sh index b162cc895235..99329d69b8f6 100755 --- a/website/scripts/vercel-ignore.sh +++ b/website/scripts/vercel-ignore.sh @@ -5,76 +5,20 @@ # as "build", so this script exits 0 only when it can see that the published # site is unchanged. If git history is missing, it builds (fail open). # -# The published site is website/ (Docusaurus app, blog, pages, static) plus -# the doc trees Docusaurus compiles: docs/core, docs/rest, docs/graphql. -# Package, example, CI, changeset, and other docs changes do not build. -# # Preview branches must not use `git diff HEAD^ HEAD`. Merging master into a # pull request makes that diff the incoming master tree, so a site commit # already on master starts a full preview build of an unrelated PR. set -u -if [[ "${VERCEL_GIT_COMMIT_REF:-}" == gh-pages* ]]; then - echo "vercel-ignore: skip — gh-pages branch" - exit 0 -fi - -cd "$(git rev-parse --show-toplevel)" || { - echo "vercel-ignore: build — cannot find repo root" - exit 1 -} - -# 0 when this path can change the published site. -is_site_file() { - local f="$1" - case "$f" in - website/*) ;; - docs/core | docs/core/*) ;; - docs/rest | docs/rest/*) ;; - docs/graphql | docs/graphql/*) ;; - *) return 1 ;; - esac - case "$f" in - website/CHANGELOG.md) return 1 ;; - esac - if [[ "$f" == website/* && "$f" == *"/__tests__/"* ]]; then - return 1 - fi - if [[ "$f" == website/* ]]; then - case "$f" in - *.test.ts | *.test.tsx | *.test.js | *.test.jsx | *.test.mjs | *.test.cjs | *.test.sh) - return 1 - ;; - esac - fi - return 0 -} - -# Prints site paths changed between $1 and $2. -# Returns 0 if any, 1 if none, 2 if the diff could not be computed. -site_diff() { - local base="$1" head="$2" f found=0 tmp - if ! git cat-file -e "${base}^{commit}" >/dev/null 2>&1; then - return 2 - fi - if ! git cat-file -e "${head}^{commit}" >/dev/null 2>&1; then - return 2 - fi - tmp="$(mktemp)" - if ! git diff --name-only --no-renames "$base" "$head" >"$tmp"; then - rm -f "$tmp" - return 2 - fi - while IFS= read -r f; do - if [ -n "$f" ] && is_site_file "$f"; then - printf '%s\n' "$f" - found=1 - fi - done <"$tmp" - rm -f "$tmp" - [ "$found" -eq 1 ] -} +# 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. +SITE_PATHS=( + website docs/core docs/rest docs/graphql + ':(exclude)website/CHANGELOG.md' + ':(exclude,glob)website/**/__tests__/**' + ':(exclude,glob)website/**/*.test.*' +) build() { echo "vercel-ignore: build — $*" @@ -86,90 +30,59 @@ skip() { exit 0 } +[[ "${VERCEL_GIT_COMMIT_REF:-}" == gh-pages* ]] && skip "gh-pages branch" + +cd "$(git rev-parse --show-toplevel)" || build "cannot find repo root" + +# Builds if site paths changed between $1 and $2 (or the diff fails); else skips. decide() { - local files rc - files="$(site_diff "$1" "$2")" - rc=$? - case "$rc" in - 0) build "$3: ${files//$'\n'/, }" ;; - 1) skip "$3" ;; - *) build "could not diff $1..$2" ;; - esac + local files + files="$(git diff --name-only --no-renames "$1" "$2" -- "${SITE_PATHS[@]}" 2>/dev/null)" || + build "could not diff $1..$2" + [ -n "$files" ] || skip "$3" + build "$3: ${files//$'\n'/, }" } -is_ancestor() { - [ -n "${1:-}" ] && git merge-base --is-ancestor "$1" "$2" >/dev/null 2>&1 +has_rev() { + git rev-parse --verify -q "$1" >/dev/null } -# Production deploys the branch's own history (squash merges are one commit). -# Previews compare the branch's changes, not commits merged in from upstream. -production_ref() { - case "${VERCEL_GIT_COMMIT_REF:-}" in - master | rest-hooks-site) return 0 ;; - esac - [ "${VERCEL_ENV:-}" = "production" ] +is_ancestor() { + [ -n "$1" ] && git merge-base --is-ancestor "$1" "$2" 2>/dev/null } -ensure_master() { - if git rev-parse --verify -q origin/master >/dev/null 2>&1; then - echo origin/master - return 0 - fi - # A local master branch can be stale. Fetch the remote first, and only then - # fall back to it. Bound the fetch so a hung network call cannot hold a - # Vercel build machine. - if command -v timeout >/dev/null 2>&1; then - timeout 15 git fetch --no-tags --depth=80 origin master:refs/remotes/origin/master >/dev/null 2>&1 || true - else - git fetch --no-tags --depth=80 origin master:refs/remotes/origin/master >/dev/null 2>&1 || true - fi - if git rev-parse --verify -q origin/master >/dev/null 2>&1; then - echo origin/master - return 0 - fi - if git rev-parse --verify -q master >/dev/null 2>&1; then - echo master - return 0 - fi - return 1 +# Prints master's sha. Vercel clones shallow, so fetch it if missing; the +# timeout keeps a hung network call from holding the build machine. +upstream() { + has_rev origin/master || + timeout 15 git fetch -q --no-tags --depth=80 origin master:refs/remotes/origin/master 2>/dev/null + git rev-parse --verify -q origin/master || git rev-parse --verify -q master } prev="${VERCEL_GIT_PREVIOUS_SHA:-}" -if production_ref; then - if is_ancestor "$prev" HEAD; then - decide "$prev" HEAD "production changes since ${prev:0:12}" - elif git rev-parse --verify -q 'HEAD^' >/dev/null 2>&1; then - decide 'HEAD^' HEAD "production changes in $(git rev-parse --short HEAD)" - else - build "production commit has no parent" - fi +# Production deploys the branch's own history (squash merges are one commit). +if [[ "${VERCEL_GIT_COMMIT_REF:-}" =~ ^(master|rest-hooks-site)$ || "${VERCEL_ENV:-}" == production ]]; then + is_ancestor "$prev" HEAD && decide "$prev" HEAD "production changes since ${prev:0:12}" + has_rev 'HEAD^' && decide 'HEAD^' HEAD "production changes in $(git rev-parse --short HEAD)" + build "production commit has no parent" fi -# Merge commit on a preview branch: parents are (branch tip, upstream). -# Diff against the upstream parent so master's files are not "our" changes. -# If this branch already deployed and the branch tip has no new site files -# since then, merging upstream does not need another preview. -if git rev-parse --verify -q 'HEAD^2' >/dev/null 2>&1; then - if is_ancestor "$prev" 'HEAD^1'; then - decide "$prev" 'HEAD^1' "preview changes since ${prev:0:12} (merge)" - else - decide 'HEAD^2' HEAD "preview changes vs upstream" - fi +# Previews compare the branch's changes, not commits merged in from upstream. +# A merge commit's parents are (branch tip, upstream): if the branch tip has +# no new site files since the last preview, merging upstream needs no rebuild; +# otherwise diff against the upstream parent so master's files don't count. +if has_rev 'HEAD^2'; then + is_ancestor "$prev" 'HEAD^1' && decide "$prev" 'HEAD^1' "preview changes since ${prev:0:12} (merge)" + decide 'HEAD^2' HEAD "preview changes vs upstream" fi -if is_ancestor "$prev" HEAD; then - decide "$prev" HEAD "preview changes since ${prev:0:12}" -fi +is_ancestor "$prev" HEAD && decide "$prev" HEAD "preview changes since ${prev:0:12}" -if master="$(ensure_master)"; then - if base="$(git merge-base HEAD "$master" 2>/dev/null)" && [ -n "$base" ]; then - decide "$base" HEAD "preview changes vs ${master}" - fi +if master="$(upstream)" && base="$(git merge-base HEAD "$master" 2>/dev/null)"; then + decide "$base" HEAD "preview changes vs master" fi -if git rev-parse --verify -q 'HEAD^' >/dev/null 2>&1; then - decide 'HEAD^' HEAD "preview changes in $(git rev-parse --short HEAD)" -fi +has_rev 'HEAD^' && decide 'HEAD^' HEAD "preview changes in $(git rev-parse --short HEAD)" build "no base to compare" diff --git a/website/scripts/vercel-ignore.test.sh b/website/scripts/vercel-ignore.test.sh index 4e1f69cb7900..79040e6f5ad3 100755 --- a/website/scripts/vercel-ignore.test.sh +++ b/website/scripts/vercel-ignore.test.sh @@ -26,43 +26,20 @@ commit() { git -C "$repo" commit -m "$msg" >/dev/null } -run_ignore() { - local ref="$1" - local prev="${2-}" - ( - cd "$repo" - VERCEL_GIT_COMMIT_REF="$ref" \ - VERCEL_GIT_PREVIOUS_SHA="$prev" \ - VERCEL_ENV="${3:-}" \ - bash "$script" - ) -} - -expect_skip() { - local name="$1" - shift - local out rc +# expect [previous-sha] [vercel-env] +expect() { + local want="$1" name="$2" ref="$3" out rc code=1 + [ "$want" = skip ] && code=0 set +e - out="$(run_ignore "$@" 2>&1)" + out="$( + cd "$repo" && + VERCEL_GIT_COMMIT_REF="$ref" VERCEL_GIT_PREVIOUS_SHA="${4-}" VERCEL_ENV="${5-}" \ + bash "$script" 2>&1 + )" rc=$? set -e - if [ "$rc" -ne 0 ]; then - printf 'FAIL %s: expected skip (0), got %s\n%s\n' "$name" "$rc" "$out" >&2 - exit 1 - fi - printf 'ok %s\n' "$name" -} - -expect_build() { - local name="$1" - shift - local out rc - set +e - out="$(run_ignore "$@" 2>&1)" - rc=$? - set -e - if [ "$rc" -ne 1 ]; then - printf 'FAIL %s: expected build (1), got %s\n%s\n' "$name" "$rc" "$out" >&2 + if [ "$rc" -ne "$code" ]; then + printf 'FAIL %s: expected %s (%s), got %s\n%s\n' "$name" "$want" "$code" "$rc" "$out" >&2 exit 1 fi printf 'ok %s\n' "$name" @@ -72,102 +49,100 @@ commit "init" README.md # --- production (master): one squash commit --- commit "pkg" packages/core/src/index.ts -expect_skip "master package-only" master +expect skip "master package-only" master commit "docs page" docs/core/api/Controller.md -expect_build "master docs/core" master +expect build "master docs/core" master commit "roadmap" docs/ROADMAP.md -expect_skip "master docs/ROADMAP.md" master +expect skip "master docs/ROADMAP.md" master commit "prettier" docs/.prettierrc -expect_skip "master docs/.prettierrc" master +expect skip "master docs/.prettierrc" master commit "blog" website/blog/2026-10-04-note.md -expect_build "master website blog" master +expect build "master website blog" master commit "changelog" website/CHANGELOG.md -expect_skip "master website changelog" master +expect skip "master website changelog" master -commit "unit test" website/src/components/Playground/__tests__/transformCode.test.ts -expect_skip "master website unit test" master +commit "unit test" website/src/components/Playground/__tests__/transformCode.test.ts website/src/components/Playground/__tests__/fixture.json +expect skip "master website unit test" master commit "colocated test" website/src/components/Playground/transformCode.test.ts website/scripts/vercel-ignore.test.sh -expect_skip "master colocated website tests" master +expect skip "master colocated website tests" master commit "ci" .circleci/config.yml .github/workflows/benchmark.yml -expect_skip "master CI-only" master +expect skip "master CI-only" master # Last successful production deploy was before a package commit. Still skip. pkg_sha="$(git -C "$repo" rev-parse HEAD)" commit "another pkg" packages/rest/src/index.ts -expect_skip "master package since previous deploy" master "$pkg_sha" +expect skip "master package since previous deploy" master "$pkg_sha" # --- preview branch, linear --- git -C "$repo" checkout -b feature >/dev/null 2>&1 commit "feature pkg" packages/vue/src/index.ts .changeset/preview.md -expect_skip "preview package and changeset" feature +expect skip "preview package and changeset" feature commit "feature roadmap" docs/ROADMAP.md -expect_skip "preview docs that are not published" feature +expect skip "preview docs that are not published" feature commit "feature page" docs/rest/api/Entity.md -expect_build "preview docs/rest" feature +expect build "preview docs/rest" feature + +commit "graphql page" docs/graphql/api/GQLEndpoint.md +expect build "preview docs/graphql since last deploy" feature "$(git -C "$repo" rev-parse HEAD^)" commit "feature ci follow-up" .github/workflows/site-preview.yml # Whole branch still contains docs/rest, and there is no previous deploy, # so the site change must still build. -expect_build "preview follow-up after unpublished site change" feature +expect build "preview follow-up after unpublished site change" feature page_sha="$(git -C "$repo" rev-parse HEAD)" commit "feature pkg follow-up" packages/core/src/other.ts -expect_skip "preview package follow-up after site deploy" feature "$page_sha" +expect skip "preview package follow-up after site deploy" feature "$page_sha" commit "playground source" website/src/components/Playground/transformCode.ts -expect_build "preview playground source since last deploy" feature "$page_sha" +expect build "preview playground source since last deploy" feature "$page_sha" # Multi-commit branch with no prior deploy: the site edit is not the tip. -git -C "$repo" checkout master >/dev/null 2>&1 -git -C "$repo" checkout -b stacked >/dev/null 2>&1 +git -C "$repo" checkout -b stacked master >/dev/null 2>&1 commit "stacked site" website/src/pages/index.js commit "stacked pkg" packages/endpoint/src/index.ts -expect_build "preview multi-commit site change not at tip" stacked +expect build "preview multi-commit site change not at tip" stacked # --- merge master into a package PR (the credit-burn case) --- git -C "$repo" checkout master >/dev/null 2>&1 commit "master site moves" website/src/pages/index.js docs/core/concepts/overview.md master_sha="$(git -C "$repo" rev-parse HEAD)" -git -C "$repo" checkout -b pkg-pr >/dev/null 2>&1 -# Branch point is the commit BEFORE "master site moves" only if we reset. -# Recreate the PR from the parent of that master commit so master is ahead. -git -C "$repo" reset --hard HEAD^ >/dev/null +# Branch from before that commit, so master is ahead when the PR merges it. +git -C "$repo" checkout -b pkg-pr "$master_sha^" >/dev/null 2>&1 commit "pr packages" packages/core/src/set.ts .circleci/config.yml git -C "$repo" merge --no-edit "$master_sha" >/dev/null -expect_skip "preview merge of master into package PR" pkg-pr +expect skip "preview merge of master into package PR" pkg-pr # Site PR already deployed, then merges a master that also changed the site. -git -C "$repo" checkout master >/dev/null 2>&1 -git -C "$repo" checkout -b site-pr >/dev/null 2>&1 -git -C "$repo" reset --hard HEAD^ >/dev/null +git -C "$repo" checkout -b site-pr "$master_sha^" >/dev/null 2>&1 commit "pr website" website/docusaurus.config.ts deployed="$(git -C "$repo" rev-parse HEAD)" git -C "$repo" merge --no-edit "$master_sha" >/dev/null -expect_skip "preview merge after the site commit already deployed" site-pr "$deployed" +expect skip "preview merge after the site commit already deployed" site-pr "$deployed" # Site PR that has never deployed, merged with master: still build. git -C "$repo" checkout -B site-pr-fresh "$deployed" >/dev/null 2>&1 git -C "$repo" merge --no-edit "$master_sha" >/dev/null -expect_build "preview merge of a site PR with no prior deploy" site-pr-fresh +expect build "preview merge of a site PR with no prior deploy" site-pr-fresh # gh-pages branches never build, even if website files differ. -expect_skip "gh-pages branch" gh-pages-bench +expect skip "gh-pages branch" gh-pages-bench # VERCEL_ENV=production uses this branch's history, not the diff against master. # A site commit still builds; a later package-only commit does not. git -C "$repo" checkout master >/dev/null 2>&1 -expect_build "production env site tip" other-branch "" production +expect build "production env site tip" other-branch "" production commit "prod pkg" packages/normalizr/src/index.ts -expect_skip "production env package tip" other-branch "" production +expect skip "production env package tip" other-branch "" production echo "all vercel-ignore cases passed" From d42025a4dcfeee1229e3e348b2bb6e226665b559 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 09:13:18 +0000 Subject: [PATCH 3/7] internal(ci): Build previews when no merge-base is available The HEAD^ fallback only checked the tip commit, so a site edit earlier on the branch could skip its first preview when shallow history hid the merge-base with master. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BamoUG3kFNUhtM7qYaGBgk --- .cursor/rules/ci-config.mdc | 2 +- website/scripts/vercel-ignore.sh | 4 ++-- website/scripts/vercel-ignore.test.sh | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index bcdd73da779a..009c2d50a9a4 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -26,5 +26,5 @@ alwaysApply: false ## Vercel docs site (`website/scripts/vercel-ignore.sh`) - `website/vercel.json` `ignoreCommand` runs it: exit 0 skips, exit 1 builds. It skips only when it can see the published site is unchanged, so any git failure builds. `SITE_PATHS` matches the workflow `paths` minus unpublished files (`website/CHANGELOG.md`, `website/**/__tests__/**`, `website/**/*.test.*`); `gh-pages*` branches always skip. `website/scripts/vercel-ignore.test.sh` covers each rule. -- Previews diff the PR's own changes (since the last preview, against a merge commit's upstream parent, or from the merge-base with `master`). Never `git diff HEAD^ HEAD` there: after merging `master` into a package PR that diff is master's site changes. Production (`master`, `rest-hooks-site`, `VERCEL_ENV=production`) diffs since the last deploy, else `HEAD^`. +- Previews diff the PR's own changes (since the last preview, against a merge commit's upstream parent, or from the merge-base with `master`; with none, build). Never `git diff HEAD^ HEAD` there: after merging `master` into a package PR that diff is master's site changes. Production (`master`, `rest-hooks-site`, `VERCEL_ENV=production`) diffs since the last deploy, else `HEAD^`. - A skipped build still creates a canceled Vercel deployment; `git.deploymentEnabled: false` does not stick while the project dashboard overrides it. diff --git a/website/scripts/vercel-ignore.sh b/website/scripts/vercel-ignore.sh index 99329d69b8f6..2007e49021ae 100755 --- a/website/scripts/vercel-ignore.sh +++ b/website/scripts/vercel-ignore.sh @@ -83,6 +83,6 @@ if master="$(upstream)" && base="$(git merge-base HEAD "$master" 2>/dev/null)"; decide "$base" HEAD "preview changes vs master" fi -has_rev 'HEAD^' && decide 'HEAD^' HEAD "preview changes in $(git rev-parse --short HEAD)" - +# Without a base, the tip commit alone can't prove earlier commits left the +# site unchanged. build "no base to compare" diff --git a/website/scripts/vercel-ignore.test.sh b/website/scripts/vercel-ignore.test.sh index 79040e6f5ad3..a4fa9ce2ce93 100755 --- a/website/scripts/vercel-ignore.test.sh +++ b/website/scripts/vercel-ignore.test.sh @@ -135,6 +135,13 @@ git -C "$repo" checkout -B site-pr-fresh "$deployed" >/dev/null 2>&1 git -C "$repo" merge --no-edit "$master_sha" >/dev/null expect build "preview merge of a site PR with no prior deploy" site-pr-fresh +# No merge-base with master (e.g. shallow history): the tip alone can't prove +# the site is unchanged, so build. +git -C "$repo" checkout --orphan unrelated >/dev/null 2>&1 +commit "unrelated site" website/src/pages/index.js +commit "unrelated pkg" packages/core/src/index.ts +expect build "preview without merge-base builds" unrelated + # gh-pages branches never build, even if website files differ. expect skip "gh-pages branch" gh-pages-bench From c6afab076c7f546631314a68b4ae7dc7ba7d6480 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 11:20:33 +0000 Subject: [PATCH 4/7] internal(ci): Count merged feature branches and fail open on production A merge commit is only treated as "merging master" when its second parent is on master, so merging a stacked branch with site changes still builds. Production no longer falls back to HEAD^, which could miss a site change earlier in a multi-commit push; it deepens the shallow clone to reach the last deploy and builds when there is none. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BamoUG3kFNUhtM7qYaGBgk --- .cursor/rules/ci-config.mdc | 2 +- website/scripts/vercel-ignore.sh | 16 +++++----- website/scripts/vercel-ignore.test.sh | 42 +++++++++++++++++++-------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 009c2d50a9a4..373a774191d8 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -26,5 +26,5 @@ alwaysApply: false ## Vercel docs site (`website/scripts/vercel-ignore.sh`) - `website/vercel.json` `ignoreCommand` runs it: exit 0 skips, exit 1 builds. It skips only when it can see the published site is unchanged, so any git failure builds. `SITE_PATHS` matches the workflow `paths` minus unpublished files (`website/CHANGELOG.md`, `website/**/__tests__/**`, `website/**/*.test.*`); `gh-pages*` branches always skip. `website/scripts/vercel-ignore.test.sh` covers each rule. -- Previews diff the PR's own changes (since the last preview, against a merge commit's upstream parent, or from the merge-base with `master`; with none, build). Never `git diff HEAD^ HEAD` there: after merging `master` into a package PR that diff is master's site changes. Production (`master`, `rest-hooks-site`, `VERCEL_ENV=production`) diffs since the last deploy, else `HEAD^`. +- Previews diff the PR's own changes (since the last preview, against `master` when the tip merges it, or from the merge-base with `master`; with none, build). Merges of other branches count in full. Never `git diff HEAD^ HEAD` there: after merging `master` into a package PR that diff is master's site changes. Production (`master`, `rest-hooks-site`, `VERCEL_ENV=production`) diffs since the last deploy (deepening the shallow clone to reach it), and builds when there is none. A push can carry several commits, so never diff only `HEAD^`. - A skipped build still creates a canceled Vercel deployment; `git.deploymentEnabled: false` does not stick while the project dashboard overrides it. diff --git a/website/scripts/vercel-ignore.sh b/website/scripts/vercel-ignore.sh index 2007e49021ae..292e3da1e238 100755 --- a/website/scripts/vercel-ignore.sh +++ b/website/scripts/vercel-ignore.sh @@ -61,18 +61,20 @@ upstream() { prev="${VERCEL_GIT_PREVIOUS_SHA:-}" -# Production deploys the branch's own history (squash merges are one commit). +# A push can carry several commits (rebase merges), so compare against the +# last deploy, deepening the shallow clone if it's out of reach. if [[ "${VERCEL_GIT_COMMIT_REF:-}" =~ ^(master|rest-hooks-site)$ || "${VERCEL_ENV:-}" == production ]]; then + [ -n "$prev" ] && ! has_rev "$prev^{commit}" && [ -n "${VERCEL_GIT_COMMIT_REF:-}" ] && + timeout 15 git fetch -q --no-tags --deepen=200 origin "$VERCEL_GIT_COMMIT_REF" 2>/dev/null is_ancestor "$prev" HEAD && decide "$prev" HEAD "production changes since ${prev:0:12}" - has_rev 'HEAD^' && decide 'HEAD^' HEAD "production changes in $(git rev-parse --short HEAD)" - build "production commit has no parent" + build "no previous production deploy to compare" fi # Previews compare the branch's changes, not commits merged in from upstream. -# A merge commit's parents are (branch tip, upstream): if the branch tip has -# no new site files since the last preview, merging upstream needs no rebuild; -# otherwise diff against the upstream parent so master's files don't count. -if has_rev 'HEAD^2'; then +# Merging master: if the branch tip has no new site files since the last +# preview, no rebuild; otherwise diff against the merged master commit so its files +# don't count. Merges of other branches fall through and count in full. +if has_rev 'HEAD^2' && master="$(upstream)" && is_ancestor 'HEAD^2' "$master"; then is_ancestor "$prev" 'HEAD^1' && decide "$prev" 'HEAD^1' "preview changes since ${prev:0:12} (merge)" decide 'HEAD^2' HEAD "preview changes vs upstream" fi diff --git a/website/scripts/vercel-ignore.test.sh b/website/scripts/vercel-ignore.test.sh index a4fa9ce2ce93..0a188366575e 100755 --- a/website/scripts/vercel-ignore.test.sh +++ b/website/scripts/vercel-ignore.test.sh @@ -45,35 +45,38 @@ expect() { printf 'ok %s\n' "$name" } +# Previous deploy for a single-commit push. +parent() { git -C "$repo" rev-parse HEAD^; } + commit "init" README.md # --- production (master): one squash commit --- commit "pkg" packages/core/src/index.ts -expect skip "master package-only" master +expect skip "master package-only" master "$(parent)" commit "docs page" docs/core/api/Controller.md -expect build "master docs/core" master +expect build "master docs/core" master "$(parent)" commit "roadmap" docs/ROADMAP.md -expect skip "master docs/ROADMAP.md" master +expect skip "master docs/ROADMAP.md" master "$(parent)" commit "prettier" docs/.prettierrc -expect skip "master docs/.prettierrc" master +expect skip "master docs/.prettierrc" master "$(parent)" commit "blog" website/blog/2026-10-04-note.md -expect build "master website blog" master +expect build "master website blog" master "$(parent)" commit "changelog" website/CHANGELOG.md -expect skip "master website changelog" master +expect skip "master website changelog" master "$(parent)" commit "unit test" website/src/components/Playground/__tests__/transformCode.test.ts website/src/components/Playground/__tests__/fixture.json -expect skip "master website unit test" master +expect skip "master website unit test" master "$(parent)" commit "colocated test" website/src/components/Playground/transformCode.test.ts website/scripts/vercel-ignore.test.sh -expect skip "master colocated website tests" master +expect skip "master colocated website tests" master "$(parent)" commit "ci" .circleci/config.yml .github/workflows/benchmark.yml -expect skip "master CI-only" master +expect skip "master CI-only" master "$(parent)" # Last successful production deploy was before a package commit. Still skip. pkg_sha="$(git -C "$repo" rev-parse HEAD)" @@ -135,6 +138,14 @@ git -C "$repo" checkout -B site-pr-fresh "$deployed" >/dev/null 2>&1 git -C "$repo" merge --no-edit "$master_sha" >/dev/null expect build "preview merge of a site PR with no prior deploy" site-pr-fresh +# Merging another feature branch (not upstream) brings its site changes in. +git -C "$repo" checkout -b feat-docs "$deployed" >/dev/null 2>&1 +commit "stacked docs" docs/core/api/Stacked.md +git -C "$repo" checkout -b feat "$deployed" >/dev/null 2>&1 +commit "feat pkg" packages/core/src/feat.ts +git -C "$repo" merge --no-edit feat-docs >/dev/null +expect build "preview merge of a non-upstream branch with site changes" feat "$deployed" + # No merge-base with master (e.g. shallow history): the tip alone can't prove # the site is unchanged, so build. git -C "$repo" checkout --orphan unrelated >/dev/null 2>&1 @@ -145,11 +156,18 @@ expect build "preview without merge-base builds" unrelated # gh-pages branches never build, even if website files differ. expect skip "gh-pages branch" gh-pages-bench +# Last production deploy is outside the clone (shallow history): the tip +# alone can't prove earlier commits in the push left the site unchanged. +git -C "$repo" checkout master >/dev/null 2>&1 +commit "rebase-merged docs" docs/rest/api/Rebased.md +commit "rebase-merged pkg" packages/rest/src/rebased.ts +expect build "master with unreachable previous deploy" master 0123456789abcdef0123456789abcdef01234567 + # VERCEL_ENV=production uses this branch's history, not the diff against master. # A site commit still builds; a later package-only commit does not. -git -C "$repo" checkout master >/dev/null 2>&1 -expect build "production env site tip" other-branch "" production +commit "prod site" website/src/pages/index.js +expect build "production env site tip" other-branch "$(parent)" production commit "prod pkg" packages/normalizr/src/index.ts -expect skip "production env package tip" other-branch "" production +expect skip "production env package tip" other-branch "$(parent)" production echo "all vercel-ignore cases passed" From f195e8a81207846250a1e8993a1c6ee374f7ab19 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 11:29:22 +0000 Subject: [PATCH 5/7] internal(ci): Count merge resolutions and deepen for long PRs Merging master into a PR now always diffs against the merged master commit, so a site conflict resolution in the merge rebuilds the preview. When the merge-base with master is outside Vercel's shallow clone, the script deepens the branch and master instead of building every push of a long package-only PR. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BamoUG3kFNUhtM7qYaGBgk --- .cursor/rules/ci-config.mdc | 2 +- website/scripts/vercel-ignore.sh | 27 ++++++++++++++++++--------- website/scripts/vercel-ignore.test.sh | 14 ++++++++++++-- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 373a774191d8..6e89d4b8823a 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -26,5 +26,5 @@ alwaysApply: false ## Vercel docs site (`website/scripts/vercel-ignore.sh`) - `website/vercel.json` `ignoreCommand` runs it: exit 0 skips, exit 1 builds. It skips only when it can see the published site is unchanged, so any git failure builds. `SITE_PATHS` matches the workflow `paths` minus unpublished files (`website/CHANGELOG.md`, `website/**/__tests__/**`, `website/**/*.test.*`); `gh-pages*` branches always skip. `website/scripts/vercel-ignore.test.sh` covers each rule. -- Previews diff the PR's own changes (since the last preview, against `master` when the tip merges it, or from the merge-base with `master`; with none, build). Merges of other branches count in full. Never `git diff HEAD^ HEAD` there: after merging `master` into a package PR that diff is master's site changes. Production (`master`, `rest-hooks-site`, `VERCEL_ENV=production`) diffs since the last deploy (deepening the shallow clone to reach it), and builds when there is none. A push can carry several commits, so never diff only `HEAD^`. +- Previews diff the PR's own changes (since the last preview, against the merged commit when the tip merges `master`, so conflict resolutions count, or from the merge-base with `master`; with none, build). Merges of other branches count in full. When a base is outside Vercel's ~10-commit clone, the script deepens the branch and `master` before giving up. Never `git diff HEAD^ HEAD` there: after merging `master` into a package PR that diff is master's site changes. Production (`master`, `rest-hooks-site`, `VERCEL_ENV=production`) diffs since the last deploy and builds when there is none. A push can carry several commits, so never diff only `HEAD^`. - A skipped build still creates a canceled Vercel deployment; `git.deploymentEnabled: false` does not stick while the project dashboard overrides it. diff --git a/website/scripts/vercel-ignore.sh b/website/scripts/vercel-ignore.sh index 292e3da1e238..65a5298f07f9 100755 --- a/website/scripts/vercel-ignore.sh +++ b/website/scripts/vercel-ignore.sh @@ -59,29 +59,38 @@ upstream() { git rev-parse --verify -q origin/master || git rev-parse --verify -q master } +# Vercel clones about 10 commits deep. Fetch more of this branch and master +# when a comparison base is out of reach. +deepen() { + [ -n "${VERCEL_GIT_COMMIT_REF:-}" ] && + timeout 30 git fetch -q --no-tags --deepen=300 origin "$VERCEL_GIT_COMMIT_REF" \ + '+refs/heads/master:refs/remotes/origin/master' 2>/dev/null +} + prev="${VERCEL_GIT_PREVIOUS_SHA:-}" # A push can carry several commits (rebase merges), so compare against the -# last deploy, deepening the shallow clone if it's out of reach. +# last deploy. if [[ "${VERCEL_GIT_COMMIT_REF:-}" =~ ^(master|rest-hooks-site)$ || "${VERCEL_ENV:-}" == production ]]; then - [ -n "$prev" ] && ! has_rev "$prev^{commit}" && [ -n "${VERCEL_GIT_COMMIT_REF:-}" ] && - timeout 15 git fetch -q --no-tags --deepen=200 origin "$VERCEL_GIT_COMMIT_REF" 2>/dev/null + [ -n "$prev" ] && ! has_rev "$prev^{commit}" && deepen is_ancestor "$prev" HEAD && decide "$prev" HEAD "production changes since ${prev:0:12}" build "no previous production deploy to compare" fi # Previews compare the branch's changes, not commits merged in from upstream. -# Merging master: if the branch tip has no new site files since the last -# preview, no rebuild; otherwise diff against the merged master commit so its files -# don't count. Merges of other branches fall through and count in full. +# When the tip merges master, diff against the merged master commit: that +# counts the branch's site files and any conflict resolutions, not master's. +# Merges of other branches fall through and count in full. if has_rev 'HEAD^2' && master="$(upstream)" && is_ancestor 'HEAD^2' "$master"; then - is_ancestor "$prev" 'HEAD^1' && decide "$prev" 'HEAD^1' "preview changes since ${prev:0:12} (merge)" - decide 'HEAD^2' HEAD "preview changes vs upstream" + decide 'HEAD^2' HEAD "preview changes vs master (merge)" fi is_ancestor "$prev" HEAD && decide "$prev" HEAD "preview changes since ${prev:0:12}" -if master="$(upstream)" && base="$(git merge-base HEAD "$master" 2>/dev/null)"; then +merge_base() { + master="$(upstream)" && git merge-base HEAD "$master" 2>/dev/null +} +if base="$(merge_base)" || { deepen && base="$(merge_base)"; }; then decide "$base" HEAD "preview changes vs master" fi diff --git a/website/scripts/vercel-ignore.test.sh b/website/scripts/vercel-ignore.test.sh index 0a188366575e..253880b66554 100755 --- a/website/scripts/vercel-ignore.test.sh +++ b/website/scripts/vercel-ignore.test.sh @@ -126,12 +126,22 @@ commit "pr packages" packages/core/src/set.ts .circleci/config.yml git -C "$repo" merge --no-edit "$master_sha" >/dev/null expect skip "preview merge of master into package PR" pkg-pr -# Site PR already deployed, then merges a master that also changed the site. +# Site PR already deployed, then merges master: rebuild so the preview +# reflects the merged result. git -C "$repo" checkout -b site-pr "$master_sha^" >/dev/null 2>&1 commit "pr website" website/docusaurus.config.ts deployed="$(git -C "$repo" rev-parse HEAD)" git -C "$repo" merge --no-edit "$master_sha" >/dev/null -expect skip "preview merge after the site commit already deployed" site-pr "$deployed" +expect build "preview merge of master into a deployed site PR" site-pr "$deployed" + +# A conflict resolution in the merge commit changes the site itself. +git -C "$repo" checkout -b conflict-pr "$master_sha^" >/dev/null 2>&1 +commit "pr index" website/src/pages/index.js +conflict_deployed="$(git -C "$repo" rev-parse HEAD)" +git -C "$repo" merge --no-edit "$master_sha" >/dev/null 2>&1 || true +printf 'resolved\n' >"$repo/website/src/pages/index.js" +git -C "$repo" commit -qam "merge master" >/dev/null +expect build "preview merge with a site conflict resolution" conflict-pr "$conflict_deployed" # Site PR that has never deployed, merged with master: still build. git -C "$repo" checkout -B site-pr-fresh "$deployed" >/dev/null 2>&1 From 52f32e36c6e59c77c98745af7bba056bfdfe36a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 14:06:59 +0000 Subject: [PATCH 6/7] internal(ci): Tidy vercel-ignore helpers and rule Group merge_base with the other helpers and condense the cursor rule's diff-base guidance into one bullet. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BamoUG3kFNUhtM7qYaGBgk --- .cursor/rules/ci-config.mdc | 2 +- website/scripts/vercel-ignore.sh | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 6e89d4b8823a..4c3739212056 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -26,5 +26,5 @@ alwaysApply: false ## Vercel docs site (`website/scripts/vercel-ignore.sh`) - `website/vercel.json` `ignoreCommand` runs it: exit 0 skips, exit 1 builds. It skips only when it can see the published site is unchanged, so any git failure builds. `SITE_PATHS` matches the workflow `paths` minus unpublished files (`website/CHANGELOG.md`, `website/**/__tests__/**`, `website/**/*.test.*`); `gh-pages*` branches always skip. `website/scripts/vercel-ignore.test.sh` covers each rule. -- Previews diff the PR's own changes (since the last preview, against the merged commit when the tip merges `master`, so conflict resolutions count, or from the merge-base with `master`; with none, build). Merges of other branches count in full. When a base is outside Vercel's ~10-commit clone, the script deepens the branch and `master` before giving up. Never `git diff HEAD^ HEAD` there: after merging `master` into a package PR that diff is master's site changes. Production (`master`, `rest-hooks-site`, `VERCEL_ENV=production`) diffs since the last deploy and builds when there is none. A push can carry several commits, so never diff only `HEAD^`. +- Never judge a push by `git diff HEAD^ HEAD`: after merging `master` into a package PR it shows master's site changes, and a push can carry several commits. Previews diff the PR's own changes (since the last preview, against the merged commit when the tip merges `master`, or from the merge-base with `master`); production diffs since the last deploy. When a base is outside Vercel's ~10-commit clone the script deepens the branch and `master`, and builds if it still has none. - A skipped build still creates a canceled Vercel deployment; `git.deploymentEnabled: false` does not stick while the project dashboard overrides it. diff --git a/website/scripts/vercel-ignore.sh b/website/scripts/vercel-ignore.sh index 65a5298f07f9..09a0449af9d5 100755 --- a/website/scripts/vercel-ignore.sh +++ b/website/scripts/vercel-ignore.sh @@ -67,6 +67,10 @@ deepen() { '+refs/heads/master:refs/remotes/origin/master' 2>/dev/null } +merge_base() { + master="$(upstream)" && git merge-base HEAD "$master" 2>/dev/null +} + prev="${VERCEL_GIT_PREVIOUS_SHA:-}" # A push can carry several commits (rebase merges), so compare against the @@ -87,9 +91,6 @@ fi is_ancestor "$prev" HEAD && decide "$prev" HEAD "preview changes since ${prev:0:12}" -merge_base() { - master="$(upstream)" && git merge-base HEAD "$master" 2>/dev/null -} if base="$(merge_base)" || { deepen && base="$(merge_base)"; }; then decide "$base" HEAD "preview changes vs master" fi From 56fec30589538694a985aad9f2e97b860a14eb5b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 14:07:22 +0000 Subject: [PATCH 7/7] internal(ci): Trim ci-config rule for the Vercel ignore step Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01BamoUG3kFNUhtM7qYaGBgk --- .cursor/rules/ci-config.mdc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.cursor/rules/ci-config.mdc b/.cursor/rules/ci-config.mdc index 4c3739212056..490a6482dd4e 100644 --- a/.cursor/rules/ci-config.mdc +++ b/.cursor/rules/ci-config.mdc @@ -19,12 +19,11 @@ alwaysApply: false ## GitHub Actions (`.github/workflows/`) - Workflows install only needed workspaces via `./scripts/ci-install.sh [extra-workspace ...]`. -- Docs site builds (`site-preview.yml`, `site-release.yml`) run only for the published site: `website/**`, `docs/core/**`, `docs/rest/**`, `docs/graphql/**`, and the workflow file itself. Keep that list in sync with `website/scripts/vercel-ignore.sh`. Package, example, CI, changeset, and other docs changes (including `docs/ROADMAP.md`) do not start those workflows. `site-preview` builds on the runner and does not deploy; the Vercel Git integration publishes the preview. +- `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:`. ## Vercel docs site (`website/scripts/vercel-ignore.sh`) -- `website/vercel.json` `ignoreCommand` runs it: exit 0 skips, exit 1 builds. It skips only when it can see the published site is unchanged, so any git failure builds. `SITE_PATHS` matches the workflow `paths` minus unpublished files (`website/CHANGELOG.md`, `website/**/__tests__/**`, `website/**/*.test.*`); `gh-pages*` branches always skip. `website/scripts/vercel-ignore.test.sh` covers each rule. -- Never judge a push by `git diff HEAD^ HEAD`: after merging `master` into a package PR it shows master's site changes, and a push can carry several commits. Previews diff the PR's own changes (since the last preview, against the merged commit when the tip merges `master`, or from the merge-base with `master`); production diffs since the last deploy. When a base is outside Vercel's ~10-commit clone the script deepens the branch and `master`, and builds if it still has none. -- A skipped build still creates a canceled Vercel deployment; `git.deploymentEnabled: false` does not stick while the project dashboard overrides it. +- Vercel's ignore step: exit 0 skips, anything else builds, so fail open. Never diff only `HEAD^`: a master merge or multi-commit push makes it wrong. Run `vercel-ignore.test.sh` after changes. +- `git.deploymentEnabled: false` doesn't stick (dashboard overrides it); skipped pushes still show as canceled deployments.