Skip to content

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 deployedapps/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 workflowapps/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's path="/" route wouldn't match under /playground/ and every load would bounce through the path="*" redirect back to the site root, escaping the subpath.
  • src/data/widget-examples.ts's widgetHtmlPathForId — prefixes widget.html?id=… with import.meta.env.BASE_URL instead of a hardcoded /. This URL is both the live-preview iframe src and 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 deploy step sets timeout: 900000 (15 min, up from the deploy-pages default of 10) — gives a genuinely-slow-but-would-have- succeeded deployment more room before being killed.
  • .github/workflows/retry-deploy-pages.yml, a separate workflow_run- triggered workflow, re-runs just the failed/cancelled jobs of deploy-apps.yml (via the "re-run failed jobs" API — reuses the already-uploaded artifact, no rebuild) exactly once, gated on run_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 pipelinetemplates/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:

  • docs project
  • widgets project
  • templates project

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:
bash
pnpm install --frozen-lockfile && pnpm build:docs
  • Build output directory:
text
apps/docs/.vitepress/dist

widgets 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:
bash
pnpm install --frozen-lockfile && pnpm build:playground
  • Build output directory:
text
apps/playground/dist

templates project

Template code imports grist-widget-sdk, so core must be built first.

  • Build command:
bash
pnpm install --frozen-lockfile && pnpm --filter grist-widget-sdk build && pnpm --filter grist-widget-template-vite build
  • Build output directory:
text
templates/grist-widget-template-vite/dist

Common Cloudflare errors and fixes

  • No projects matched the filters: the --filter value does not match package.json:name.
  • Output directory ".../.../dist" not found: Root directory + output path are duplicated. If Root is already apps/playground, output should be dist (not apps/playground/dist).
  • Cannot find module 'grist-widget-sdk': build grist-widget-sdk first in the same command chain.
  • No Wrangler configuration file found and No functions dir at /functions found: expected for static Pages deployments.

Build commands (workspace-safe)

From repository root:

bash
pnpm build        # all workspaces except docs
pnpm build:docs   # VitePress only

Per workspace:

bash
pnpm build:docs
pnpm build:playground

Build 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/_redirects enables SPA fallback routing for static hosting.
  • apps/docs outputs static docs from VitePress to .vitepress/dist.
  • apps/www outputs Next's static export to out/; the "Assemble site" step in deploy-apps.yml writes a .nojekyll file 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.

Released under the ISC License.