mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
- ci-pr.yml: infra e2e (cache/install/build-for-e2e + DB prep) só quando selected — diff high-risk não queima minutos à toa; comentários corrigidos - engineering-standards.mdc + TESTING.md: política OPS72 (era OPS59/ci.yml stale) - execution-pipeline/agent-work-issue: mecanismo real da #72 (CI roda famílias em paralelo, não sequencial) + ressalva do diff high-risk no pool - changelog/impl plan: verificação verdadeira (suíte full local + triagem dos flakes)
226 lines
9.7 KiB
YAML
226 lines
9.7 KiB
YAML
# PR gate: feature → main (OPS71 — GitHub Actions). SINGLE sequential job
|
||
# (OPS62, X1): one container, one checkout, one pnpm install, one runner slot.
|
||
# Fail-fast is STRUCTURAL — a failed step skips every step after it, the job
|
||
# fails and the run fails. GitHub-native `concurrency` cancels the in-flight
|
||
# run when the PR gets a new push (the Forgejo era had no cancel API; here it
|
||
# is free and matches the OPS62 "one slot per PR" spirit).
|
||
#
|
||
# The job id `checks` preserves the `CI (PR) / checks` check-run that the
|
||
# branch protection rule and the native auto-merge rely on (OPS71): GitHub
|
||
# names it <workflow name> / <job name> — keep both literals frozen. GitHub's
|
||
# auto-merge is server-side: it only merges with every required check green,
|
||
# so the OPS64 "rollup can lie" defense is structural here (single job posts
|
||
# one honest check-run; no matrix, no shards).
|
||
#
|
||
# Job-level skips come from the `scope` classifier (scripts/ci-scope.mjs) run
|
||
# as a step; its outputs drive `if:` on the heavy steps (code/build/test:
|
||
# none/changed/full; e2e: none/selected/full). e2e policy (OPS72): the PR
|
||
# runs ONLY the blast radius — the specs the classifier detected as touched
|
||
# (mode `selected`), never `full`. A high-risk diff (schema/lockfile/test
|
||
# harness) classifies as `full` → the PR simply runs no e2e: the local agent
|
||
# mirror (`pnpm test:e2e:affected`) runs full in that same case, and the
|
||
# deploy `verify` job runs the full suite before publishing.
|
||
#
|
||
# Schema migrations are NOT serialized across PRs (migration-lock removed
|
||
# 2026-08-12): a stale chain is caught by `pnpm migrate` + `test:int` on every
|
||
# PR, incl. campaignMigrationReconciliation.
|
||
#
|
||
# Fork-PRs are excluded at job level (`if:` same-repo): the repo is public and
|
||
# a fork PR would burn hosted minutes running arbitrary code. Same guard class
|
||
# as the RCE guard on the post-merge workflows.
|
||
#
|
||
# Postgres services (OPS71 — GitHub hosted): jobs run directly on the runner
|
||
# VM (NOT inside the job-container network of the Forgejo era), so services
|
||
# are reached at localhost on PUBLISHED host ports. Each hosted run is its own
|
||
# VM, so fixed ports never collide across runs. Two isolated services — int
|
||
# on 5432 and build+e2e on 5433 — keep e2e from inheriting int fixture
|
||
# residue.
|
||
name: CI (PR)
|
||
|
||
on:
|
||
pull_request:
|
||
branches: [main]
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
concurrency:
|
||
group: ci-pr-${{ github.head_ref }}
|
||
cancel-in-progress: true
|
||
|
||
env:
|
||
PAYLOAD_SECRET: test-only-secret-not-used-in-production
|
||
NEXT_PUBLIC_SITE_URL: http://localhost:3000
|
||
|
||
jobs:
|
||
checks:
|
||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 50
|
||
services:
|
||
postgres-int:
|
||
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
|
||
postgres-build:
|
||
image: postgres:17-alpine
|
||
env:
|
||
POSTGRES_USER: teqo
|
||
POSTGRES_PASSWORD: teqo
|
||
POSTGRES_DB: teqo_test
|
||
ports:
|
||
- 5433:5432
|
||
options: >-
|
||
--health-cmd "pg_isready -U teqo"
|
||
--health-interval 5s
|
||
--health-timeout 5s
|
||
--health-retries 12
|
||
steps:
|
||
- uses: actions/checkout@v5
|
||
with:
|
||
fetch-depth: 0
|
||
- uses: pnpm/action-setup@v6
|
||
- uses: actions/setup-node@v5
|
||
with:
|
||
node-version: 24
|
||
cache: pnpm
|
||
- run: pnpm install --frozen-lockfile
|
||
|
||
- name: Classify PR blast radius
|
||
id: scope
|
||
env:
|
||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||
run: |
|
||
set -euo pipefail
|
||
result=$(node scripts/ci-scope.mjs)
|
||
echo "scope: $(echo "$result" | jq -c '{code:.code.mode,build:.build.mode,test:.test.mode,e2e:.e2e.mode,reason:{code:.code.reason,build:.build.reason,test:.test.reason,e2e:.e2e.reason}}')"
|
||
echo "code_mode=$(echo "$result" | jq -r .code.mode)" >> "$GITHUB_OUTPUT"
|
||
echo "build_mode=$(echo "$result" | jq -r .build.mode)" >> "$GITHUB_OUTPUT"
|
||
echo "test_mode=$(echo "$result" | jq -r .test.mode)" >> "$GITHUB_OUTPUT"
|
||
echo "e2e_mode=$(echo "$result" | jq -r .e2e.mode)" >> "$GITHUB_OUTPUT"
|
||
echo "e2e_specs=$(echo "$result" | jq -r '[.e2e.specs[] | "tests/e2e/" + . + ".e2e.spec.ts"] | join(" ")')" >> "$GITHUB_OUTPUT"
|
||
|
||
# Content guards are PR-scoped and cheap — run them before the suite so
|
||
# a guard failure fails fast instead of after 20 minutes of tests.
|
||
- name: Changelog is append-only
|
||
env:
|
||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||
PR_BODY: ${{ github.event.pull_request.body }}
|
||
run: node scripts/check-changelog-append-only.mjs
|
||
- name: Aggregate is up to date with docs/changelog
|
||
run: node scripts/build-changelog.mjs --check
|
||
- name: No conflict markers in markdown diffs
|
||
env:
|
||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||
run: node scripts/check-docs-conflict-markers.mjs
|
||
- name: Block Closes/Fixes/Resolves on plans-only PRs
|
||
env:
|
||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||
PR_BODY: ${{ github.event.pull_request.body }}
|
||
run: node scripts/check-plans-only-pr-closes.mjs
|
||
|
||
- name: Check test file locations
|
||
run: node scripts/check-test-locations.mjs
|
||
- name: Lint
|
||
run: pnpm lint
|
||
- name: Format check
|
||
run: pnpm format:check
|
||
- name: Typecheck
|
||
if: steps.scope.outputs.code_mode != 'none'
|
||
run: pnpm typecheck
|
||
- name: Knip
|
||
if: steps.scope.outputs.code_mode != 'none'
|
||
run: pnpm knip
|
||
- name: Check cycles
|
||
if: steps.scope.outputs.code_mode != 'none'
|
||
run: pnpm check:cycles
|
||
|
||
- name: Unit tests (full suite)
|
||
if: steps.scope.outputs.test_mode == 'full'
|
||
run: pnpm test:unit
|
||
- name: Unit tests (changed since PR base)
|
||
if: steps.scope.outputs.test_mode == 'changed'
|
||
run: pnpm test:unit -- --changed origin/${{ github.base_ref }} --passWithNoTests
|
||
- name: Prepare database URL (int service)
|
||
run: |
|
||
set -euo pipefail
|
||
echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" >> "$GITHUB_ENV"
|
||
# vitest re-loads .env.test over the process env, so the service URL
|
||
# must win through the .env.test.local layer (same mechanism
|
||
# `pnpm worktree next` uses).
|
||
echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" > .env.test.local
|
||
- name: Migrate (int database)
|
||
run: pnpm migrate
|
||
- name: Seed minimal (int database)
|
||
run: pnpm db:seed:minimal
|
||
|
||
- name: Integration tests (full suite)
|
||
if: steps.scope.outputs.test_mode == 'full'
|
||
env:
|
||
# vitest forks ≈ CPUs and each fork holds a Payload pool (max 10) →
|
||
# overflows the service Postgres' 100 clients ("too many clients
|
||
# already"). 4 forks stay well under. The unit step above stays
|
||
# CPU-relative (no DB).
|
||
VITEST_MAX_WORKERS: 4
|
||
run: pnpm test:int
|
||
- name: Integration tests (changed since PR base)
|
||
if: steps.scope.outputs.test_mode == 'changed'
|
||
env:
|
||
VITEST_MAX_WORKERS: 4
|
||
run: pnpm test:int -- --changed origin/${{ github.base_ref }} --passWithNoTests
|
||
|
||
- name: Prepare database URL (build/e2e service)
|
||
if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode == 'selected'
|
||
run: |
|
||
set -euo pipefail
|
||
echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5433/teqo_test" >> "$GITHUB_ENV"
|
||
echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5433/teqo_test" > .env.test.local
|
||
- name: Migrate (build/e2e database)
|
||
if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode == 'selected'
|
||
run: pnpm migrate
|
||
- name: Seed minimal (build/e2e database)
|
||
if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode == 'selected'
|
||
run: pnpm db:seed:minimal
|
||
|
||
- name: Build (against the local service database)
|
||
if: steps.scope.outputs.build_mode != 'none'
|
||
run: pnpm build
|
||
|
||
- name: Cache Playwright browsers
|
||
if: steps.scope.outputs.e2e_mode == 'selected'
|
||
uses: actions/cache@v4
|
||
with:
|
||
path: ~/.cache/ms-playwright
|
||
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||
restore-keys: |
|
||
playwright-${{ runner.os }}-
|
||
- name: Install Playwright chromium
|
||
if: steps.scope.outputs.e2e_mode == 'selected'
|
||
run: pnpm exec playwright install --with-deps chromium
|
||
- name: Build for e2e
|
||
# Selected mode needs the build too (the Playwright webServer serves
|
||
# the `.next-e2e` production build).
|
||
if: steps.scope.outputs.e2e_mode == 'selected'
|
||
run: pnpm build
|
||
env:
|
||
NEXT_DIST_DIR: .next-e2e
|
||
- name: E2E tests (blast radius — manifest-selected specs)
|
||
if: steps.scope.outputs.e2e_mode == 'selected'
|
||
# OPS72: the PR never runs e2e full — mode `full` (high-risk diff)
|
||
# skips this step by design (local mirror + deploy verify cover it).
|
||
# Explicit CI workers; the local default stays 2 (playwright.config.ts).
|
||
# 4 workers in one process replace the former 2 shards × 2 workers.
|
||
env:
|
||
PLAYWRIGHT_WORKERS: 4
|
||
# The `--` separates pnpm from the scope paths: pnpm consumes it and
|
||
# playwright receives the bare spec paths (no flag parsing drift).
|
||
run: pnpm test:e2e -- ${{ steps.scope.outputs.e2e_specs }}
|