teqo-backup/.github/workflows/ci-pr.yml
Francisco Solla 377a2c89b1
Some checks failed
Deploy / preflight (push) Has been cancelled
Deploy / verify (push) Has been cancelled
Deploy / deploy staging (teqo-staging) (push) Has been cancelled
Deploy / requeue when main advanced (push) Has been cancelled
Deploy / deploy production (teqo-1313) (push) Has been cancelled
fix(ci): pina ubuntu-24.04 nos jobs com Playwright
O label ubuntu-latest migrou para Ubuntu 26.04 em out/2026 e o Playwright 1.58.2 não tem build para 26.04 (run 42095564329: 'Playwright does not support chromium on ubuntu26.04-x64'). Pina o job checks do ci-pr e o verify do deploy em ubuntu-24.04 até o bump do Playwright.
2026-11-05 17:06:44 -03:00

243 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/curated/unmapped-risk). e2e policy
# (OPS72 + OPS86): 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 `curated`: the
# PR runs the curated e2e cross-section (never zero), 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. Risk-area files
# (`src/utilities/access`, `src/lib/schemas`, push, AI) without a manifest
# entry classify `unmapped-risk` and the job FAILS closed listing them —
# touched code never passes CI on zero e2e. The unit/int `changed` steps go
# through `vitest-changed-or-full.mjs`: a zero selection falls back to the
# full suite instead of green with no tests (OPS86).
#
# 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 service (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. ONE service (OPS88): a
# deterministic reset (`pnpm db:reset` — drop schema → migrate → seed
# minimal) between the integration phase and the build/e2e phase keeps e2e
# from inheriting int fixture residue, instead of a second container.
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
# Playwright 1.58.2 has no ubuntu26.04 build and `ubuntu-latest` migrated
# to 26.04 in Oct 2026 (run 42095564329). Pin 24.04 until the Playwright
# bump lands; `ubuntu-latest` here breaks every PR at browser install.
runs-on: ubuntu-24.04
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
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"
echo "e2e_unmapped=$(echo "$result" | jq -r '[.e2e.unmapped[]] | join(" ")')" >> "$GITHUB_OUTPUT"
# OPS86 fail-closed: risk-area files without a manifest entry must
# never pass green with zero e2e. Fail fast, before the expensive steps.
- name: Fail on unmapped risk paths
if: steps.scope.outputs.e2e_mode == 'unmapped-risk'
run: |
echo "::error::Risk-area files touched by this PR have no e2e manifest mapping (fail-closed). Unmapped files:"
for file in ${{ steps.scope.outputs.e2e_unmapped }}; do
echo "::error::- $file"
done
echo "::error::Add entries in scripts/lib/e2e-affected-manifest.mjs (E2E_AFFECTED_MANIFEST) covering the risk-area paths."
exit 1
# 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: 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, full on empty selection)
if: steps.scope.outputs.test_mode == 'changed'
run: node scripts/vitest-changed-or-full.mjs --suite unit --base origin/${{ github.base_ref }}
- name: Prepare database URL
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
run: pnpm migrate
- name: Seed minimal
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, full on empty selection)
if: steps.scope.outputs.test_mode == 'changed'
env:
VITEST_MAX_WORKERS: 4
run: node scripts/vitest-changed-or-full.mjs --suite int --base origin/${{ github.base_ref }}
- name: Reset database (drop → migrate → seed)
# OPS88: one Postgres per run — the build/e2e phase starts from the
# same clean baseline (drop schema → migrate → seed:minimal) instead
# of a second container. Runs whenever a build phase follows
# (selected/curated e2e always builds `.next-e2e` — OPS86).
if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode == 'selected' || steps.scope.outputs.e2e_mode == 'curated'
run: pnpm db:reset
- name: Build (against the local service database)
# Only when it is the single build of the run: selected/curated e2e
# runs serve the `.next-e2e` build (below), so the default build is
# skipped there (OPS88 — one build per run when build+e2e run together).
if: steps.scope.outputs.build_mode != 'none' && steps.scope.outputs.e2e_mode != 'selected' && steps.scope.outputs.e2e_mode != 'curated'
run: pnpm build
- name: Cache Playwright browsers
if: steps.scope.outputs.e2e_mode == 'selected' || steps.scope.outputs.e2e_mode == 'curated'
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' || steps.scope.outputs.e2e_mode == 'curated'
run: pnpm exec playwright install --with-deps chromium
- name: Build for e2e
# Selected and curated modes need the build too (the Playwright
# webServer serves the `.next-e2e` production build).
# NEXT_PUBLIC_SITE_URL is inlined at build time, so the canonical
# `.test` origin must reach THIS build — it lets the agenda feed
# dialog generate a link under the production build when AgendaFeed
# is in the blast radius (OPS83).
if: steps.scope.outputs.e2e_mode == 'selected' || steps.scope.outputs.e2e_mode == 'curated'
run: pnpm build
env:
NEXT_DIST_DIR: .next-e2e
NEXT_PUBLIC_SITE_URL: https://feed.e2e.teqo.test
# S15 — deterministic cutout stub: the e2e build never downloads the
# MediaPipe wasm nor runs inference.
NEXT_PUBLIC_CARDS_CUTOUT_STUB: '1'
NEXT_PUBLIC_FACE_SEARCH_STUB: '1'
- name: E2E tests (blast radius — manifest-selected or curated specs)
if: steps.scope.outputs.e2e_mode == 'selected' || steps.scope.outputs.e2e_mode == 'curated'
# OPS72 + OPS86: the PR never runs e2e full — a high-risk diff runs
# the curated cross-section (`curated`, never zero) and the full suite
# stays in the deploy verify; `unmapped-risk` failed the job earlier.
# 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
NEXT_PUBLIC_SITE_URL: https://feed.e2e.teqo.test
# 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 }}