Skip to content

feat: replace Spain-only company region with a global country #443

feat: replace Spain-only company region with a global country

feat: replace Spain-only company region with a global country #443

Workflow file for this run

name: CI
on:
pull_request:
branches: [main]
# Run on ready-for-review too (not a default type), so a draft flipped to
# ready is checked immediately instead of waiting for the next push.
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Least privilege: CI only reads the repo. No token write scope.
permissions:
contents: read
jobs:
ci:
name: Lint, Build, Type Check, Test
# A draft PR is work in progress, so skip CI until it's marked ready.
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
# The integration phase runs the whole suite against a remote Neon branch,
# which is ~15x slower per query than local Postgres; it already sat near the
# old 15-minute cap (14m32s–14m52s on passing runs) and grows with the suite,
# so give it headroom rather than tip over the ceiling on a slow run.
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
cache: "pnpm"
# A prod deploy is a rolling update — the old instance keeps serving while
# the new one boots — so a migration that drops/renames a column the old
# version still reads breaks it mid-rollout. Fail a PR that adds a
# non-backward-compatible migration unless it carries an `expand-contract:`
# marker (docs/runbooks.md). Runs before install — needs only git + node.
- name: Migration safety (expand-contract)
run: |
git fetch --no-tags --depth=1 origin "$GITHUB_BASE_REF"
node scripts/check-migration-safety.mjs "origin/$GITHUB_BASE_REF"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Secret scan (secretlint)
run: pnpm lint-secrets
- name: Check (Biome + dprint)
run: pnpm check
- name: Check dependencies
run: pnpm check-deps
- name: Build
run: pnpm build
# The TanStack Start Vite plugin (run by Build) is the single source of
# truth for apps/internal/src/routeTree.gen.ts. If Build regenerated a
# different tree, the committed one is stale — fail so it gets committed.
- name: Verify route tree is up to date
run: |
git diff --exit-code -- apps/internal/src/routeTree.gen.ts \
|| { echo "::error::routeTree.gen.ts is stale — run 'pnpm build' and commit the regenerated file."; exit 1; }
- name: Type check
run: pnpm check-types
# Fast unit suite first — no external services needed. Any test that
# touches Postgres lives under `*.integration.test.ts` and runs in
# the integration phase below against an ephemeral Neon branch.
- name: Test (unit)
run: pnpm test
# Integration phase: ephemeral Neon branch, MinIO, seed, and the
# integration suite. CI runs only on pull requests, so this phase runs
# on every CI run.
#
# Spin up an ephemeral Neon branch for the integration phase.
# Outputs are job-scoped + secret-marked per Neon's docs, so
# migrations + seed + integration tests all live in this job.
# expires_at is a safety net: if the runner is killed before the
# delete step runs, Neon auto-cleans the branch after 6 h.
- name: Compute Neon branch expiry (6h)
id: neon-expiry
run: echo "iso=$(date -u -d '+6 hours' +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Create Neon branch
id: neon-branch
uses: neondatabase/create-branch-action@fb620d43d4c565abaf088b848a4e28e5c4ea4d9c # 6.3.1
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
branch_name: ci-${{ github.run_id }}-${{ github.run_attempt }}
api_key: ${{ secrets.NEON_API_KEY }}
expires_at: ${{ steps.neon-expiry.outputs.iso }}
# Our 0001 migration creates app_user + app_service alongside
# neondb_owner, so the action's "infer the role" path fails with
# "Multiple roles found". Pin neondb_owner — it owns the schema
# and integration tests SET LOCAL ROLE to switch into app_user.
role: neondb_owner
- name: Apply migrations to CI branch
env:
DATABASE_URL: ${{ steps.neon-branch.outputs.db_url }}
run: pnpm db:migrate
# Spin up MinIO (S3-compatible storage) via the same docker-compose
# file dev uses. `storage-init` is a one-shot mc container that
# waits for storage to be reachable and `mb --ignore-existing` the
# bucket; `--wait` blocks until both services are healthy or, in
# storage-init's case, have exited 0.
- name: Start MinIO via docker compose
run: docker compose -f docker/docker-compose.yml up -d --wait storage storage-init
# Seed fixtures (notably the `taller` / `restaurant` orgs) that
# several integration tests rely on. EMAIL_CREDENTIAL_KEY is base64
# of 32 zero bytes — fine for a branch that gets deleted at
# end-of-run. STORAGE_* points at the MinIO container started
# above; the seedDemoEmails stage PUTs raw RFC822 bytes there.
- name: Seed CI branch
env:
DATABASE_URL: ${{ steps.neon-branch.outputs.db_url }}
BETTER_AUTH_SECRET: ci-only-not-prod-ci-only-not-prod-ci
EMAIL_CREDENTIAL_KEY: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
STORAGE_ENDPOINT: http://localhost:9000
STORAGE_REGION: us-east-1
STORAGE_ACCESS_KEY_ID: batuda
STORAGE_SECRET_ACCESS_KEY: batuda-secret
STORAGE_BUCKET: batuda-assets
EMAIL_PROVIDER: local-inbox
GEOCODER_PROVIDER: nominatim
run: pnpm cli seed
# --concurrency=1 forces the workspaces' test:integration tasks to
# run one at a time. multi-org-isolation (server) TRUNCATEs the
# email tables in beforeAll; if mail-worker's threading runs in
# parallel, its INSERT loses the race to that TRUNCATE. Within each
# workspace, vitest's fileParallelism: false already serializes
# files. The proper fix (per-test transactions in multi-org) is
# tracked as a follow-up CI-perf slice.
- name: Test (integration)
env:
DATABASE_URL: ${{ steps.neon-branch.outputs.db_url }}
BETTER_AUTH_SECRET: ci-only-not-prod-ci-only-not-prod-ci
EMAIL_CREDENTIAL_KEY: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
STORAGE_ENDPOINT: http://localhost:9000
STORAGE_REGION: us-east-1
STORAGE_ACCESS_KEY_ID: batuda
STORAGE_SECRET_ACCESS_KEY: batuda-secret
STORAGE_BUCKET: batuda-assets
run: pnpm exec turbo test:integration --concurrency=1
# Boot the built server against the REAL config.production.json + the
# deploy workflow's exact `-e` secret list (dummy values), so a release
# where the baked config needs a key the deploy never injects fails the
# build instead of crashing on boot and taking prod down (2026-07-11).
# Reuses the Neon branch + MinIO the integration job already stood up.
- name: Test (deploy boot parity)
env:
DATABASE_URL: ${{ steps.neon-branch.outputs.db_url }}
STORAGE_ENDPOINT: http://localhost:9000
STORAGE_REGION: us-east-1
STORAGE_ACCESS_KEY_ID: batuda
STORAGE_SECRET_ACCESS_KEY: batuda-secret
STORAGE_BUCKET: batuda-assets
run: pnpm --filter @batuda/server exec vitest run src/main.boot.test.ts --config vitest.boot.config.ts
# Cleanup runs even on test failure; only skipped if the branch was
# never created (e.g. earlier step failed).
- name: Delete Neon branch
if: always() && steps.neon-branch.outcome == 'success'
uses: neondatabase/delete-branch-action@4468d825d5a88ef4012f1705a82f02ec3072f776 # v3.2.1
with:
project_id: ${{ vars.NEON_PROJECT_ID }}
branch: ci-${{ github.run_id }}-${{ github.run_attempt }}
api_key: ${{ secrets.NEON_API_KEY }}