Today Platform Web — Dev Docs
Workflow

Deployment

Five Vercel projects, four owned by GitHub Actions prebuilt deploys and one on Vercel's Git Integration. CI gates every Actions-owned deploy before it ships.

The repo deploys to five Vercel projects. Four are owned by GitHub Actions (prebuilt deploys, Vercel auto-deploy disabled in vercel.json); the fifth predates the convention and runs on Vercel's Git Integration.

Vercel projectSource pathWorkflowRepo variableOwner
today-webapps/webdeploy-web.ymlVERCEL_PROJECT_ID_WEBActions
today-adminapps/admindeploy-admin.ymlVERCEL_PROJECT_ID_ADMINActions
today-design-systemdesign-system/docsdeploy-docs.ymlVERCEL_PROJECT_ID_DOCSActions
today-dev-docs (Vercel project: web-dev-docs)apps/dev-docsdeploy-dev-docs.ymlVERCEL_PROJECT_ID_DEV_DOCSActions (intended) — see Current state
webview-bridge-docsapps/webview-bridge-docs(none)(n/a)Vercel Git

For the underlying conceptual model — what build time / deploy time / runtime mean on Vercel, what vercel.json actually controls, and why we keep projects linked to Git even with auto-deploy off — see Architecture → Vercel deploy model.

Current state of each project's deploy path

Verified against the Vercel REST API on 2026-06-03 (last 100 deployments per project, source breakdown):

Projectvercel.json git.deploymentEnabledProject-level previewDeploymentsDisabledActions workflow statusLast 100 deployments source
today-webfalsetruepassing100 cli
today-adminfalsetruepassing100 cli
today-design-systemfalsetruepassing100 cli
web-dev-docsabsent (defaults true)nullfailing every run (missing repo variable VERCEL_PROJECT_ID_DEV_DOCS)3 git, 1 import
webview-bridge-docsabsentnulln/a75 git, 1 import, 3 redeploy

The web-dev-docs row is a known misconfig: deploy-dev-docs.yml references vars.VERCEL_PROJECT_ID_DEV_DOCS but that variable is not set on the repo, so vercel pull exits with You specified VERCEL_ORG_ID but you forgot to specify VERCEL_PROJECT_ID. In practice, Vercel's default Git Integration is shipping dev-docs today, not the Actions workflow. Resolving requires either:

  1. Set VERCEL_PROJECT_ID_DEV_DOCS = prj_2tjNCCu9K751RCGGnwUsMVRxwXGr as a repo variable, and add git.deploymentEnabled: false to apps/dev-docs/vercel.json (Actions-owned shape, matches the other three).
  2. Delete .github/workflows/deploy-dev-docs.yml entirely (Vercel-Git-owned shape, matches webview-bridge-docs).
  3. Keep the workflow as workflow_dispatch-only for emergency manual deploys; remove push / pull_request triggers.

Don't do "set the variable but leave vercel.json alone" — that flips the state from "one deploy path (Vercel Git)" to "two deploy paths racing" (Actions prebuilt + Vercel Git both fire on every push).

Environment separation

Each workflow maps the Git ref to a GitHub environment + Vercel target:

Git refGitHub environmentVercel target
main<app>-productionproduction
dev / pr-*<app>-previewpreview

PRs land on the preview target; only after sync-main.yml advances main does the production deploy fire.

Workflow shape (same across the four)

Every deploy workflow follows the same skeleton:

on:
  push:
    branches: [main, dev]
    paths: ['apps/<name>/**', 'packages/<deps>/**']
  pull_request:
    types: [opened, synchronize, reopened]
    paths: [same]
  workflow_dispatch:

env:
  VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
  VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID_<NAME> }}
  NODE_VERSION: '24.14.1'

jobs:
  deploy:
    environment:
      name: ${{ github.ref == 'refs/heads/main' && '<app>-production' || '<app>-preview' }}
    steps:
      - checkout (with LFS)
      - setup pnpm + node + cache
      - install Vercel CLI
      - vercel pull (preview or production env)
      - inject @todayai-labs/* GH Packages auth into .npmrc
      - vercel build (prebuilt deploy)
      - wait for required CI checks
      - vercel deploy --prebuilt
      - PR comment with preview URL
      - mark Vercel git status successful

The wait-for-CI step is what makes deploys safe — it blocks until Lint, Format, Typecheck, Unit Tests, and Build are all green on the same SHA. See deploy-docs.yml's "Wait for CI checks" step for the exact polling logic (it dedupes runs to handle merge-queue speculative-branch entries).

Path-based triggering

Each deploy workflow declares paths: for both push and pull_request events so changes outside the app don't trigger a deploy. For deploy-dev-docs.yml:

paths:
  - '.github/workflows/deploy-dev-docs.yml'
  - 'apps/dev-docs/**'
  - 'packages/tsconfig/**'

A change in apps/web doesn't run deploy-dev-docs. Likewise a pnpm-lock.yaml change triggers every deploy (since any package could be affected) — that's intentional.

Caching

Each workflow caches the .vercel/ directory (project metadata, pulled env vars) using a weekly-rotating key:

key: vercel-<app>-${{ runner.os }}-week${{ steps.cache-week.outputs.week }}-...

The week number expires the cache every ~7 days so stale Vercel project config never lingers. Day-to-day, the cache hits, and vercel pull is near-instant.

PR preview comments

Each workflow posts a PR comment with the preview URL after deploy:

📚 Docs Preview deployed!
https://today-dev-docs-git-pr-ca-dev-docs.vercel.app

The comment is updated in place on subsequent pushes (looking up the previous bot comment by body content), so each PR has one preview-URL comment that always shows the latest deploy.

Required Vercel setup

For a new app (and for apps/dev-docs initially), the human-on-keyboard checklist:

  1. Vercel side:
    • Create a new project in the todayai team
    • Set root directory to apps/<name>
    • Framework preset: Next.js (Turbopack or Webpack as the app dictates)
    • Enable Deployment Protection if the site is internal
  2. GitHub side:
    • Add VERCEL_PROJECT_ID_<NAME> to repo variables
    • Confirm VERCEL_ORG_ID is already set (it's shared)
    • Confirm VERCEL_TOKEN is already in repo secrets
  3. GitHub environments:
    • Create <app>-preview and <app>-production GitHub environments
    • Optionally add reviewers on <app>-production for a manual prod gate

After that, the deploy workflow runs unattended.

Build duplication and the wait-for-CI ordering

A push that touches apps/web currently triggers two next build invocations of apps/web in parallel — one from ci.yml's Build job (verification, output discarded; required check for branch protection) and one from deploy-web.yml's Build Project step (vercel build, produces .vercel/output for prebuilt upload). Measured for SHA 64c2a6a:

ci.yml `Build` job         06:52:13Z → 06:55:44Z  (~3.5 min, every affected app)
deploy-web.yml `Build`     06:52:38Z → 06:54:14Z  (~1.5 min, apps/web only)

The duplication is intentional — see Architecture → Vercel deploy model for why both are valuable rather than redundant.

However, the current step order inside each deploy workflow has a real inefficiency:

current: vercel pull → vercel build → Wait for CI checks → vercel deploy
better:  vercel pull → Wait for CI checks → vercel build → vercel deploy

In the current order, if CI fails after vercel build succeeded, the deploy artifact was built and is then thrown away. Reordering short-circuits that wasted compute when CI is already red on the same SHA. Low-risk follow-up worth doing the next time someone is in these workflows.

Pre-existing oddities

Two notes worth knowing:

  • deploy-docs.yml line 242 stamps the Vercel commit-status context as Vercel – today-tdx-docs, but the Vercel project is named today-design-system. The status check therefore never overlaps with the one Vercel for GitHub would otherwise post — it's effectively an orphan status. Cosmetic; should be renamed to Vercel – today-design-system.
  • vercel.json per app includes an ignoreCommand: that lists the package's own path plus its workspace dep paths. Updating workspace deps requires updating this list, or Vercel won't rebuild when those deps change.

Rollback

Vercel keeps every deploy; rolling back is "promote a previous deploy" from the Vercel UI. The deploy workflow doesn't automate rollback — by design, since the recovery flow involves a human deciding which prior deploy is the right rollback target.

For code-level rollback, see dev → main sync § "Rolling back".

On this page