Skip to content

Commit 168941c

Browse files
authored
build(docs): stop rebuilding the docs site on every push to main (#12746)
Adds an ignoreCommand to apps/docs/vercel.json, backed by scripts/vercel-ignore-docs.sh: preview skips, content/ or apps/docs/ changes build, otherwise turbo decides from the dependency graph, and anything indeterminate builds. The content/** check is not redundant with the dependency-graph check: turbo computes affected packages by package directory and this repo's MDX lives at the repo root, so a graph check alone answers SKIP for a pure documentation edit (verified on commit 1265f12). Refs #12743, #12711, #12698
1 parent 3c0f3ea commit 168941c

4 files changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/docs": patch
3+
---
4+
5+
build(docs): stop rebuilding the docs site on every push to `main` (#12743)
6+
7+
Every push to `main` rebuilt the documentation site, and almost none of them
8+
changed what it renders. Measured over one week across the team:
9+
10+
| project | production builds | build-minutes | avg |
11+
|---|---|---|---|
12+
| **objectstack (docs)** | **228** | **2835 (98.6%)** | **12.4 min** |
13+
| objectui | 123 | 36 | 0.3 min |
14+
| hotcrm | 42 | 5 | 0.1 min |
15+
16+
The team runs `concurrentBuilds: 1`, so an 18-second `objectui` build queued
17+
behind a 12–46 minute docs build; the queue reached **92 deployments, the oldest
18+
34 hours old**. At 4 vCPU those docs builds cost roughly **$171/month** against a
19+
$20 included allowance, and 168 of the 228 were failures, so most of it bought
20+
nothing.
21+
22+
`apps/docs/vercel.json` now declares an `ignoreCommand` (which overrides the
23+
dashboard's Ignored Build Step, moving the rule into version control where it is
24+
reviewable and revertible). `scripts/vercel-ignore-docs.sh` decides:
25+
26+
1. non-production → skip (unchanged from the rule it replaces)
27+
2. `content/**` or `apps/docs/**` changed → build
28+
3. otherwise → ask turbo whether the docs dependency graph is affected
29+
4. anything indeterminate → **build**
30+
31+
**Step 2 is not redundant with step 3, and dropping it would silently stop
32+
publishing documentation.** `turbo --filter=<pkg>...[range]` computes affected
33+
packages *by package directory*, and this repo's MDX lives at the repo root in
34+
`content/`, outside the `apps/docs` boundary. `turbo.json` does list
35+
`"$TURBO_ROOT$/content/**"` under `@objectstack/docs#build`'s `inputs`, but
36+
`inputs` only feeds the cache hash — it does not widen the affected-package
37+
calculation. Verified on `main`: commit `1265f12b` touches only
38+
`content/docs/api/client-sdk.mdx`, and a dependency-graph check alone answers
39+
SKIP for it.
40+
41+
The asymmetry in step 4 is the point. A wrong "build" costs a few build-minutes;
42+
a wrong "skip" leaves the site quietly stale with no error anywhere. So a missing
43+
`VERCEL_GIT_PREVIOUS_SHA`, a shallow clone that cannot reach it, an unparseable
44+
turbo verdict, and a non-0/1 exit from turbo all build.
45+
46+
Deliberately not `npx turbo-ignore`, which #12698 suggested: it is deprecated
47+
upstream ("Use `turbo query affected` instead") and derives its own comparison
48+
range, falling back to `[HEAD^]` when it cannot read Vercel's git environment —
49+
silently answering a different question than the one asked. The range is named
50+
explicitly here instead.
51+
52+
`scripts/vercel-ignore-docs.selftest.sh` pins all six cases against real commits
53+
from this repo's history, including the `content/**` one.

apps/docs/vercel.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"framework": "nextjs",
44
"installCommand": "cd ../.. && pnpm install",
55
"buildCommand": "cd ../.. && pnpm turbo run build --filter=@objectstack/docs",
6+
"ignoreCommand": "bash ../../scripts/vercel-ignore-docs.sh",
67
"github": {
78
"silent": true
89
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Self-test for scripts/vercel-ignore-docs.sh.
4+
#
5+
# The script decides whether a docs production build runs. Its two failure
6+
# directions are not symmetric: a wrong "build" wastes build-minutes, a wrong
7+
# "skip" silently stops publishing documentation. These cases pin both, and in
8+
# particular pin the `content/**` case (#12743) — the repo's MDX lives outside
9+
# the `apps/docs` package boundary, so a dependency-graph check alone reports
10+
# SKIP for a pure documentation edit.
11+
#
12+
# Run: bash scripts/vercel-ignore-docs.selftest.sh
13+
set -uo pipefail
14+
cd "$(git rev-parse --show-toplevel)" || exit 1
15+
16+
SCRIPT=scripts/vercel-ignore-docs.sh
17+
fail=0
18+
19+
# Historical commits on `main`, each a single well-understood kind of change.
20+
# If history is ever rewritten these stop resolving; the test says so rather
21+
# than silently passing.
22+
SHA_CONTENT=1265f12b # touches only content/docs/api/client-sdk.mdx
23+
SHA_SPEC=366f8957 # touches packages/spec (the docs app's only workspace dep)
24+
SHA_CLAUDE=72f91652 # touches only .claude/**
25+
26+
check() {
27+
local label="$1" want="$2"; shift 2
28+
local out rc got
29+
out=$(env "$@" bash "$SCRIPT" 2>&1); rc=$?
30+
got=$([ "$rc" -eq 0 ] && echo skip || echo build)
31+
if [ "$got" = "$want" ]; then
32+
printf ' ok %-32s -> %s\n' "$label" "$got"
33+
else
34+
printf ' FAIL %-32s -> %s (wanted %s)\n %s\n' "$label" "$got" "$want" "$(echo "$out" | tail -1)"
35+
fail=1
36+
fi
37+
}
38+
39+
at() { # at <sha> -> echoes env assignments pinning the range to that commit
40+
local sha="$1" parent
41+
parent=$(git rev-parse "${sha}^" 2>/dev/null) || return 1
42+
echo "VERCEL_GIT_PREVIOUS_SHA=${parent} VERCEL_GIT_COMMIT_SHA=${sha}"
43+
}
44+
45+
echo "vercel-ignore-docs selftest"
46+
47+
check "preview deployments skip" skip VERCEL_ENV=preview
48+
check "no baseline builds" build VERCEL_ENV=production
49+
check "unreachable baseline builds" build VERCEL_ENV=production \
50+
VERCEL_GIT_PREVIOUS_SHA=0000000000000000000000000000000000000000
51+
52+
for pair in "content-only:$SHA_CONTENT:build" "spec-dependency:$SHA_SPEC:build" "claude-only:$SHA_CLAUDE:skip"; do
53+
label=${pair%%:*}; rest=${pair#*:}; sha=${rest%%:*}; want=${rest#*:}
54+
if ! git cat-file -e "${sha}^{commit}" 2>/dev/null; then
55+
printf ' SKIP %-32s (commit %s no longer in history)\n' "$label" "$sha"
56+
continue
57+
fi
58+
# shellcheck disable=SC2046
59+
check "$label" "$want" VERCEL_ENV=production $(at "$sha")
60+
done
61+
62+
if [ "$fail" -ne 0 ]; then echo "SELFTEST FAILED"; exit 1; fi
63+
echo "all cases passed"

scripts/vercel-ignore-docs.sh

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Vercel "Ignored Build Step" for @objectstack/docs.
4+
#
5+
# Vercel's contract is inverted from the usual one:
6+
# exit 0 -> IGNORE the build (skip)
7+
# exit 1 -> RUN the build
8+
#
9+
# Why this exists: every push to `main` used to rebuild the docs site. Measured
10+
# over one week, that was 228 production builds consuming 2835 build-minutes —
11+
# 98.6% of the whole team's build time — while `objectui` (18s/build) and
12+
# `hotcrm` (6s/build) queued behind them on a `concurrentBuilds: 1` team. The
13+
# queue reached 92 deployments, the oldest 34 hours old (#12743).
14+
#
15+
# ⚠️ The single most important property of this script is its FAILURE DIRECTION.
16+
# A wrong "build" costs a few build-minutes. A wrong "skip" silently stops
17+
# publishing documentation, with no error anywhere — the site just quietly goes
18+
# stale. Every indeterminate case below therefore exits 1.
19+
set -uo pipefail
20+
21+
# All paths below are repo-relative, but Vercel runs this from the project's
22+
# Root Directory (apps/docs), so anchor to the repo root first.
23+
cd "$(git rev-parse --show-toplevel)" || exit 1
24+
25+
# `VERCEL_GIT_COMMIT_SHA` is what Vercel is deploying; falling back to HEAD lets
26+
# this script be exercised locally and in tests.
27+
HEAD_SHA="${VERCEL_GIT_COMMIT_SHA:-HEAD}"
28+
PREV_SHA="${VERCEL_GIT_PREVIOUS_SHA:-}"
29+
30+
# 1. Preview deployments never build the docs site. This preserves exactly the
31+
# behaviour of the dashboard rule this replaces, whose preview half was
32+
# already correct; only its production half ("always build") was wasteful.
33+
if [ "${VERCEL_ENV:-}" != "production" ]; then
34+
echo "skip: VERCEL_ENV=${VERCEL_ENV:-unset} is not production"
35+
exit 0
36+
fi
37+
38+
# 2. No baseline to compare against -> build. Happens on the first deployment
39+
# after this lands, and any time Vercel cannot name a previous success.
40+
if [ -z "$PREV_SHA" ]; then
41+
echo "build: no VERCEL_GIT_PREVIOUS_SHA to compare against"
42+
exit 1
43+
fi
44+
45+
# 3. Vercel builds from a shallow clone, so the previous commit is frequently
46+
# absent. Try to fetch it; if it stays unreachable, build.
47+
if ! git cat-file -e "${PREV_SHA}^{commit}" 2>/dev/null; then
48+
git fetch --depth=100 origin "$PREV_SHA" >/dev/null 2>&1 || true
49+
fi
50+
if ! git cat-file -e "${PREV_SHA}^{commit}" 2>/dev/null; then
51+
echo "build: previous SHA ${PREV_SHA} is not reachable in this clone"
52+
exit 1
53+
fi
54+
55+
# 4. The site's own sources.
56+
#
57+
# This check is NOT redundant with turbo-ignore below, and removing it breaks
58+
# documentation publishing. `turbo --filter=<pkg>...[range]` computes affected
59+
# packages BY PACKAGE DIRECTORY. This repo's MDX lives at the repo root in
60+
# `content/`, outside the `apps/docs` package boundary, so turbo does not see
61+
# it. `turbo.json` does list `"$TURBO_ROOT$/content/**"` under
62+
# `@objectstack/docs#build`'s `inputs`, but `inputs` only feeds the cache
63+
# hash — it does not widen the affected-package calculation.
64+
#
65+
# Measured on `main`: commit 1265f12b touches only
66+
# `content/docs/api/client-sdk.mdx`, and `turbo-ignore` alone reports SKIP.
67+
if ! git diff --quiet "$PREV_SHA" "$HEAD_SHA" -- content apps/docs; then
68+
echo "build: content/ or apps/docs/ changed since ${PREV_SHA}"
69+
exit 1
70+
fi
71+
72+
# 5. Nothing in the site's own sources changed. Ask turbo whether the docs app
73+
# is affected through its dependency graph — `@objectstack/spec` is the only
74+
# workspace dependency, but it changes often.
75+
#
76+
# Deliberately NOT `turbo-ignore`: that wrapper is deprecated upstream ("Use
77+
# `turbo query affected` instead") and it derives its own comparison range,
78+
# falling back to `[HEAD^]` when it cannot read Vercel's git environment —
79+
# a range that silently answers a different question than the one asked here.
80+
# Naming the range explicitly keeps this decision reviewable and testable.
81+
echo "no direct docs changes; asking turbo about the dependency graph"
82+
DRY=$(npx --yes "turbo@${TURBO_VERSION:-^2}" run build \
83+
--filter="@objectstack/docs...[${PREV_SHA}...${HEAD_SHA}]" \
84+
--dry=json 2>/dev/null)
85+
if [ -z "$DRY" ]; then
86+
echo "build: could not get a verdict from turbo"
87+
exit 1
88+
fi
89+
90+
AFFECTED=$(printf '%s' "$DRY" | node -e '
91+
let s="";
92+
process.stdin.on("data", d => s += d);
93+
process.stdin.on("end", () => {
94+
try { process.stdout.write(String((JSON.parse(s).tasks || []).length)); }
95+
catch { process.stdout.write("error"); }
96+
});
97+
' 2>/dev/null)
98+
99+
case "$AFFECTED" in
100+
0) echo "skip: nothing in @objectstack/docs dependency graph changed since ${PREV_SHA}"; exit 0 ;;
101+
''|*[!0-9]*) echo "build: could not parse turbo's verdict (${AFFECTED:-empty})"; exit 1 ;;
102+
*) echo "build: ${AFFECTED} task(s) in the docs dependency graph affected"; exit 1 ;;
103+
esac

0 commit comments

Comments
 (0)