Skip to main content

CI/CD and previews

Four workflows. The interesting one is preview.yml, which puts a testable app on your phone for every pull request.

What happens on a pull request

The preview stack

ComponentCommandIsolation
APIwrangler versions upload --env previewA per-version URL. Production traffic is never touched
Databasewrangler d1 migrations apply --env preview --remoteShared crag-topo-db-preview
iOS appeas update --branch pr-<n>A per-PR EAS branch
Docswrangler pages deploy --branch pr-<n>A per-PR Pages URL

Why versions upload and not deploy

wrangler versions upload publishes a version and hands back a preview URL without shifting any production traffic. There is no path by which a preview becomes production because someone passed the wrong flag.

The bit that makes it actually useful

env:
EXPO_PUBLIC_API_URL: ${{ steps.worker.outputs.url }}

The app update has this PR's Worker URL compiled into it. Scanning the QR code gives you a phone already talking to the backend built from the same commit — no tunnel, no LAN address, no editing a URL in a settings screen.

Where each piece is actually hosted

Worth being explicit, because it is not all one account:

Hosted by
Worker API, preview D1, docs siteCloudflare — your account
The app's JS bundle (EAS Update)Expou.expo.dev/<projectId>
Building the app binaryExpo (EAS Build), or your Mac
Running the workflowsGitHub Actions — hosts nothing itself

Expo is a third infrastructure dependency alongside Cloudflare, not something running inside it.

The health gate

Before commenting, the workflow polls /health up to ten times and asserts that the revision it reports equals the PR head SHA. A green comment therefore means the preview is genuinely up and serving the right commit — not that a deploy command exited zero.

If the health check never passes, the job fails and there is no comment. Do not merge a PR whose preview never came up.

Testing a PR from your phone

The comment on each PR carries a QR code. Point the camera at it and the app opens on that branch, already talking to that branch's API. Check the Profile tab if you are unsure — it shows the API URL and the build commit the running app is using, so a stale cached update is obvious.

The one-off you need first

Expo Go cannot do this. EAS Update needs expo-updates in the binary, and runtimeVersion in app.config.ts stops the app launching in Expo Go at all. You need a development build on the phone.

The build is a shell that can load any JS bundle you point it at; the QR code is a link to a bundle. Apple governs the shell, not what it loads — so QR codes work identically however the build was signed.

Free Apple ID (what we do)Developer Program (£79/yr)
Build itOn your Mac, over a cableYour Mac, or eas build in the cloud
Shell lifetime7 daysAbout a year
Scanning PR QR codes
Handing a build to someone else

The workflow: plug in once a week, rebuild, then scan PR QR codes from anywhere until it expires.

pnpm --filter @crag-topo/mobile exec expo run:ios --device --configuration Release

A couple of minutes, and pull requests rarely outlive a seven-day window anyway, so the expiry does not strand you mid-review. Paying gets rid of the weekly rebuild and lets you build without a Mac; neither is needed to review a PR from the sofa.

A new native module needs a new build, not just an update. Say so in the PR description when you add one.

What happens on main

deploy.yml, in a deliberate order:

Migrations before the Worker, so new code never meets an old schema. Worker before the app update, so the app never meets an API that has not shipped. Health check between them, so a failed deploy does not get an app update pointed at it.

concurrency: cancel-in-progress: false — two deploys queue rather than one killing the other mid-migration.

Forked pull requests

preview.yml is skipped:

if: github.event.pull_request.head.repo.full_name == github.repository

Forks cannot read secrets, and deploying them would let anyone with a fork write to the preview database. Forked PRs still get the full ci.yml.

The docs freshness check

docs-check.yml compares changed paths. If anything under apps/api, apps/mobile, packages/ or .github/workflows/ changed but nothing under docs/ did, it leaves a comment.

It comments; it does not fail. The author knows better than a path glob whether a change is genuinely documentation-neutral. If it is — a refactor, a dependency bump, a test — say so and carry on. The comment deletes itself once docs/ is touched.

Setup required before any of this works

Until the secrets exist, preview.yml fails on every PR. Full list and how to create them: Environments.

Quick version:

KindNameUsed by
SecretCLOUDFLARE_API_TOKENpreview, deploy
SecretCLOUDFLARE_ACCOUNT_IDpreview, deploy
SecretEXPO_TOKENpreview, deploy
VariableEAS_PROJECT_IDpreview, deploy
VariablePRODUCTION_API_URLdeploy

Plus, once, by hand:

wrangler d1 create crag-topo-db
wrangler d1 create crag-topo-db-preview
# put the returned ids into apps/api/wrangler.jsonc
wrangler pages project create crag-topo-docs

Known gaps

  • The preview database is shared across PRs. Two branches with conflicting migrations will fight. Per-PR databases are the fix if it becomes a problem — see ADR-0006.
  • Nothing is cleaned up. EAS branches and Worker versions accumulate after merge.
  • An EAS Update cannot deliver a native module change. If a PR adds one, reviewers need a new dev build, and there is no workflow for that — say it in the PR description.
  • Actions are pinned to major tags, not SHAs. expo/expo-github-action and marocchino/sticky-pull-request-comment run in a job that holds deploy credentials.