zudo-doc
GitHub repository

Type to search...

to open search from anywhere

Deployment

Created Apr 13, 2026Updated Jul 18, 2026Takeshi Takatsudo

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 docHistory is enabled)

  • worker-entry.ts — the source-owned Wrangler entry; it imports generated dist/_worker.js, which retains dist/_zfb_inner.mjs

The site is served at root (base = "/") via the Workers static asset layer. Dynamic routes (e.g. /api/ai-chat) are handled by dist/_worker.js; 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/_worker.js and dist/_zfb_inner.mjs 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:

  1. build-site — full clone (fetch-depth: 0), builds the zfb site with pnpm build (no SKIP_DOC_HISTORY — the preBuild step needs full git history to populate the SSG meta block)

  2. html-validate — validates the built dist/ for HTML element errors

  3. build-history — full clone (fetch-depth: 0), runs @takazudo/zudo-doc-history-server generate to pre-extract git history for all content files

  4. deploy — merges the site and history artifacts, then deploys via wrangler deploy (production)

  5. 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:

  1. check-template-drift — verifies no drift between source files and generator templates

  2. check-no-host-alias-in-package — rejects host-only import aliases from package sources

  3. check-pin-parity — verifies @takazudo/zfb pin is consistent across all package sources

  4. check-fixture-settings-drift — verifies every key in src/config/settings.ts is present in all e2e fixture settings files (or allowlisted)

  5. check-package-safelist — verifies the generated dist/safelist.css covers all responsive/arbitrary utility classes in the @takazudo/zudo-doc package

  6. check-b4push-ci-parity — verifies every lightweight guard gate in scripts/run-b4push.sh has a matching CI job

  7. check-e2e-spec-naming — verifies all e2e spec filenames use known fixture prefixes so Playwright picks them up

  8. check-flaky-tracking-issue — requires tracked issues for flaky/local-only tests

  9. check-wait-debt — rejects unannotated fixed waits in E2E tests

  10. check-component-tokens — verifies generated component-token CSS is current

  11. lint-gates — runs tag, compatibility, resolution, and design-token guards

  12. typecheck — runs root and workspace TypeScript checks

  13. worker-contract — restores the build-site artifact, runs Workers-runtime Durable Object tests, and verifies the Wrangler dry-run

  14. package-tests — runs all package-scoped vitest suites

  15. root-tests — runs the root unit test suite

  16. build-site — full clone (fetch-depth: 0), pnpm build, then check:links (no SKIP_DOC_HISTORY — same rationale as production: preBuild needs full history for SSG meta dates)

  17. html-validate — validates the built dist/ for HTML element errors

  18. build-history — full clone, doc history generation

  19. e2e — runs Playwright end-to-end tests

  20. 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.

ContextValueWhy
CI build-site jobunsetpreBuild needs full git history to populate the SSG meta block (Created/Updated/Author)
CI e2e jobunsetE2E tests verify history features inline
Local pnpm buildunsetThe 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:

ContextpostBuild JSONWhy
SKIP_DOC_HISTORY=1skippedoverrides everything below — wins over DOC_HISTORY_SKIP_POSTBUILD, GEN_DOC_HISTORY, and CI
DOC_HISTORY_SKIP_POSTBUILD=1skippedoverrides GEN_DOC_HISTORY and CI, loses only to SKIP_DOC_HISTORY
GEN_DOC_HISTORY=1generatedexplicit local opt-in
CI / GITHUB_ACTIONS setgeneratedkeeps the build-site artifact byte-identical to before
plain local pnpm buildskippedthe #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/check-compatibility-contract.ts'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: 1

The 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:

SecretDescription
CLOUDFLARE_API_TOKENCloudflare API token with Workers deployment permissions
CLOUDFLARE_ACCOUNT_IDYour Cloudflare account ID
IFTTT_PROD_NOTIFYIFTTT 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.

Revision History

Takeshi TakatsudoCreated: 2026-04-13T21:29:34+09:00Updated: 2026-07-18T19:20:39+09:00

AI Assistant

Ask a question about the documentation.

Preview theme

Loading theme previews…