Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions .github/workflows/docs-deploy.yml
Original file line number Diff line number Diff line change
@@ -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"
8 changes: 8 additions & 0 deletions apps/site/vercel.json
Original file line number Diff line number Diff line change
@@ -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"
}
31 changes: 31 additions & 0 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** |
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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 |

Expand Down
7 changes: 7 additions & 0 deletions scripts/__tests__/ci-cd-pipeline-doc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3567,6 +3567,13 @@ const SWEEP_DECLARED_NON_RUN_COMMANDS = new Map<string, string>([
'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 ' +
Expand Down
Loading