Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Commit 7e30d0b

Browse files
hotlongclaude
andauthored
ci(site): deploy the docs from a GitHub Actions workflow every 6 hours, and turn Vercel's Git-triggered builds off (objectui#12178) (#12182)
Refs: #12178 · stacked on #12179 ## Why Almost all of the maintainer's Vercel bill was "Build CPU Minutes" for the `objectui` project (Root Directory `apps/site`, served at objectui.org). Vercel built once for every commit pushed to `main`, and started a preview build for every other branch (`claude/*`, `gh-readonly-queue/*`, `changeset-release/*`, ...) that the dashboard's Ignored Build Step then cancelled. Every production build since 2026-09-06 failed (#12178: 2023 failed production builds, about 9,400 build-minutes, the site frozen). This repository is public, so `ubuntu-latest` minutes are free. The maintainer decided that the docs deploy **only from `main`**, on **free compute**, **every 6 hours**. ## What - **New `.github/workflows/docs-deploy.yml`** - Triggers: `schedule`, cron `43 5,11,17,23 * * *` (every 6 hours), and `workflow_dispatch`. Nothing else: no `push` trigger and no `paths` list. - Why that cron: `grep -rn 'cron:' .github/workflows/` shows hours 0/6/12/18, 1/7/13/19 and 2/8/14/20 each already carry another 6-hourly job. 5/11/17/23 meet only one daily patrol, at 05:23. Minute 43 is off the top of the hour and clear of the merge-queue patrol's :37 and :52. A comment in the file records this. - `concurrency: { group: docs-deploy-production, cancel-in-progress: true }`. A later run builds a tip that contains the earlier run's commits, so cancelling the earlier run loses nothing. - Steps, all run from the **repository root**: `vercel pull --yes --environment=production`, then `vercel build --prod`, then `vercel deploy --prebuilt --prod`, with the CLI pinned (`VERCEL_CLI_VERSION`, 60.1.3). The project's Root Directory is `apps/site`, so `vercel build` builds in `apps/site`, reads `apps/site/vercel.json` and writes `./.vercel/output`, and `vercel deploy --prebuilt` uploads that directory. I checked this against the pinned CLI's source: the build's work path is the cwd joined with the project's `rootDirectory`, and the prebuilt output dir is the cwd's `.vercel/output`. Running from inside `apps/site` would apply the Root Directory twice. - `VERCEL_ORG_ID` (`team_06nvlGKZCkwhzdXmRdxeCNnB`) and `VERCEL_PROJECT_ID` (`prj_v1rXIczaI7YGmXmseQCLFUFOFxTB`) are inlined. They are identifiers, not secrets. - `VERCEL_TOKEN` is passed only to the `pull` and `deploy` steps. `vercel build` needs no token (it is in the CLI's no-token command list), and it runs `pnpm i` plus the whole workspace build, which includes every dependency's install scripts, so it never sees the token. - **Which way it fails:** the first step runs before checkout. If `VERCEL_TOKEN` is missing, it stops with `::error title=VERCEL_TOKEN is missing::...` and `exit 1`. If the ref is not `refs/heads/main`, it stops with `::error title=Docs deploy runs from main only::...`. A failed build or a failed upload is a red step. **Nothing turns into a skipped green.** - **`apps/site/vercel.json`** gains `"git": { "deploymentEnabled": false }`. That is Vercel's documented switch for turning off Git-triggered deployments on every branch, and I validated the file against `https://openapi.vercel.sh/vercel.json`. Once this is merged, Vercel builds nothing on push, and this workflow is the only way to production. The `installCommand` (`pnpm i`) and the fixed `buildCommand` come from #12179. - **`content/docs/guide/ci-cd-pipeline.md`**: a `Docs Deploy (docs-deploy.yml)` section, a Workflow Inventory row and a `VERCEL_TOKEN` secrets row. **`scripts/__tests__/ci-cd-pipeline-doc.test.ts`**: one declared entry. The section names `turbo run build`, which the job really runs but through `vercel build` and `vercel.json`, not in a `run:` step, so the command-parity sweep cannot see it. ## Secret: already in place `VERCEL_TOKEN` exists as an **organization** secret of `objectstack-ai` (`gh api repos/objectstack-ai/objectui/actions/organization-secrets` lists it, updated 2026-10-12T03:10:52Z), and this repository can see it. `secrets.VERCEL_TOKEN` resolves org secrets the same way, so nothing has to be configured before merging. The dry run below used it. ## Order with #12179 This branch contains #12179's commit. #12179 creates `apps/site/vercel.json`, and this PR adds the `git` block to it. - If #12179 merges first (it can merge as soon as it is green), merge `origin/main` into this branch. The only conflict will be add/add on `apps/site/vercel.json`. Resolve it by taking **this branch's** version, which is #12179's file plus the `git` block. - Alternatively, merge this PR alone (it carries #12179's change). #12179 is then redundant and can be shut without merging. ## Evidence - **Build-only dry run of this exact workflow on GitHub Actions**: run 38200568488, success, 5m08s. It ran from a temporary commit that added a `pull_request` trigger and skipped the deploy step. That commit is reverted on this branch, and the final tree is byte-identical to the reviewed commit. In that run: `Vercel CLI 60.1.3 (Node.js 22.23.3)`, `vercel pull` downloaded the production settings into `./.vercel/project.json`, `vercel build` ran `Running "install" command: pnpm i` and `Running "cd ../.. && pnpm turbo run build --filter=@object-ui/site"`, turbo reported `Tasks: 30 successful, 30 total` (including `@object-ui/example-schema-catalog:build`, the package whose absence breaks the Vercel build in #12178), Next generated 559 of 559 static pages, then `Created all serverless functions`, then `Build Completed in .vercel/output`. `builds.json` shows `@vercel/next` with this file's `installCommand` and `buildCommand`. - **Not yet exercised: the `vercel deploy --prebuilt --prod` upload.** The first real run after merge is that test. - actionlint 1.7.12 reports nothing (shellcheck was not installed locally, so the `run:` scripts were not shell-linted). The YAML parses. A local run of the 88 `scripts/__tests__` files that read workflows or the CI/CD page passed, except one test whose timeout was not caused by this change: `check-i18n-dead-keys`, real-tree leg, 15 s, on a machine at load 4. CI is the judge. ## After merge 1. Publish once and verify: `gh workflow run docs-deploy.yml --repo objectstack-ai/objectui --ref main`, then watch the run. On green, the step summary prints the deployment URL, and objectui.org should show current content. #12179's build fix is part of this branch, so that run is expected to be green. If it is red, #12178 has the diagnosis of the build failure this replaces. The run is free either way. 2. From then on, the docs refresh **at most every 6 hours** (05:43, 11:43, 17:43 and 23:43 UTC). To publish immediately, use **Run workflow** on the workflow's Actions page (or the `gh workflow run` line above). 3. Shut #12178 by hand once a production deployment is READY. Neither PR carries a closing keyword. Side effects worth knowing: - The `Vercel` commit status stops appearing on PRs and commits once branches contain this `vercel.json`. `scripts/dependabot-merge-gate.mjs` does not read statuses, so nothing waits on it. - Branches created before this merge do not contain `git.deploymentEnabled: false`, so Vercel will still start a preview for them until they pick up `main`. The dashboard's Ignored Build Step still cancels those, so leave it in place for now. - The dashboard build and install commands are overridden by `vercel.json` and no longer matter. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 35a2b59 commit 7e30d0b

4 files changed

Lines changed: 184 additions & 0 deletions

File tree

‎.github/workflows/docs-deploy.yml‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
name: Docs Deploy
2+
3+
# Builds the docs site (`apps/site`, the Vercel project `objectui`, served at
4+
# objectui.org) on a GitHub-hosted runner and uploads the prebuilt output to
5+
# Vercel as a production deployment. Vercel itself builds nothing.
6+
#
7+
# ## Why the build moved here (objectui#12178)
8+
#
9+
# On the maintainer's Vercel bill almost all of the cost was "Build CPU
10+
# Minutes". Vercel built this project once for every commit pushed to `main`,
11+
# plus a preview build for every other branch. The preview builds were
12+
# cancelled by the Ignored Build Step, and every production build since
13+
# 2026-09-06 failed (objectui#12178), so all of that spend deployed nothing.
14+
# This repository is public, so `ubuntu-latest` minutes cost nothing. The
15+
# maintainer decided the site deploys only from `main`, on free compute.
16+
# `apps/site/vercel.json` turns Vercel's Git-triggered deployments off
17+
# (`git.deploymentEnabled: false`), so this workflow is now the ONLY way the
18+
# site gets deployed.
19+
#
20+
# ## When it runs: every 6 hours, or on demand
21+
#
22+
# There is no `push` trigger, on purpose. The maintainer chose a fixed clock:
23+
# at most 4 builds a day, no competition with pull-request CI for runners,
24+
# and no `paths` list to keep in step with the site's build closure. The docs
25+
# can therefore be up to 6 hours behind `main`. To publish now, use "Run
26+
# workflow" on this workflow's Actions page (or
27+
# `gh workflow run docs-deploy.yml --ref main`).
28+
#
29+
# The cron is `43 5,11,17,23 * * *`, chosen against
30+
# `grep -rn 'cron:' .github/workflows/`. Hours 0/6/12/18, 1/7/13/19 and
31+
# 2/8/14/20 each already carry another 6-hourly job, while 5/11/17/23 meet
32+
# only one daily patrol (05:23). Minute 43 is off the top of the hour, where
33+
# scheduled runs bunch and get delayed, and clear of the 15-minute merge-queue
34+
# patrol's :37 and :52. That census was read once, when this was written, and
35+
# nothing re-checks it; re-run the grep before you rely on it.
36+
#
37+
# ## Which way it fails
38+
#
39+
# Red, never a quiet green. With a missing `VERCEL_TOKEN`, a ref other than
40+
# `main`, a failed build or a failed upload, the job goes red with an
41+
# `::error::` saying which. A run that skipped and still went green would stop
42+
# publishing the docs with nothing to show for it, which is the failure that
43+
# froze the site for five weeks (objectui#12178).
44+
#
45+
# ## How the Vercel CLI is driven
46+
#
47+
# - The project's Root Directory is `apps/site`, so all three commands run from
48+
# the REPOSITORY ROOT. `vercel pull` writes the project settings, including
49+
# that Root Directory, to `./.vercel/`. `vercel build` then builds in
50+
# `apps/site`, reads `apps/site/vercel.json` (whose `installCommand` and
51+
# `buildCommand` override the dashboard) and writes `./.vercel/output`.
52+
# `vercel deploy --prebuilt` uploads that directory. Running the commands
53+
# from inside `apps/site` would apply the Root Directory twice.
54+
# - `VERCEL_ORG_ID` / `VERCEL_PROJECT_ID` identify the team and project. They
55+
# are identifiers, not secrets.
56+
# - `VERCEL_TOKEN` reaches only the two steps that call the Vercel API (`pull`
57+
# and `deploy`). `vercel build` needs no token, and it runs `pnpm i` plus the
58+
# whole workspace build, which means every dependency's install scripts. So
59+
# it is deliberately not given the token.
60+
# - The CLI is pinned (`VERCEL_CLI_VERSION`) so that a CLI release cannot
61+
# change what a scheduled run does. Bump it on purpose.
62+
#
63+
# The build itself is the one `ci.yml`'s `Build Docs` job runs
64+
# (`pnpm turbo run build --filter=@object-ui/site`, via
65+
# `apps/site/vercel.json`), so a green `Build Docs` on `main` predicts a green
66+
# build here.
67+
68+
on:
69+
schedule:
70+
- cron: '43 5,11,17,23 * * *'
71+
workflow_dispatch: {}
72+
73+
permissions:
74+
contents: read
75+
76+
# A later run builds the tip of `main`, which contains everything an earlier
77+
# run would have deployed, so an in-flight run is safe to cancel.
78+
concurrency:
79+
group: docs-deploy-production
80+
cancel-in-progress: true
81+
82+
jobs:
83+
deploy:
84+
name: Deploy docs to Vercel (production)
85+
runs-on: ubuntu-latest
86+
timeout-minutes: 30
87+
env:
88+
VERCEL_ORG_ID: team_06nvlGKZCkwhzdXmRdxeCNnB
89+
VERCEL_PROJECT_ID: prj_v1rXIczaI7YGmXmseQCLFUFOFxTB
90+
VERCEL_CLI_VERSION: 60.1.3
91+
steps:
92+
# Before anything else, so that a run which cannot deploy fails in
93+
# seconds, and fails red.
94+
- name: Refuse to run without VERCEL_TOKEN, or from a ref other than main
95+
env:
96+
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
97+
run: |
98+
if [ "${GITHUB_REF}" != 'refs/heads/main' ]; then
99+
echo "::error title=Docs deploy runs from main only::This run is on ${GITHUB_REF}. The docs site deploys only from main (objectui#12178); dispatch the workflow with --ref main."
100+
exit 1
101+
fi
102+
if [ -z "${VERCEL_TOKEN}" ]; then
103+
echo "::error title=VERCEL_TOKEN is missing::The secret VERCEL_TOKEN is not available to this run, so nothing can be deployed and objectui.org stays on its last deployment. Create a Vercel token scoped to the Object Stack team and store it as an Actions secret named VERCEL_TOKEN (repository or organization level)."
104+
exit 1
105+
fi
106+
107+
- name: Checkout code
108+
uses: actions/checkout@v7
109+
110+
- name: Enable Corepack and download the pinned pnpm
111+
run: bash scripts/ci-setup-pnpm.sh
112+
113+
- name: Setup Node.js
114+
uses: actions/setup-node@v7
115+
with:
116+
node-version: '22.x'
117+
cache: 'pnpm'
118+
119+
- name: Install the Vercel CLI
120+
run: npm install --global "vercel@${VERCEL_CLI_VERSION}"
121+
122+
- name: Pull the production project settings
123+
env:
124+
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
125+
run: vercel pull --yes --environment=production
126+
127+
- name: Build the site
128+
run: vercel build --prod
129+
130+
- name: Deploy the prebuilt output to production
131+
env:
132+
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
133+
run: |
134+
url=$(vercel deploy --prebuilt --prod)
135+
echo "Deployed: ${url}"
136+
echo "### Docs deployed to production" >> "$GITHUB_STEP_SUMMARY"
137+
echo "" >> "$GITHUB_STEP_SUMMARY"
138+
echo "${url} (commit ${GITHUB_SHA})" >> "$GITHUB_STEP_SUMMARY"

‎apps/site/vercel.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "https://openapi.vercel.sh/vercel.json",
3+
"git": {
4+
"deploymentEnabled": false
5+
},
6+
"installCommand": "pnpm i",
7+
"buildCommand": "cd ../.. && pnpm turbo run build --filter=@object-ui/site"
8+
}

‎content/docs/guide/ci-cd-pipeline.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ one has its own section below.
5656
| `changelog.yml` | Auto Changelog | Manual dispatch only — nothing triggers it automatically | n/a |
5757
| `shadcn-check.yml` | Check Shadcn Components | Weekly cron `0 9 * * 1`; manual | n/a |
5858
| `check-links.yml` | Check Links | Weekly cron `17 4 * * 0`; manual | n/a — reports, never gates |
59+
| `docs-deploy.yml` | Docs Deploy | 6-hourly cron `43 5,11,17,23 * * *`; manual | n/a — it deploys objectui.org and gates nothing. It goes **red** whenever the docs did not ship |
5960
| `published-dist-gate.yml` | Published Dist Tooling Scan | Nightly cron `41 3 * * *`; push to `main` touching the gate; manual | No — the blocking copy runs on the publish path, not here |
6061
| `spec-range-floors.yml` | Spec Range Floor Scan | Nightly cron `11 4 * * *`; push to `main` touching the gate; manual | No — the blocking copy runs on the publish path, not here |
6162
| `node-esm-load-gate.yml` | Node ESM Load Scan | Nightly cron `17 4 * * *`; push to `main` touching the gate; manual | No — the per-PR half is `pnpm check:esm-specifiers` in **Type Check** |
@@ -2654,6 +2655,35 @@ What follows for readers: the root `CHANGELOG.md` is a **periodically hand-curat
26542655
an auto-maintained full history. The per-package `CHANGELOG.md` files that Changesets writes on
26552656
each release commit are the source of truth for granular and current history.
26562657

2658+
### Docs Deploy (`docs-deploy.yml`)
2659+
2660+
**Trigger:** a 6-hourly cron (`43 5,11,17,23 * * *`) and manual dispatch. It has no `push` or
2661+
pull-request leg.
2662+
2663+
This workflow is the only way the docs site (`apps/site`, served at objectui.org) reaches
2664+
production. It builds the site on a GitHub-hosted runner and uploads the result to the Vercel
2665+
project as a prebuilt production deployment, using `vercel pull`, then `vercel build --prod`, then
2666+
`vercel deploy --prebuilt --prod`. The CLI version is pinned in the workflow's
2667+
`VERCEL_CLI_VERSION`. All three commands run from the repository root, because the Vercel
2668+
project's Root Directory is `apps/site`. `apps/site/vercel.json` turns Vercel's Git-triggered
2669+
deployments off (`git.deploymentEnabled: false`), so Vercel builds nothing when a branch is
2670+
pushed. The same file holds the build command, `pnpm turbo run build --filter=@object-ui/site`
2671+
run from the repository root, which is exactly what `ci.yml`'s **Build Docs** job runs. So a green
2672+
**Build Docs** on `main` predicts a green build here.
2673+
2674+
**Why it exists:** [#12178](https://github.com/objectstack-ai/objectui/issues/12178). Vercel build
2675+
minutes were nearly the whole Vercel bill, and every production build from 2026-09-06 until that
2676+
issue was fixed failed. Runners on this public repository cost nothing.
2677+
2678+
**Freshness:** the published docs can be up to six hours behind `main`. To publish right away, use
2679+
**Run workflow** on the workflow's Actions page (or `gh workflow run docs-deploy.yml --ref main`).
2680+
2681+
**Failure direction:** a missing `VERCEL_TOKEN`, a run on any ref other than `main`, a failed build
2682+
or a failed upload all turn the run **red** with an `::error::` that says which one. None of them
2683+
is ever a skipped green, because a silent skip would stop the docs updating with nothing to show
2684+
for it. `VERCEL_TOKEN` is given only to the `pull` and `deploy` steps. The build step runs the
2685+
workspace install and build, including dependencies' install scripts, without it.
2686+
26572687
## Repository Maintenance
26582688

26592689
### Auto-Labeler (`labeler.yml`)
@@ -3291,6 +3321,7 @@ on:
32913321
| `GITHUB_TOKEN` | All workflows | GitHub API access (automatic) |
32923322
| `NPM_TOKEN` | `changeset-release.yml` | npm package publishing |
32933323
| `CROSS_REPO_ISSUE_TOKEN` | `cross-repo-issue-closer.yml` | Closing issues in sibling repositories. `GITHUB_TOKEN` cannot do this — it is scoped to the repository running the workflow. When absent the workflow reports instead of closing. |
3324+
| `VERCEL_TOKEN` | `docs-deploy.yml` | Vercel API token for the Object Stack team, used by `vercel pull` and `vercel deploy`. It is stored as an **organization** secret. If it is missing, the run fails red. |
32943325
| `TURBO_TOKEN` | Build workflows | Turbo remote cache authentication |
32953326
| `TURBO_TEAM` | Build workflows | Turbo remote cache team identifier |
32963327

‎scripts/__tests__/ci-cd-pipeline-doc.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3567,6 +3567,13 @@ const SWEEP_DECLARED_NON_RUN_COMMANDS = new Map<string, string>([
35673567
'work is `lycheeverse/lychee-action`), so the rule reads no command on the workflow side ' +
35683568
'either.',
35693569
],
3570+
[
3571+
'docs-deploy.yml: turbo run build',
3572+
'The job really runs it, but not in a `run:` step. `vercel build` runs it as the ' +
3573+
'`buildCommand` of `apps/site/vercel.json`, so the parser cannot see it on the workflow ' +
3574+
"side. The section names it to say the deploy builds exactly what `ci.yml`'s Build Docs " +
3575+
'job builds (objectui#12178).',
3576+
],
35703577
[
35713578
'docs-route-eager-closure.yml: scripts/dependabot-merge-gate.mjs',
35723579
'Named as the registry whose `REQUIRED_CONTEXTS` declares this context blocking — the ' +

0 commit comments

Comments
 (0)