From cc39b9cf9963eefeb702ac87571419180537b6fa Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:12:46 +0800 Subject: [PATCH 1/4] fix(site): build the docs deploy with the command CI already runs, so the production build stops failing (objectui#12178) Every production build of the Vercel project `objectui` (Root Directory `apps/site`) has failed since 2026-09-06 with `Module not found: Can't resolve '@object-ui/example-schema-catalog'`, so objectui.org has been frozen since then. The project's build command only exists in the Vercel dashboard: `pnpm --filter '../../packages/*' -r build && pnpm build`. Its filter selects `packages/*` only and never builds `examples/schema-catalog`, whose `exports` resolve only into `dist/`. Until #7986 that gap was covered by `apps/site`'s `prebuild` hook (`pnpm --filter @object-ui/example-schema-catalog build`). #7986 removed the hook on purpose so that turbo's `^build` is the only source of build order, and moved every in-repo path to the turbo form, including the root `site:build` script. The dashboard command lives outside the repo, so it was not updated. The deployment for #7986's commit is the first ERROR, and the one for the commit before it is the last READY. CI's `Build Docs` stayed green the whole time because it runs `pnpm turbo run build --filter='@object-ui/site'`, which builds the site's dependency closure, schema catalog included. This adds `apps/site/vercel.json`, which Vercel reads from the Root Directory and which overrides the dashboard values. Its `buildCommand` is that same turbo command, run from the repo root, and its `installCommand` keeps the dashboard's current `pnpm i`. With that, the deploy's build is in the repo and is the build CI validates. Refs: objectui#12178 Co-Authored-By: Claude Opus 5.5 --- apps/site/vercel.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 apps/site/vercel.json diff --git a/apps/site/vercel.json b/apps/site/vercel.json new file mode 100644 index 0000000000..c52dc484c8 --- /dev/null +++ b/apps/site/vercel.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "installCommand": "pnpm i", + "buildCommand": "cd ../.. && pnpm turbo run build --filter=@object-ui/site" +} From fd40e2f90489c2edea8b69023adcd7b08892823d Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:38:06 +0800 Subject: [PATCH 2/4] ci(site): deploy the docs from a GitHub Actions workflow every 6 hours, and turn Vercel's Git-triggered builds off (objectui#12178) Almost all of the maintainer's Vercel bill was "Build CPU Minutes" for the `objectui` project. Vercel built once for every commit pushed to `main`, and started a preview build for every other branch that the Ignored Build Step then cancelled. Every production build since 2026-09-06 failed (objectui#12178), so the spend deployed nothing. This repository is public, so `ubuntu-latest` minutes are free. The maintainer decided the docs deploy only from `main`, on free compute, every 6 hours. - `.github/workflows/docs-deploy.yml` runs on a 6-hourly cron (`43 5,11,17,23 * * *`, chosen off the hour and away from the hours other crons already use) and on `workflow_dispatch`. There is no `push` trigger and no `paths` list. The workflow runs `vercel pull`, `vercel build --prod` and `vercel deploy --prebuilt --prod` with a pinned CLI, all from the repo root, because the project's Root Directory is `apps/site`. Vercel builds nothing. The team and project ids are inlined (they are not secrets). `VERCEL_TOKEN` reaches only the `pull` and `deploy` steps: the build step, which runs every dependency's install scripts, never sees it. - Which way it fails: a missing `VERCEL_TOKEN`, or a run on a ref other than `main`, fails red with an `::error::` before checkout. It never skips green, because a silent skip would stop the docs updating without anyone seeing it. `concurrency: docs-deploy-production` with `cancel-in-progress`, because a later run builds a tip that contains the earlier one. - `apps/site/vercel.json` gains `git.deploymentEnabled: false`, which turns off Git-triggered deployments for every branch. The workflow becomes the only path to production. - `content/docs/guide/ci-cd-pipeline.md` gets the workflow's section, its inventory row and the `VERCEL_TOKEN` secret row. `scripts/__tests__/ci-cd-pipeline-doc.test.ts` declares the one command the section names that no `run:` step shows: `turbo run build`, which runs through `vercel build` and the `buildCommand` in `vercel.json`. Refs: objectui#12178 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs-deploy.yml | 138 +++++++++++++++++++ apps/site/vercel.json | 3 + content/docs/guide/ci-cd-pipeline.md | 31 +++++ scripts/__tests__/ci-cd-pipeline-doc.test.ts | 7 + 4 files changed, 179 insertions(+) create mode 100644 .github/workflows/docs-deploy.yml diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 0000000000..7fccd94f5d --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,138 @@ +name: Docs Deploy + +# Builds the docs site (`apps/site`, the Vercel project `objectui`, served at +# objectui.org) on a GitHub-hosted runner and uploads the prebuilt output to +# Vercel as a production deployment. Vercel itself builds nothing. +# +# ## Why the build moved here (objectui#12178) +# +# On the maintainer's Vercel bill almost all of the cost was "Build CPU +# Minutes". Vercel built this project once for every commit pushed to `main`, +# plus a preview build for every other branch. The preview builds were +# cancelled by the Ignored Build Step, and every production build since +# 2026-09-06 failed (objectui#12178), so all of that spend deployed nothing. +# This repository is public, so `ubuntu-latest` minutes cost nothing. The +# maintainer decided the site deploys only from `main`, on free compute. +# `apps/site/vercel.json` turns Vercel's Git-triggered deployments off +# (`git.deploymentEnabled: false`), so this workflow is now the ONLY way the +# site gets deployed. +# +# ## When it runs: every 6 hours, or on demand +# +# There is no `push` trigger, on purpose. The maintainer chose a fixed clock: +# at most 4 builds a day, no competition with pull-request CI for runners, +# and no `paths` list to keep in step with the site's build closure. The docs +# can therefore be up to 6 hours behind `main`. To publish now, use "Run +# workflow" on this workflow's Actions page (or +# `gh workflow run docs-deploy.yml --ref main`). +# +# The cron is `43 5,11,17,23 * * *`, chosen against +# `grep -rn 'cron:' .github/workflows/`. Hours 0/6/12/18, 1/7/13/19 and +# 2/8/14/20 each already carry another 6-hourly job, while 5/11/17/23 meet +# only one daily patrol (05:23). Minute 43 is off the top of the hour, where +# scheduled runs bunch and get delayed, and clear of the 15-minute merge-queue +# patrol's :37 and :52. That census was read once, when this was written, and +# nothing re-checks it; re-run the grep before you rely on it. +# +# ## Which way it fails +# +# Red, never a quiet green. With a missing `VERCEL_TOKEN`, a ref other than +# `main`, a failed build or a failed upload, the job goes red with an +# `::error::` saying which. A run that skipped and still went green would stop +# publishing the docs with nothing to show for it, which is the failure that +# froze the site for five weeks (objectui#12178). +# +# ## How the Vercel CLI is driven +# +# - The project's Root Directory is `apps/site`, so all three commands run from +# the REPOSITORY ROOT. `vercel pull` writes the project settings, including +# that Root Directory, to `./.vercel/`. `vercel build` then builds in +# `apps/site`, reads `apps/site/vercel.json` (whose `installCommand` and +# `buildCommand` override the dashboard) and writes `./.vercel/output`. +# `vercel deploy --prebuilt` uploads that directory. Running the commands +# from inside `apps/site` would apply the Root Directory twice. +# - `VERCEL_ORG_ID` / `VERCEL_PROJECT_ID` identify the team and project. They +# are identifiers, not secrets. +# - `VERCEL_TOKEN` reaches only the two steps that call the Vercel API (`pull` +# and `deploy`). `vercel build` needs no token, and it runs `pnpm i` plus the +# whole workspace build, which means every dependency's install scripts. So +# it is deliberately not given the token. +# - The CLI is pinned (`VERCEL_CLI_VERSION`) so that a CLI release cannot +# change what a scheduled run does. Bump it on purpose. +# +# The build itself is the one `ci.yml`'s `Build Docs` job runs +# (`pnpm turbo run build --filter=@object-ui/site`, via +# `apps/site/vercel.json`), so a green `Build Docs` on `main` predicts a green +# build here. + +on: + schedule: + - cron: '43 5,11,17,23 * * *' + workflow_dispatch: {} + +permissions: + contents: read + +# A later run builds the tip of `main`, which contains everything an earlier +# run would have deployed, so an in-flight run is safe to cancel. +concurrency: + group: docs-deploy-production + cancel-in-progress: true + +jobs: + deploy: + name: Deploy docs to Vercel (production) + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + VERCEL_ORG_ID: team_06nvlGKZCkwhzdXmRdxeCNnB + VERCEL_PROJECT_ID: prj_v1rXIczaI7YGmXmseQCLFUFOFxTB + VERCEL_CLI_VERSION: 60.1.3 + steps: + # Before anything else, so that a run which cannot deploy fails in + # seconds, and fails red. + - name: Refuse to run without VERCEL_TOKEN, or from a ref other than main + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + if [ "${GITHUB_REF}" != 'refs/heads/main' ]; then + 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." + exit 1 + fi + if [ -z "${VERCEL_TOKEN}" ]; then + 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)." + exit 1 + fi + + - name: Checkout code + uses: actions/checkout@v7 + + - name: Enable Corepack and download the pinned pnpm + run: bash scripts/ci-setup-pnpm.sh + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: '22.x' + cache: 'pnpm' + + - name: Install the Vercel CLI + run: npm install --global "vercel@${VERCEL_CLI_VERSION}" + + - name: Pull the production project settings + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: vercel pull --yes --environment=production + + - name: Build the site + run: vercel build --prod + + - name: Deploy the prebuilt output to production + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + url=$(vercel deploy --prebuilt --prod) + echo "Deployed: ${url}" + echo "### Docs deployed to production" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "${url} (commit ${GITHUB_SHA})" >> "$GITHUB_STEP_SUMMARY" diff --git a/apps/site/vercel.json b/apps/site/vercel.json index c52dc484c8..805dc13fd2 100644 --- a/apps/site/vercel.json +++ b/apps/site/vercel.json @@ -1,5 +1,8 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", + "git": { + "deploymentEnabled": false + }, "installCommand": "pnpm i", "buildCommand": "cd ../.. && pnpm turbo run build --filter=@object-ui/site" } diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index e32b8a657d..fb77eb1dee 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -56,6 +56,7 @@ one has its own section below. | `changelog.yml` | Auto Changelog | Manual dispatch only — nothing triggers it automatically | n/a | | `shadcn-check.yml` | Check Shadcn Components | Weekly cron `0 9 * * 1`; manual | n/a | | `check-links.yml` | Check Links | Weekly cron `17 4 * * 0`; manual | n/a — reports, never gates | +| `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 | | `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 | | `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 | | `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 an auto-maintained full history. The per-package `CHANGELOG.md` files that Changesets writes on each release commit are the source of truth for granular and current history. +### Docs Deploy (`docs-deploy.yml`) + +**Trigger:** a 6-hourly cron (`43 5,11,17,23 * * *`) and manual dispatch. It has no `push` or +pull-request leg. + +This workflow is the only way the docs site (`apps/site`, served at objectui.org) reaches +production. It builds the site on a GitHub-hosted runner and uploads the result to the Vercel +project as a prebuilt production deployment, using `vercel pull`, then `vercel build --prod`, then +`vercel deploy --prebuilt --prod`. The CLI version is pinned in the workflow's +`VERCEL_CLI_VERSION`. All three commands run from the repository root, because the Vercel +project's Root Directory is `apps/site`. `apps/site/vercel.json` turns Vercel's Git-triggered +deployments off (`git.deploymentEnabled: false`), so Vercel builds nothing when a branch is +pushed. The same file holds the build command, `pnpm turbo run build --filter=@object-ui/site` +run from the repository root, which is exactly what `ci.yml`'s **Build Docs** job runs. So a green +**Build Docs** on `main` predicts a green build here. + +**Why it exists:** [#12178](https://github.com/objectstack-ai/objectui/issues/12178). Vercel build +minutes were nearly the whole Vercel bill, and every production build from 2026-09-06 until that +issue was fixed failed. Runners on this public repository cost nothing. + +**Freshness:** the published docs can be up to six hours behind `main`. To publish right away, use +**Run workflow** on the workflow's Actions page (or `gh workflow run docs-deploy.yml --ref main`). + +**Failure direction:** a missing `VERCEL_TOKEN`, a run on any ref other than `main`, a failed build +or a failed upload all turn the run **red** with an `::error::` that says which one. None of them +is ever a skipped green, because a silent skip would stop the docs updating with nothing to show +for it. `VERCEL_TOKEN` is given only to the `pull` and `deploy` steps. The build step runs the +workspace install and build, including dependencies' install scripts, without it. + ## Repository Maintenance ### Auto-Labeler (`labeler.yml`) @@ -3291,6 +3321,7 @@ on: | `GITHUB_TOKEN` | All workflows | GitHub API access (automatic) | | `NPM_TOKEN` | `changeset-release.yml` | npm package publishing | | `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. | +| `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. | | `TURBO_TOKEN` | Build workflows | Turbo remote cache authentication | | `TURBO_TEAM` | Build workflows | Turbo remote cache team identifier | diff --git a/scripts/__tests__/ci-cd-pipeline-doc.test.ts b/scripts/__tests__/ci-cd-pipeline-doc.test.ts index 8fb8c6d330..d11ffcff80 100644 --- a/scripts/__tests__/ci-cd-pipeline-doc.test.ts +++ b/scripts/__tests__/ci-cd-pipeline-doc.test.ts @@ -3567,6 +3567,13 @@ const SWEEP_DECLARED_NON_RUN_COMMANDS = new Map([ 'work is `lycheeverse/lychee-action`), so the rule reads no command on the workflow side ' + 'either.', ], + [ + 'docs-deploy.yml: turbo run build', + 'The job really runs it, but not in a `run:` step. `vercel build` runs it as the ' + + '`buildCommand` of `apps/site/vercel.json`, so the parser cannot see it on the workflow ' + + "side. The section names it to say the deploy builds exactly what `ci.yml`'s Build Docs " + + 'job builds (objectui#12178).', + ], [ 'docs-route-eager-closure.yml: scripts/dependabot-merge-gate.mjs', 'Named as the registry whose `REQUIRED_CONTEXTS` declares this context blocking — the ' + From 1844c7ba1ae95497d57180b6f638dbfb2c0e5d81 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:38:25 +0800 Subject: [PATCH 3/4] chore(ci): TEMPORARY pull_request dry run of docs-deploy.yml (build only, no deploy) - reverted in the next commit Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs-deploy.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 7fccd94f5d..58bfb21ade 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -69,6 +69,10 @@ on: schedule: - cron: '43 5,11,17,23 * * *' workflow_dispatch: {} + # TEMPORARY (removed before review): build-only dry run on this PR, deploy step skipped. + pull_request: + paths: + - '.github/workflows/docs-deploy.yml' permissions: contents: read @@ -95,7 +99,7 @@ jobs: env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: | - if [ "${GITHUB_REF}" != 'refs/heads/main' ]; then + if [ "${GITHUB_EVENT_NAME}" != 'pull_request' ] && [ "${GITHUB_REF}" != 'refs/heads/main' ]; then 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." exit 1 fi @@ -127,7 +131,16 @@ jobs: - name: Build the site run: vercel build --prod + - name: TEMPORARY dry-run evidence + if: github.event_name == 'pull_request' + run: | + ls -la .vercel/output + cat .vercel/output/config.json | head -c 2000; echo + ls .vercel/output/functions | head -50 + cat .vercel/output/builds.json | head -c 1500; echo + - name: Deploy the prebuilt output to production + if: github.event_name != 'pull_request' env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: | From a45c593fa63ca3cc6db119fed2a4a5bd5729a7e4 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:45:28 +0800 Subject: [PATCH 4/4] chore(ci): drop the temporary pull_request dry run of docs-deploy.yml The dry run (run 38200568488) built the site through `vercel pull` and `vercel build --prod` from the repository root: `vercel build` ran the `installCommand` and `buildCommand` from `apps/site/vercel.json`, turbo reported 30 of 30 tasks successful, and the output was written to `.vercel/output`. The deploy step was skipped. The workflow goes back to `schedule` plus `workflow_dispatch` only. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs-deploy.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 58bfb21ade..7fccd94f5d 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -69,10 +69,6 @@ on: schedule: - cron: '43 5,11,17,23 * * *' workflow_dispatch: {} - # TEMPORARY (removed before review): build-only dry run on this PR, deploy step skipped. - pull_request: - paths: - - '.github/workflows/docs-deploy.yml' permissions: contents: read @@ -99,7 +95,7 @@ jobs: env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: | - if [ "${GITHUB_EVENT_NAME}" != 'pull_request' ] && [ "${GITHUB_REF}" != 'refs/heads/main' ]; then + if [ "${GITHUB_REF}" != 'refs/heads/main' ]; then 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." exit 1 fi @@ -131,16 +127,7 @@ jobs: - name: Build the site run: vercel build --prod - - name: TEMPORARY dry-run evidence - if: github.event_name == 'pull_request' - run: | - ls -la .vercel/output - cat .vercel/output/config.json | head -c 2000; echo - ls .vercel/output/functions | head -50 - cat .vercel/output/builds.json | head -c 1500; echo - - name: Deploy the prebuilt output to production - if: github.event_name != 'pull_request' env: VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: |