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.
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.
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>/clientand the deploy snapshot todist/apps/<project>/server/wrangler.json. There is no transformed preview config. wrangler previewwrites apreviewentry 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
--varso 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.
- GitHub
staging:CLOUDFLARE_ACCOUNT_IDvariable or secret andCLOUDFLARE_API_TOKENsecret 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.
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.
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.
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.