Deploy notes
Audience: LLM agents working in this repository. Companion doc: none — repo-maintenance only. Verified in: iter/docs-polish-dx
Use this document when deployment behavior or CI build commands are involved. Deployment configuration for demo.grist-widgets.com (Cloudflare Pages) is not documented in this repo — it lives in the Cloudflare dashboard.
Expected deploy targets
This repository is structured so that at minimum these artifacts are deployable:
- landing / docs-integrated site from
apps/www - docs site from
apps/docs - widgets app from
apps/playground - template app from
templates/grist-widget-template-vite
apps/web (the earlier Vite + shadcn landing site) still lives in the repo but is no longer built or deployed — apps/www (Next.js + fumadocs, static-exported) replaced it at the GitHub Pages site root.
apps/docs (VitePress) is likewise no longer built or deployed by this repo's GitHub Pages workflow — apps/www's own /docs/sdk/{guide,api,design} is a full port of apps/docs' guide/api/design content, plus quickstart/cli/registry pages VitePress never had. apps/docs' code stays in the repo and can still deploy independently to Cloudflare Pages (see "Cloudflare Pages" below) — that path is unaffected by this change.
GitHub Pages deploy: apps/playground + apps/www (one workflow)
Both of this repo's own GitHub Pages targets — apps/www at the site root (including its own /docs/*), apps/playground under /playground/ — are built and published by one workflow: .github/workflows/deploy-apps.yml. It runs on every push to main touching either of apps/playground/**, apps/www/**, packages/core/** (or workflow_dispatch), builds both apps in one job, assembles their output into a single folder tree (site/playground/, site/* for www's root + /docs/* files), and publishes it with actions/upload-pages-artifact + actions/deploy-pages — the modern Actions-native Pages flow, not a gh-pages branch. Requires this repo's Settings → Pages → Source to be "GitHub Actions."
Base paths: PLAYGROUND_BASE=/<repo>/playground/ and WWW_BASE=/<repo>/ are both set as build-step env vars (read by apps/playground/vite.config.ts and apps/www/next.config.mjs respectively) — both need this, including www, even though www publishes at the site's content root. A GitHub Pages project site (this one — as opposed to a <user>.github.io user site) is served at https://<user>.github.io/<repo>/; that /<repo>/ segment is a real path prefix on the github.io domain, not the domain root. Framework defaults that assume the domain root make every asset reference absolute against the domain root, 404ing everything under it — found live the first time apps/web (the previous root app) deployed end-to-end (index.html loaded, every asset 404'd, white screen); apps/www avoids the same trap via Next's basePath config (trimmed from WWW_BASE's trailing slash — Next wants none — in apps/www/next.config.mjs), which also rewrites assetPrefix automatically. Local testing via next start/serving out/ directly doesn't catch a missing base path either: that serves at the actual domain root of localhost, where no base path is correct, which is exactly why this class of bug is easy to ship unnoticed. Playground's base-awareness also needs, and already has:
src/main.tsx's<BrowserRouter basename={import.meta.env.BASE_URL}>— without it, the router'spath="/"route wouldn't match under/playground/and every load would bounce through thepath="*"redirect back to the site root, escaping the subpath.src/data/widget-examples.ts'swidgetHtmlPathForId— prefixeswidget.html?id=…withimport.meta.env.BASE_URLinstead of a hardcoded/. This URL is both the live-preview iframesrcand the documented "prod URL" you paste into a real Grist doc, so a bare/would 404 under a subpath deploy.
No release/dev channel for either — every push to main touching their paths overwrites the whole deployment (both apps rebuild together; there is no per-app partial deploy). concurrency: { group: "pages", cancel-in-progress: false } — GitHub's own documented pattern for Actions-based Pages deploys, one deployment in flight at a time, none cancelled mid-flight.
apps/playground additionally deploys independently to Cloudflare Pages (demo.grist-widgets.com) via its own dashboard config, entirely unaffected by this workflow — see "Cloudflare Pages" below. apps/docs deploys only to Cloudflare Pages now (grist-widgets.com) — no longer part of this repo's GitHub Pages workflow at all. apps/www is GitHub-Pages-only, no Cloudflare target.
Transient Pages backend flakiness — timeout + auto-retry
Found live, repeatedly, unrelated to anything in this workflow: the actions/deploy-pages step sometimes sits in deployment_in_progress for its full internal timeout and then self-aborts (conclusion: cancelled), or occasionally fails outright — backend GitHub Pages flakiness on this repo, not a race (confirmed via the Actions API: exactly one deployment in flight, build already succeeded). Two things address this without needing a human to notice and manually re-trigger every time:
- The
deploystep setstimeout: 900000(15 min, up from thedeploy-pagesdefault of 10) — gives a genuinely-slow-but-would-have- succeeded deployment more room before being killed. .github/workflows/retry-deploy-pages.yml, a separateworkflow_run- triggered workflow, re-runs just the failed/cancelled jobs ofdeploy-apps.yml(via the "re-run failed jobs" API — reuses the already-uploaded artifact, no rebuild) exactly once, gated onrun_attempt. Still cancelled/failed after that retry (attempt 2)? It stops there and leaves it alone — two failures in a row stops looking transient and needs a human.
Why one workflow, not three
This used to be three independent workflows (deploy-docs.yml, deploy-playground.yml, deploy-web.yml), each pushing its own commit to a shared gh-pages branch, serialized against each other only at the git-push level (concurrency.group: pages-gh-pages + a re-clone-and-retry publish step). That worked for the git side, but every successful push to that branch independently triggered GitHub's own hidden native pages-build-deployment workflow (not something in this repo's .github/workflows/ — GitHub injects it automatically whenever Pages Source is "Deploy from a branch"). A merge touching multiple apps' trigger paths at once (routine — e.g. a lockfile change) fired all three workflows in quick succession, each pushing its own commit, each spawning its own native deployment; GitHub Pages processes one deployment at a time, so the later ones queued behind the earlier ones and occasionally hit actions/deploy-pages's own ~10-minute timeout waiting. Consolidating to one workflow, one artifact, one deploy-pages call removes the race by construction — there is structurally only ever one deployment triggered per merge.
This consolidation deliberately does not cover templates/grist-widget-template-vite. Its GitHub-Pages-inside-this-repo showcase (formerly /template/ on this same gh-pages branch, via the now removed deploy-template-showcase.yml) has been retired entirely — see "Template showcase (removed)" below. The template's live preview now lives only on the external grist-widget-template repo, via template-canary.yml (unchanged, unrelated to Pages Source or this workflow — see apps/docs/files/releasing.md).
Template showcase (removed)
templates/grist-widget-template-vite used to have a second, independent GitHub Pages presence inside this monorepo: its own copy, deployed to /template/ on this repo's gh-pages branch (deploy-template-showcase.yml + scripts/deploy/template-showcase.mjs), alongside a dev/template-showcase branch for a live-preview /template/dev/ channel. Both the workflow and the script have been deleted, along with their smoke test (smoke-template-showcase-deploy.yml + scripts/smoke/template-showcase-deploy.sh).
This does not affect the template's own bundled deploy pipeline — templates/grist-widget-template-vite/.github/workflows/deploy.yml + scripts/deploy.mjs — which is embedded verbatim into every scaffolded widget repo via create-grist-widget and deploys that repo's own, separate GitHub Pages site. That pipeline is untouched, was never deployed inside grist-widget-sdk, and this repo's own Settings → Pages has no bearing on it.
The template's only remaining live preview is the external github.com/ArthurBlanchon/grist-widget-template repo, maintained by template-canary.yml (dev + canary/latest branches, manual PR to promote to that repo's own main — see apps/docs/files/releasing.md), which is completely unchanged by any of this.
Cloudflare Pages (3 projects)
Recommended setup is one Pages project per target:
docsprojectwidgetsprojecttemplatesproject
The safest default is:
- set Root directory to repository root
- use
pnpm --filter ...commands with real workspace names - set Build output directory to a path relative to repository root
docs project
- Build command:
pnpm install --frozen-lockfile && pnpm build:docs- Build output directory:
apps/docs/.vitepress/distwidgets project
Use this command even if widgets currently builds without importing core directly. It prevents future breakages once widgets starts using grist-widget-sdk.
- Build command:
pnpm install --frozen-lockfile && pnpm build:playground- Build output directory:
apps/playground/disttemplates project
Template code imports grist-widget-sdk, so core must be built first.
- Build command:
pnpm install --frozen-lockfile && pnpm --filter grist-widget-sdk build && pnpm --filter grist-widget-template-vite build- Build output directory:
templates/grist-widget-template-vite/distCommon Cloudflare errors and fixes
No projects matched the filters: the--filtervalue does not matchpackage.json:name.Output directory ".../.../dist" not found: Root directory + output path are duplicated. If Root is alreadyapps/playground, output should bedist(notapps/playground/dist).Cannot find module 'grist-widget-sdk': buildgrist-widget-sdkfirst in the same command chain.No Wrangler configuration file foundandNo functions dir at /functions found: expected for static Pages deployments.
Build commands (workspace-safe)
From repository root:
pnpm build # all workspaces except docs
pnpm build:docs # VitePress onlyPer workspace:
pnpm build:docs
pnpm build:playgroundBuild ordering guarantees
turbo.json uses:
build.dependsOn: ["^build"]test.dependsOn: ["^build"]
This ensures dependent apps build against freshly built upstream workspace packages such as grist-widget-sdk.
Static hosting notes
apps/playground/public/_redirectsenables SPA fallback routing for static hosting.apps/docsoutputs static docs from VitePress to.vitepress/dist.apps/wwwoutputs Next's static export toout/; the "Assemble site" step indeploy-apps.ymlwrites a.nojekyllfile at the assembled site's root so GitHub Pages' default Jekyll processing doesn't drop the_next/static asset folder (Jekyll ignores underscore-prefixed directories by default — everything else about the deploy can be correct and this alone still 404s every asset).
Agent checklist before touching deploy config
- Confirm workspace/package names still match commands.
- Confirm output paths used by hosting provider still match build output.
- Confirm no deploy instructions reference removed directories.