feat: replace Spain-only company region with a global country #443
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |