Skip to content

Latest commit

 

History

History
222 lines (186 loc) · 13.8 KB

preview-deploys.md

File metadata and controls

222 lines (186 loc) · 13.8 KB

App Preview Deploys

Official Worker Previews

GitHub Actions builds each Nx app and runs wrangler preview --name pr-<number> against its existing Worker. It no longer creates a separate Worker per PR.

Project Worker Stable Preview hostname
web sketchi-web pr-<number>-sketchi-web.<account>.workers.dev
playground sketchi-studio pr-<number>-sketchi-studio.<account>.workers.dev
icons sketchi-icons pr-<number>-sketchi-icons.<account>.workers.dev
excalidraw sketchi-excalidraw pr-<number>-sketchi-excalidraw.<account>.workers.dev

Cloudflare's Worker Previews provide branch-specific bindings, variables, secrets, and observability under an existing Worker. They are separate from production versions/deployments, so they cannot accidentally become a production version through wrangler versions deploy. Version URLs created by wrangler versions upload --preview-alias use version resources and are not branch-isolated environments. We use Previews, not version aliases. Workers Builds supports Previews too, but this repo retains GitHub Actions and Nx as its build system.

Data isolation

Each app declares a previews block in its checked-in Wrangler config. Production settings stay at the top level. Previews do not inherit production variables or bindings. Studio's Preview R2 bucket and both Pipeline streams are the existing preview resources listed below; no data resources are created by this workflow. AI, Browser Run, and Worker Loader bindings are configured explicitly where needed. Assets and compatibility settings remain at the top level as required by Wrangler.

The workflow uses --ignore-base-config to exclude dashboard Preview base settings when a PR Preview is created. It does not import production secrets or upload secrets. These apps use configured API bindings without Worker secrets. If a future feature requires secrets, provision preview-only credentials through a separately reviewed change. Do not copy production secrets into the Preview base.

scripts/01-validate-preview-config.mjs checks the generated build config before upload: Worker identity, explicit runtime bindings, and that previews binds every production R2 bucket and Pipeline stream binding to a non-production target (the R2 target must match the top-level preview_bucket_name). Each binding entry may only use reviewed keys (Pipelines: binding, stream, remote; R2: binding, bucket_name, remote, jurisdiction), because Wrangler also forwards the legacy Pipelines pipeline key. Any other previews field, such as KV, D1, or service bindings, fails until its isolation is reviewed. Cloudflare resources and isolation explains that R2 and Pipelines isolation depends on binding separate resources; service bindings currently call the target Worker's production deployment.

Workflow and comments

Previews are opt-in. A same-repository pull request to main with the preview label deploys the apps it affects (nx show projects --affected between the PR's merge base and head) on every push; a change to the preview tooling itself (app-preview.yml, the shared setup action, scripts/0*-*.mjs, scripts/lib/deploy/) deploys all five. Unlabelled PRs deploy nothing, and adding another label runs nothing. Forks are excluded. Closing a same-repository PR deletes all five Previews when one can exist: the PR carries the preview label or a preview comment, however the Preview was deployed (including before previews were opt-in). Closing a PR with neither runs no cleanup jobs.

Manual dispatch takes an open same-repository PR number, including a stacked PR whose base is not main, and a project choice (affected, all, or one app). It resolves the PR head SHA before checkout; the same SHA appears in deploy metadata and comments. It also adds the preview label before anything uploads, so closing the PR cleans up even if no Preview comment exists yet, and later pushes keep the PR's affected Previews current.

  • The shared mise setup uses the repository's exact Node/pnpm pins and a frozen lockfile.
  • Each build writes assets to dist/apps/<project>/client and the deploy snapshot to dist/apps/<project>/server/wrangler.json. There is no transformed preview config.
  • wrangler preview writes a preview entry to its structured output file (WRANGLER_OUTPUT_FILE_PATH) with the stable Preview URL and an immutable deployment URL. The URL reader checks the Worker and Preview name and comments only the stable workers.dev URL. Missing or disabled Preview URLs fail the job.
  • One owned sticky bot comment per app reports status, commit, URL, and workflow run.
  • Web receives sibling official Preview URLs through --var so its Icons and Playground links stay on the same PR when the same run deploys them; otherwise they point at production. These overrides affect only the Preview.

web, playground, and icons are public product surfaces. excalidraw remains internal and are not linked from public navigation.

Pipeline-bound Playground uploads share cloudflare-pipeline-bound-upload with production. queue: max queues pending jobs without cancelling active uploads; other apps retain their per-app/per-PR concurrency policies.

Preview and production uploads run through 07-wrangler-retry.mjs. It retries only Cloudflare's assets-upload-session failure with code 10013, twice, after 30s and then 60s (cloudflare/workers-sdk#16447). Cloudflare returns it before any asset is uploaded. Every other error fails on the first attempt.

Required configuration

  • GitHub staging: CLOUDFLARE_ACCOUNT_ID variable or secret and CLOUDFLARE_API_TOKEN secret with Workers edit/deploy and bound-resource access.
  • GitHub production: separate production credentials for production deploys.
  • CHROMATIC_PROJECT_TOKEN: staging secret for the CI Storybook gate.

Infisical project sketchi, path /github, is the source of GitHub environment secrets: staging maps to GitHub staging; prod maps to GitHub production. Do not combine both environments in repository-wide secrets. GRAPHITE_TOKEN is optional and is not required by these workflows.

The existing Workers must have workers.dev Preview URLs enabled. If disabled, apply that setting through the normal production/operator workflow, not by publishing PR code with wrangler deploy. Missing Cloudflare credentials produce an unconfigured comment rather than a successful preview claim.

Cleanup and legacy Workers

Closing a PR runs cleanup-preview. scripts/04-delete-preview.mjs deletes Preview pr-<number> from each of the five Workers through the Cloudflare Previews API; a missing Preview (HTTP 404) counts as already deleted. A successful delete can leave the Preview hostname serving the deleted code (cloudflare/workers-sdk#15945), so the script then checks the Preview URL every 10 seconds for up to 2 minutes until Cloudflare answers with its no-Preview 404 (x-preview-user-error: true). Only then is the sticky comment marked deleted. If the URL still serves, the comment is marked deletion-pending with the reachable URL and the job fails, so the leak stays visible. The job shares deploy-preview's concurrency group. Once a deploy job holds that lock, it re-checks the PR state right before wrangler preview and skips the upload if the PR has closed, so a deploy that was queued at close time doesn't recreate the Preview. A PR that closes during the upload itself is still handled: its cleanup job waits for the same lock and deletes the Preview afterwards. Cloudflare also evicts the least recently deployed Preview at its limit (100 Free / 500 Paid per Worker; 100 deployments per Preview).

Legacy sketchi-*-pr-* Workers from the previous per-PR Worker flow are untouched by this migration. Remove them separately; this workflow never deletes a Worker.

Operational scripts

  • 00-resolve-worker-app.mjs: resolve and validate project/Worker identity.
  • 01-validate-preview-config.mjs: validate the built Preview settings and PR name.
  • 02-extract-preview-url.mjs: read the Preview entry from Wrangler's output file; production still parses its deploy log.
  • 03-upsert-preview-comment.mjs: update only the owned, anchored bot comment.
  • 04-delete-preview.mjs: delete a PR's Preview and confirm its URL stopped serving.
  • 05-prepare-production-domain-deploy.mjs: prepare an explicitly requested production domain config.
  • 07-wrangler-retry.mjs: run a Wrangler upload, retrying only the 10013 assets-upload-session error.
  • 09-select-production-projects.mjs: choose the production Workers a push or dispatch deploys.

Production Worker Deploys

The app-production-deploy workflow deploys production Workers without assigning final custom domains. A push to main deploys only the Workers whose build it reaches, compared with the previous main commit. That includes changes in a shared package the app depends on. The check uses Nx's task-input selection (NX_LEGACY_AFFECTED=false nx show projects --affected --withTarget=build), so a change only to tests, stories, docs, CI or repository scripts no build imports deploys nothing. A repository file a Worker's Vite config imports, such as scripts/lib/excalidraw-fonts.mjs, is listed in that app's build inputs and in the workflow's push filter. A contract test fails when one is missing from either. When the previous commit is unknown, the push deploys all five. A manual dispatch deploys all five, or the one app it names. A Worker whose deploy failed stays on its last version until a later push reaches it or someone dispatches it. Production deploys also pass --no-x-provision; Workers may bind existing resources, but CI deploys do not create or discover storage.

Those deploys keep workers_dev enabled so the app can be verified from Cloudflare-owned workers.dev URLs before any DNS or registrar cutover. Production deploy summaries include the same public/internal route policy and the custom domains that would be attached by a manual domain dispatch. Internal apps report no custom domains.

The Studio Worker binds Code Mode artifacts to R2. Studio binds its spans stream, which also carries Code Mode usage:

Surface Binding Remote target
Studio Previews SKETCHI_ARTIFACTS sketchi-studio-codemode-artifacts-preview
Studio production Worker SKETCHI_ARTIFACTS sketchi-studio-codemode-artifacts-production
Studio Previews TELEMETRY_SPANS d3066c58106b4facbc146e9188e383b1
Studio production TELEMETRY_SPANS e118fdd69a2548258389e82fb1ade1d6

TELEMETRY_SPANS is not remote, so local development writes to a local stream rather than production's. Every Preview shares the preview spans stream; the url.full attribute of playground.request names the Preview host. The spans path is described in architecture, and pnpm db --preview queries what a Preview recorded.

The checked-in previews block binds Studio Previews directly to the preview bucket and preview spans stream. There is no generated resource rewrite. Production deploys keep production buckets and the production stream. All Worker-bound buckets and streams must exist before their Workers deploy. The downstream R2 Data Catalog sinks are long-lived Cloudflare Pipeline resources, not Worker bindings. They write into sketchi-telemetry-production and sketchi-telemetry-preview; pnpm telemetry:resources prints their map. Preview resources are named explicitly and must already exist; deploys do not provision data resources. The CI token does not need R2 object read access just to deploy.

To check that a Preview records a call, send it with a unique x-sketchi-run-id and wait about two minutes for the sink to roll. Then pnpm db --preview -c "FROM usage_events WHERE run_id = '<id>'" returns its usage row, and span_tree on its trace_id returns its spans.

Wrangler accepts Pipeline stream names in local dry-runs, but the deploy API requires stream IDs for Worker bindings. Keep apps/playground/wrangler.jsonc on the production stream IDs and keep previews.pipelines on the preview stream IDs. The validator rejects Previews bound to production targets.

Custom domains are a post-merge operator action. The production workflow writes a generated dist/apps/<project>/server/wrangler.domains.json config from scripts/05-prepare-production-domain-deploy.mjs and deploys that route-bearing config only when explicitly dispatched with domain_action=attach. Follow Production domain cutover; never attach domains from a pull request or before the Cloudflare zone is active.

When domain_action=attach, the production domain helper attaches playground.sketchi.app to the playground project while retaining the sketchi-studio Worker service. Authenticated Studio is not exposed, even though the Playground host has /projects and persisted /diagrams/:diagramId route foundations.

The standalone Excalidraw workspace should remain unlinked from public navigation. Do not attach a public excalidraw.sketchi.app product route unless that product-route decision is explicitly reopened.