mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
- /fotos/encontre + POST /api/fotos/selfie: só o vetor 128-d, sem imagem, sem score e sem nomes de terceiros; fail-closed pela flag photoAlbum.selfieSearchEnabled (default false) e pelos Consents por chave estável (busca-selfie-fotos/busca-selfie-indice, não seedados — a superfície nasce fechada até o aval jurídico/DPIA). - Índice A/C do gate (PR #1370): só quem aderiu é indexado; descriptors do acervo são transitórios. Collection faceSubject (admin-only, sem vínculo com Contact), vínculos matchedPhotos e marker faces.checkedKey no archivePhoto (modelo + enrolledAt). - Engine @vladmandic/face-api nos dois lados: browser lazy com modelos same-origin (copy-face-vision-assets no prebuild/predev/Docker) e CLI node-wasm sem tfjs-node. CLIs ops-only faces:enroll / faces:index --apply|--verify com guardas C231/C232. - Opt-out self-service por reconhecimento (leave-index) + canal de remoção do C233; rate limit próprio (namespace selfie:<hash>) e bounded body extraído do beacon. - UI portada classe-a-classe do design estendido (paridade certificada, tier primário); e2e frontendFotosSelfie (manifest+curado+projeto, engine stubada) e canário do engine real; runbook §C234, contrato em AGENTS-public.md e changelog. - Migration aditiva add_face_subject (SQL revisado); hard-stops aprovados na sessão.
240 lines
12 KiB
YAML
240 lines
12 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/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
|
||
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
|
||
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 }}
|