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 new file mode 100644 index 0000000000..805dc13fd2 --- /dev/null +++ b/apps/site/vercel.json @@ -0,0 +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 ' +