Skip to main content

CI CD and Deploys

See also: Testing · Secrets Management · Mobile App · Wiki Publishing

All pipelines live in .github/workflows/. Runners install deps with bun install --frozen-lockfile (Bun + Node 22).

Overview

WorkflowFileTrigger
CI.github/workflows/ci.ymlPush to main + every PR to main
Deploy Preview.github/workflows/deploy-preview.ymlEvery PR to main
Deploy Dev.github/workflows/deploy-dev.ymlAfter CI succeeds on main
Deploy Production.github/workflows/deploy-prod.ymlStable version tags (v1.2.3)
Mobile Build.github/workflows/mobile-build.ymlManual only (workflow_dispatch)

(There is no wiki-publish workflow — wiki/ is the wiki, gated by the docs job; see Wiki Publishing.)

CI (ci.yml)

Three jobs, concurrency-grouped per ref (a new push cancels the in-flight run):

  1. test — prettier --check, nx affected --target=lint, nx affected --target=vite:test --coverage, Firebase Functions tests (bun run test:functions:coverage — coverage on, so the pinned thresholds gate), a production build of the web app, the bundle:check transfer gate plus wiki:bundle:check (the generated Production Bundle page must match the build that just ran), a Storybook build, and the Storybook interaction tests. CI's gate jobs and .husky/pre-push are held in lockstep by bun run hook:parity (tools/ci-hook-parity.spec.ts, run by both sides): a run: command added to the test/docs jobs must land in the hook too, or in the spec's CI_ONLY allowlist with a reason — drift fails everywhere. The e2e job is the one whole-job exemption. Test parallelism is deliberately all-defaults except one knob: every vitest config pins maxWorkers: '100%' (uncapped from '50%' 2026-07-23 per Harlo — use every core and let nx/the OS arbitrate contention). The earlier 2026-07-19 benchmark that favored the 50% cap (~10% on multi-project sweeps) was measured on a single 12-core Intel box; the dev fleet now spans ARM machines and possibly a 128-core box, so that number doesn't generalize — re-benchmark on the machine in question before ever re-capping. nx task parallelism stays at its default, and Playwright's own default (50% of cores) is already machine-derived. Percentages only — don't add hardcoded worker counts or --parallel numbers anywhere.
  2. docs — the currency gates for generated artifacts, which the affected vite:test sweep doesn't cover: nx test stretched-types, types:erd:test, wiki:test, stories:test, and firebase:remote-config:test, then types:erd:check, wiki:check (stale generated wiki pages or broken wiki links — see Wiki Publishing), stories:check, and firebase:remote-config:check (fails if the committed ERD, wiki, story catalog.json, Remote Config templates, or generated flag table is stale — regenerate with the matching build/generate script and commit).
  3. e2e (needs test) — bunx nx e2e stretched-e2e. This starts a dev server and runs the Cypress suite via tools/cypress-e2e.ts; emulator-only specs skip because CYPRESS_EMULATED is not set (see Testing).

The same checks (minus e2e) also run locally in the .husky/pre-push hook, and the story catalog is auto-regenerated by .husky/pre-commit — see Tooling and Scripts.

Deploy Preview (deploy-preview.yml) — ephemeral PR environments

On every PR: builds the app (development config), Storybook, and the wiki site (bun run wiki:site), then deploys all three to Firebase Hosting preview channels on the stretched-dev project via FirebaseExtended/action-hosting-deploy — channel pr-<number>, targets app, storybook, and wiki (site stretched-dev-wiki; the wiki documents internals — unlisted-URL exposure accepted 2026-08-02, see Wiki Publishing), expiring after 1 day. The action comments the preview URLs on the PR.

Finally it runs the Cypress suite against the deployed preview URL: CYPRESS_BASE_URL=<details_url> bunx nx e2e stretched-e2e. tools/cypress-e2e.ts sees the external base URL and skips starting a dev server (see Testing).

Auth: secrets.FIREBASE_SERVICE_ACCOUNT_DEV (a service-account JSON in GitHub Actions secrets — distinct from runtime secrets, which live in Firebase Secret Manager; see Secrets Management).

Deploy Dev (deploy-dev.yml)

Runs automatically after a successful CI run on main (workflow_run, gated on conclusion == 'success'; non-cancelling deploy-dev concurrency group). Checks out the exact CI head SHA, builds the app (development), Functions, and Storybook, then:

firebase-tools deploy --only hosting:app,hosting:storybook,functions --project stretched-dev

authenticated via google-github-actions/auth with FIREBASE_SERVICE_ACCOUNT_DEV.

Deploy Production (deploy-prod.yml)

Runs when a stable semantic-version tag with the exact v<major>.<minor>.<patch> format (for example, v1.2.3) is pushed. GitHub's production environment must require approval, and a tag ruleset must protect matching release tags from unauthorized creation, updates, or deletion (tracked in NEXT-STEPS.md). The workflow rejects leading-zero, prerelease, and malformed versions; verifies that the current remote tag still targets the event commit and that the commit is on main; then builds the app and Functions with production configs and deploys hosting:app,functions to stretched-2c16a using secrets.FIREBASE_SERVICE_ACCOUNT. The qualified hosting:app target intentionally excludes Storybook, which is not built or deployed in production. After a successful deployment, it verifies the tag target again and idempotently creates the matching GitHub release with GitHub-generated notes summarizing merged pull requests and commits since the previous release.

Mobile Build (mobile-build.yml)

Manual only — never runs automatically, so runner minutes (macOS bills at 10×) are only spent on demand. Inputs: environment (dev/prod) and platform (android/ios/both).

  • android (ubuntu, JDK 21 Temurin): bun run mobile:build:<env>bunx cap sync android./gradlew assembleDebug → uploads the debug-signed APK as an artifact (14 days). No release keystore exists yet.
  • ios (macos): builds for the simulator with CODE_SIGNING_ALLOWED=NO — validates the native pipeline; a store-ready IPA needs Apple certs first. Uploads the simulator .app.

The same APK can be built locally without runner minutes via bun run mobile:apk:dev — see Mobile App.

Firebase projects

ProjectUse
stretched-devDev hosting + Functions, PR preview channels
stretched-2c16aProduction

Hosting targets (app, storybook) and the Functions source (dist/apps/firebase-functions) are defined in firebase.json. Their project-specific Hosting site mappings are committed in .firebaserc: dev maps both app and storybook, while production maps only app to the stretched-2c16a site.