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
| Workflow | File | Trigger |
|---|---|---|
| CI | .github/workflows/ci.yml | Push to main + every PR to main |
| Deploy Preview | .github/workflows/deploy-preview.yml | Every PR to main |
| Deploy Dev | .github/workflows/deploy-dev.yml | After CI succeeds on main |
| Deploy Production | .github/workflows/deploy-prod.yml | Stable version tags (v1.2.3) |
| Mobile Build | .github/workflows/mobile-build.yml | Manual 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):
- 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, thebundle:checktransfer gate pluswiki: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-pushare held in lockstep bybun run hook:parity(tools/ci-hook-parity.spec.ts, run by both sides): arun:command added to thetest/docsjobs must land in the hook too, or in the spec'sCI_ONLYallowlist with a reason — drift fails everywhere. Thee2ejob is the one whole-job exemption. Test parallelism is deliberately all-defaults except one knob: every vitest config pinsmaxWorkers: '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--parallelnumbers anywhere. - docs — the currency gates for generated artifacts, which the affected
vite:testsweep doesn't cover:nx test stretched-types,types:erd:test,wiki:test,stories:test, andfirebase:remote-config:test, thentypes:erd:check,wiki:check(stale generated wiki pages or broken wiki links — see Wiki Publishing),stories:check, andfirebase:remote-config:check(fails if the committed ERD, wiki, storycatalog.json, Remote Config templates, or generated flag table is stale — regenerate with the matching build/generate script and commit). - e2e (needs test) —
bunx nx e2e stretched-e2e. This starts a dev server and runs the Cypress suite viatools/cypress-e2e.ts; emulator-only specs skip becauseCYPRESS_EMULATEDis 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
| Project | Use |
|---|---|
stretched-dev | Dev hosting + Functions, PR preview channels |
stretched-2c16a | Production |
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.