teqo-backup/.github/workflows/ci-pr.yml
Francisco Solla b0d158cc87 fix(cloud): bootstrap Postgres nativo no Cursor Cloud (#59)
* fix(cloud): bootstrap Postgres nativo no Cursor Cloud

VMs Cloud não têm Docker (sem daemon/systemd; overlayfs falha em
docker compose). Substitui compose por ensure-postgres.sh (PG 16 via
apt, pg_ctlcluster) e corrige placeholders do .env (BLOB token no
formato vercel_blob_rw_* exigido pelo adapter).

Testado: cloud-setup.sh completa migrate + seed em teqo e teqo_test.

* fix(ci): define GH_REPO no job migration-lock

gh pr list falha sem checkout quando não há repositório git no runner
(fatal: not a git repository). GH_REPO aponta explicitamente para o repo.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-07-30 18:36:33 -03:00

339 lines
10 KiB
YAML

# PR gate: feature → stage (and stage → main promote PRs).
# Parallel jobs with `needs` only where a real dependency exists (Postgres for
# int/build/e2e). Build runs here (local service DB only) and NEVER in
# ci-stage.yml — `pnpm build` applies migrations to whatever DATABASE_URL it
# sees, so it is forbidden anywhere near stage/prod URLs.
#
# Test selection (OPS5): unit/int run `vitest --changed origin/$GITHUB_BASE_REF`
# and e2e runs the manifest-selected specs; a diff touching a high-risk path
# (schema, lockfile, test harness, seed manifest — see
# scripts/lib/test-affected-core.mjs) falls back to the FULL suites. Push to
# stage/main always runs everything (ci-stage.yml / ci.yml).
name: CI (PR)
on:
pull_request:
branches: [stage, main]
concurrency:
group: ci-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
DATABASE_URL: postgresql://teqo:teqo@localhost:5432/teqo_test
PAYLOAD_SECRET: test-only-secret-not-used-in-production
NEXT_PUBLIC_SITE_URL: http://localhost:3000
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
format:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm format:check
typecheck:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
knip:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm exec knip
cycles:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm check:cycles
unit:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Compute affected test scope
id: scope
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
result=$(node scripts/test-affected.mjs)
echo "scope: $(echo "$result" | jq -r .reason)"
echo "mode=$(echo "$result" | jq -r .mode)" >> "$GITHUB_OUTPUT"
- name: Unit tests (full suite)
if: steps.scope.outputs.mode == 'full'
run: pnpm test:unit
- name: Unit tests (changed since PR base)
if: steps.scope.outputs.mode == 'changed'
run: pnpm test:unit -- --changed origin/${{ github.base_ref }} --passWithNoTests
- name: Unit tests skipped
if: steps.scope.outputs.mode == 'none'
run: echo "No src/ or tests/ changes in this PR — nothing to run."
int:
runs-on: ubuntu-latest
timeout-minutes: 25
services:
postgres:
# Matches production Neon major (17) and docker-compose.yml.
image: postgres:17-alpine
env:
POSTGRES_USER: teqo
POSTGRES_PASSWORD: teqo
POSTGRES_DB: teqo_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U teqo"
--health-interval 5s
--health-timeout 5s
--health-retries 12
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Compute affected test scope
id: scope
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
result=$(node scripts/test-affected.mjs)
echo "scope: $(echo "$result" | jq -r .reason)"
echo "mode=$(echo "$result" | jq -r .mode)" >> "$GITHUB_OUTPUT"
- name: Migrate test database
if: steps.scope.outputs.mode != 'none'
run: pnpm migrate
- name: Seed minimal test database
if: steps.scope.outputs.mode != 'none'
run: pnpm db:seed:minimal
- name: Integration tests (full suite)
if: steps.scope.outputs.mode == 'full'
run: pnpm test:int
- name: Integration tests (changed since PR base)
if: steps.scope.outputs.mode == 'changed'
run: pnpm test:int -- --changed origin/${{ github.base_ref }} --passWithNoTests
- name: Integration tests skipped
if: steps.scope.outputs.mode == 'none'
run: echo "No src/ or tests/ changes in this PR — nothing to run."
build:
runs-on: ubuntu-latest
timeout-minutes: 25
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: teqo
POSTGRES_PASSWORD: teqo
POSTGRES_DB: teqo_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U teqo"
--health-interval 5s
--health-timeout 5s
--health-retries 12
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build (against the local service database)
run: pnpm build
e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: teqo
POSTGRES_PASSWORD: teqo
POSTGRES_DB: teqo_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U teqo"
--health-interval 5s
--health-timeout 5s
--health-retries 12
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Install Playwright chromium
run: pnpm exec playwright install --with-deps chromium
- name: Compute affected e2e scope
id: scope
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
result=$(node scripts/e2e-affected.mjs)
echo "scope: $(echo "$result" | jq -r .reason)"
echo "mode=$(echo "$result" | jq -r .mode)" >> "$GITHUB_OUTPUT"
echo "specs=$(echo "$result" | jq -r '[.specs[] | "tests/e2e/" + . + ".e2e.spec.ts"] | join(" ")')" >> "$GITHUB_OUTPUT"
- name: Migrate test database
if: steps.scope.outputs.mode != 'none'
run: pnpm migrate
- name: Seed minimal test database
if: steps.scope.outputs.mode != 'none'
run: pnpm db:seed:minimal
# CI e2e runs against the production build (see playwright.config.ts
# webServer.command); the dist dir must match the webServer env.
- name: Build for e2e
if: steps.scope.outputs.mode != 'none'
run: pnpm build
env:
NEXT_DIST_DIR: .next/e2e
- name: E2E tests (full suite)
if: steps.scope.outputs.mode == 'full'
run: pnpm test:e2e
- name: E2E tests (manifest-selected specs)
if: steps.scope.outputs.mode == 'selected'
run: pnpm test:e2e -- ${{ steps.scope.outputs.specs }}
- name: E2E tests skipped
if: steps.scope.outputs.mode == 'none'
run: echo "No e2e-relevant changes in this PR — nothing to run."
# Branch protection on `stage`/`main` still requires the legacy context name
# `checks` from the pre-parallel monolithic job. This rollup satisfies that
# contract without re-serializing the suite.
checks:
name: checks
runs-on: ubuntu-latest
needs:
- lint
- format
- typecheck
- knip
- cycles
- unit
- int
- build
- e2e
- migration-lock
if: always()
steps:
- name: All parallel checks green
env:
NEEDS: ${{ toJSON(needs) }}
run: |
set -euo pipefail
echo "$NEEDS" | jq -e '
to_entries
| all(.value.result == "success" or .value.result == "skipped")
'
migration-lock:
# At most ONE open PR may touch schema (migrations or generated types).
# Two in flight guarantees a broken rebase and a poisoned drizzle snapshot
# chain; the second PR must wait or rebase.
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
# gh pr list against the default GITHUB_TOKEN.
contents: read
pull-requests: read
steps:
# gh expands --json files via git, so it needs the repo checked out.
- uses: actions/checkout@v4
- name: Count open PRs touching schema files
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
count=$(gh pr list --state open --limit 100 --json number,files \
--jq '[.[] | select(.files != null and ([.files[].path | test("^(src/migrations/|src/payload-types.ts$|payload-types.ts$)")] | any))] | length')
echo "Open schema-touching PRs: $count"
if [ "$count" -gt 1 ]; then
echo "::error::More than one open PR touches src/migrations/ or payload-types.ts. Merge or close the other first."
exit 1
fi