Deployment
Deploy zudo-doc sites to Cloudflare Workers with GitHub Actions CI/CD.
Overview
zudo-doc is a fully static site generator — the output of pnpm build is a directory of HTML, CSS, and JavaScript files that can be deployed anywhere. The default deployment target is Cloudflare Workers (static assets), using GitHub Actions for CI/CD automation.
This guide documents the zudo-doc showcase repository's host-owned Worker and workflows. A fresh downstream scaffold does not include these workflow files, the live AI handler, worker-entry.ts, or AiChatDailySpendCap; use this as a reference when adding those host pieces.
The CI pipeline is split into parallel jobs to keep total build time low. Site generation and document history extraction run as separate jobs and are merged before deployment.
Cloudflare Workers Static Assets
The built site is deployed using wrangler deploy. The deploy artifact includes:
The zfb build output (
dist/)Pre-generated doc history JSON files (if
docHistoryis enabled)worker-entry.ts— the source-owned Wrangler entry; it imports generateddist/, which retains_ worker. js dist/_ zfb_ inner. mjs
The site is served at root (base = "/") via the Workers static asset layer. Dynamic routes (e.g. /) are handled by dist/; all other GET requests are served as static assets from dist/.
Info
The site URL is configured via [[routes]] in wrangler.toml (custom domain binding). Live AI chat also uses the ANTHROPIC_API_KEY secret, DOCS_SITE_URL, soft per-IP RATE_LIMIT KV, and the AI_CHAT_DAILY_SPEND_CAP namespace bound to exported class AiChatDailySpendCap. Its Worker migration is v1-ai-chat-daily-spend-cap withnew_sqlite_classes = ["AiChatDailySpendCap"]; production wrangler deploy applies it.
The deployment entry is custom worker-entry.ts, not the generated adapter entry alone. The build must run before Wrangler so dist/ and dist/ exist; the custom entry preserves both the adapter handler and Durable Object export. PR CI restores that exact build artifact, runs Workers-runtime SQLite tests, then dry-runs Wrangler against the same files.
CI Pipeline Structure
The project uses three GitHub Actions workflows: production deploys (main-deploy.yml), PR checks and previews (pr-checks.yml), and branch previews (preview-deploy.yml).
Production (main-deploy.yml)
Triggered on push to main. Runs five jobs:
build-site — full clone (
fetch-depth: 0), builds the zfb site withpnpm build(noSKIP_DOC_HISTORY— the preBuild step needs full git history to populate the SSG meta block)html-validate — validates the built
dist/for HTML element errorsbuild-history — full clone (
fetch-depth: 0), runs@takazudo/zudo-doc-history-server generateto pre-extract git history for all content filesdeploy — merges the site and history artifacts, then deploys via
wrangler deploy(production)notify — sends an IFTTT webhook notification with the deploy status
Concurrency: group: production-deploy, cancel-in-progress: false — earlier deploys are allowed to finish rather than being cancelled.
PR Checks (pr-checks.yml)
Triggered on pull requests targeting main. Runs twenty jobs:
check-template-drift — verifies no drift between source files and generator templates
check-no-host-alias-in-package — rejects host-only import aliases from package sources
check-pin-parity — verifies
@takazudo/zfbpin is consistent across all package sourcescheck-fixture-settings-drift — verifies every key in
src/is present in all e2e fixture settings files (or allowlisted)config/ settings. ts check-package-safelist — verifies the generated
dist/covers all responsive/arbitrary utility classes in thesafelist. css @takazudo/zudo-docpackagecheck-b4push-ci-parity — verifies every lightweight guard gate in
scripts/has a matching CI jobrun- b4push. sh check-e2e-spec-naming — verifies all e2e spec filenames use known fixture prefixes so Playwright picks them up
check-flaky-tracking-issue — requires tracked issues for flaky/local-only tests
check-wait-debt — rejects unannotated fixed waits in E2E tests
check-component-tokens — verifies generated component-token CSS is current
lint-gates — runs tag, compatibility, resolution, and design-token guards
typecheck — runs root and workspace TypeScript checks
worker-contract — restores the
build-siteartifact, runs Workers-runtime Durable Object tests, and verifies the Wrangler dry-runpackage-tests — runs all package-scoped vitest suites
root-tests — runs the root unit test suite
build-site — full clone (
fetch-depth: 0),pnpm build, thencheck:links(noSKIP_DOC_HISTORY— same rationale as production: preBuild needs full history for SSG meta dates)html-validate — validates the built
dist/for HTML element errorsbuild-history — full clone, doc history generation
e2e — runs Playwright end-to-end tests
preview — merges artifacts and uploads a preview via
wrangler versions upload --preview-alias, then posts the URL as a PR comment
Concurrency: group: pr-checks-{pr-number}, cancel-in-progress: true — outdated runs are cancelled to save CI minutes.
Preview Deployments
Each PR gets a stable preview-alias URL via wrangler versions upload --preview-alias <alias>. The generated adapter-only preview config keeps preview_urls = true while leaving the primary workers_dev route disabled, just like production. The alias is pr-<number> for PR checks and the branch slug for branch previews. The URL is on *.workers.dev (not the production custom domain) and its subdomain starts with the alias:
https://<alias>-zudo-doc-preview.<your-subdomain>.workers.dev CI parses the exact host from wrangler's output and posts it automatically as a comment on the PR by the preview job. When the isolated service is first created, CI explicitly enables only its preview-URL subdomain setting through the Cloudflare API, then retries the aliased upload. Its entry sends GET/HEAD directly to ASSETS with an internal hostname and delegates other methods to the generated adapter. Cloudflare's asset binding matches only the pathname; this avoids treating the preview alias as a same-zone Worker fetch while retaining the API POST smoke gate. Production continues to export the generated adapter directly.
Cloudflare does not issue preview URLs for versions implementing Durable Objects. Preview workflows therefore generate an adapter-only config/entry for a separate preview service after the site build. The preview validates SSR and assets, but does not bind AI_CHAT_DAILY_SPEND_CAP or apply its migration, production RATE_LIMIT KV, or production's custom-domain route; the live AI admission and rate-limit path is production-only.
SKIP_DOC_HISTORY
When SKIP_DOC_HISTORY=1 is set, the doc-history zfb plugin skips all git history calls and writes an empty manifest, causing the visible Created/Updated/Author block to be absent from every SSG page. In standard CI this flag is not set on the build-site job — the preBuild step needs full git access to populate real dates.
| Context | Value | Why |
|---|---|---|
CI build-site job | unset | preBuild needs full git history to populate the SSG meta block (Created/Updated/Author) |
CI e2e job | unset | E2E tests verify history features inline |
Local pnpm build | unset | The Created/Updated/Author meta block is embedded (preBuild). The per-page history dropdown JSON is not generated locally by default — see GEN_DOC_HISTORY below |
Tip
SKIP_DOC_HISTORY=1 is an escape hatch for shallow-clone or custom CI variants where git history is unavailable. Setting it causes the Created/Updated/Author block to be absent from every page.
GEN_DOC_HISTORY
GEN_DOC_HISTORY=1 (introduced in #1986) gates the doc-history plugin's postBuild hook — the step that writes the per-page revision/diff JSON into dist/doc-history/ feeding the DocHistory dropdown island. The plugin's preBuild hook (the visible Created/Updated/Author meta block) is unaffected and is governed by SKIP_DOC_HISTORY alone. postBuild now follows this decision table:
| Context | postBuild JSON | Why |
|---|---|---|
SKIP_DOC_HISTORY=1 | skipped | overrides everything below — wins over DOC_HISTORY_SKIP_POSTBUILD, GEN_DOC_HISTORY, and CI |
DOC_HISTORY_SKIP_POSTBUILD=1 | skipped | overrides GEN_DOC_HISTORY and CI, loses only to SKIP_DOC_HISTORY |
GEN_DOC_HISTORY=1 | generated | explicit local opt-in |
CI / GITHUB_ACTIONS set | generated | keeps the build-site artifact byte-identical to before |
plain local pnpm build | skipped | the #1986 default — see Tip below |
Tip
postBuild is off by default locally because its per-content-file git log --follow chain exceeds zfb's 120s postBuild lifecycle budget on a large corpus and fails a plain pnpm build. It is redundant locally anyway: dev reads history live from the standalone :4322 server, and CI generates the JSON in the dedicated parallel build-history job. To make the dropdown work when previewing a locally-built dist/ (or building the Tauri offline reader), run GEN_DOC_HISTORY=1 pnpm build. Note this gates only the zfb plugin's postBuild — the standalone @takazudo/zudo-doc-history-server generate CLI used by the build-history job is never gated and always generates when invoked.
DOC_HISTORY_SKIP_POSTBUILD
DOC_HISTORY_SKIP_POSTBUILD=1 (introduced in #2927) skips only the postBuild per-page dropdown JSON step from the decision table above — the same step GEN_DOC_HISTORY opts into. Unlike SKIP_DOC_HISTORY=1, it does not blank the preBuild manifest, so the Created/Updated/Author block still shows real git dates.
Use it for a shallow-clone CI variant: one with enough git history for the cheap preBuild --name-status walk, but that wants to explicitly opt out of the heavier per-file postBuild git log --follow chain rather than relying on the plain-local-build default (which would not apply if that variant also sets CI=true, since CI otherwise generates).
Tip
The name deliberately does not contain the substring SKIP_DOC_HISTORY, so it is a distinct marker for scripts/'s literal survivor scan and needs no allowlist changes.
Inter-Job Data Sharing
Build jobs tar their output and publish it with actions/upload-artifact. Consumer jobs use actions/download-artifact and extract the archives from ${{ runner.temp }} before validation or deployment:
name: site-dist # contains site-dist.tar
name: doc-history # contains doc-history.tar
retention-days: 1The deploy and preview jobs restore both archives and merge doc history into dist/ before Wrangler. The worker-contract job restores the same site-dist artifact, so runtime tests and the dry-run verify the exact build later used by preview/deploy jobs.
Note
Tar keeps arbitrary route filenames intact during artifact upload. One-day retention bounds storage while preserving a run-scoped paper trail; this handoff does not rely on mutable cache keys or cross-run cache state.
Required Secrets
Configure these in your repository's Settings → Secrets and variables → Actions:
| Secret | Description |
|---|---|
CLOUDFLARE_API_TOKEN | Cloudflare API token with Workers deployment permissions |
CLOUDFLARE_ACCOUNT_ID | Your Cloudflare account ID |
IFTTT_PROD_NOTIFY | IFTTT webhook URL for deploy notifications (optional) |
Warning
The CLOUDFLARE_API_TOKEN needs the Workers: Edit permission (and KV Storage: Edit if you use KV namespaces). Scoping it to the specific Worker is recommended over granting account-wide access.