chore(OPS62): CI fail-fast estrutural — job único checks sequencial (X1)
ci-pr.yml e ci.yml deixam de ter jobs irmãos: 1 job 'checks' com steps em série (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e). Step vermelho interrompe o job, o run falha e o slot é liberado — sem API de cancel (Forgejo 9.0.3 não tem), sem watchdog, sem upgrade do Forgejo. Skips do ci-scope.mjs viram step id 'scope'; e2e em 1 processo com PLAYWRIGHT_WORKERS=4 (substitui 2 shards × 2 workers); 2 services Postgres por run com portas host derivadas do run_id (runs concorrentes não colidem); contexto 'CI (PR) / checks' preservado (job id checks — contrato do automerger OPS61); deploy OPS53 gated em needs checks. Plumbing morto removido (e2eShardConfig/e2e_shards). Docs: AGENT-OPS, TESTING.md, changelog. Impl: docs/plans/ops62-ci-fail-fast-forgejo-impl.md (a intenção watchdog+upgrade fica como histórico do gate).
This commit is contained in:
parent
a1a6160ed8
commit
12a4fe1005
12 changed files with 384 additions and 565 deletions
|
|
@ -1,23 +1,26 @@
|
|||
# PR gate: feature → main. Two-phase cascade:
|
||||
# Phase 1 (cheap, no Postgres): lint, format, typecheck, knip, cycles, unit
|
||||
# Phase 2 (expensive, Postgres; needs phase 1 green/skipped): int, build, e2e
|
||||
# Job-level skips come from the `scope` classifier (scripts/ci-scope.mjs). A
|
||||
# phase-1 failure skips phase 2 via `needs` + explicit if.
|
||||
# PR gate: feature → main. 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, the run fails and
|
||||
# the slot is released immediately. No cancel-run API (Forgejo 9.0.3 has
|
||||
# none), no watchdog job, no upgrades: with one job there are no siblings to
|
||||
# cancel. Steps keep their own logs, so diagnosis is intact after a failure.
|
||||
# The job id `checks` preserves the `CI (PR) / checks` status context the
|
||||
# automerger waitForChecks and branch protection rely on (OPS61).
|
||||
#
|
||||
# Job-level skips come from the `scope` classifier (scripts/ci-scope.mjs) run
|
||||
# as a step; its outputs drive `if:` on the heavy steps (same modes as the
|
||||
# former per-job cascade: none/changed/full).
|
||||
#
|
||||
# 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 and on main (ci.yml), incl. campaignMigrationReconciliation — the check's
|
||||
# hard fail deadlocked every open schema PR the moment two coexisted.
|
||||
#
|
||||
# High-risk diffs (schema, lockfile, classifiers — see
|
||||
# scripts/lib/test-affected-core.mjs) force full unit/int/e2e. Push to main
|
||||
# always runs the full suite in ci.yml (no affected skips).
|
||||
#
|
||||
# Forgejo port (OPS50): no `concurrency:`/`permissions:` (unsupported /
|
||||
# native token); Postgres services publish per-job ports (5433 int, 5434
|
||||
# build, 5435/5436 e2e shards) and jobs reach them through the bridge gateway
|
||||
# (DB_HOST from /proc/net/route). The GitHub fail-fast action was dropped
|
||||
# (Forgejo has no cancel-run plumbing); job timeouts cover that role.
|
||||
# Forgejo port (OPS50): Postgres services publish per-port on the runner
|
||||
# machine and steps reach them through the bridge gateway (DB_HOST from
|
||||
# /proc/net/route). Host ports derive from the run id (OPS62 X1) so concurrent
|
||||
# runs never collide; two isolated services — int and build+e2e — keep e2e
|
||||
# from inheriting int fixture residue (same isolation the former jobs had).
|
||||
name: CI (PR)
|
||||
|
||||
on:
|
||||
|
|
@ -29,17 +32,41 @@ env:
|
|||
NEXT_PUBLIC_SITE_URL: http://localhost:3000
|
||||
|
||||
jobs:
|
||||
scope:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
code_mode: ${{ steps.classify.outputs.code_mode }}
|
||||
build_mode: ${{ steps.classify.outputs.build_mode }}
|
||||
test_mode: ${{ steps.classify.outputs.test_mode }}
|
||||
e2e_mode: ${{ steps.classify.outputs.e2e_mode }}
|
||||
e2e_specs: ${{ steps.classify.outputs.e2e_specs }}
|
||||
e2e_matrix: ${{ steps.classify.outputs.e2e_matrix }}
|
||||
e2e_shards: ${{ steps.classify.outputs.e2e_shards }}
|
||||
timeout-minutes: 50
|
||||
services:
|
||||
# Host ports derive from the run id (OPS62 X1): the single job holds
|
||||
# both services for the whole run, and concurrent runs on the shared
|
||||
# runner would collide on fixed ports ("port is already allocated") —
|
||||
# a ~1% residual collision when two runs share run_id % 100 is loud,
|
||||
# harmless and fixed by the next push.
|
||||
postgres-int:
|
||||
image: postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_USER: teqo
|
||||
POSTGRES_PASSWORD: teqo
|
||||
POSTGRES_DB: teqo_test
|
||||
ports:
|
||||
- ${{ 5433 + (github.run_id % 100) }}: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:
|
||||
- ${{ 5533 + (github.run_id % 100) }}:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U teqo"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 12
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
|
|
@ -50,11 +77,13 @@ jobs:
|
|||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Classify PR blast radius
|
||||
id: classify
|
||||
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"
|
||||
|
|
@ -62,317 +91,9 @@ jobs:
|
|||
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"
|
||||
# OPS34: full runs split across shards (one runner per shard); selected
|
||||
# stays on a single runner (a second build for a handful of specs is
|
||||
# waste). Single source: e2eShardConfig in scripts/lib/test-affected-core.mjs.
|
||||
echo "e2e_matrix=$(echo "$result" | jq -c .e2e_shards.matrix)" >> "$GITHUB_OUTPUT"
|
||||
echo "e2e_shards=$(echo "$result" | jq -r .e2e_shards.total)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Check test file locations
|
||||
run: node scripts/check-test-locations.mjs
|
||||
- run: pnpm lint
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm format:check
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: [scope]
|
||||
if: needs.scope.outputs.code_mode != 'none'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm typecheck
|
||||
|
||||
knip:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: [scope]
|
||||
if: needs.scope.outputs.code_mode != 'none'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm knip
|
||||
|
||||
cycles:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: [scope]
|
||||
if: needs.scope.outputs.code_mode != 'none'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm check:cycles
|
||||
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: [scope]
|
||||
if: needs.scope.outputs.test_mode != 'none'
|
||||
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: Unit tests (full suite)
|
||||
if: needs.scope.outputs.test_mode == 'full'
|
||||
run: pnpm test:unit
|
||||
|
||||
- name: Unit tests (changed since PR base)
|
||||
if: needs.scope.outputs.test_mode == 'changed'
|
||||
run: pnpm test:unit -- --changed origin/${{ github.base_ref }} --passWithNoTests
|
||||
|
||||
int:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
needs: [scope, lint, format, typecheck, knip, cycles, unit]
|
||||
if: |
|
||||
always()
|
||||
&& !cancelled()
|
||||
&& needs.scope.outputs.test_mode != 'none'
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs.format.result == 'success'
|
||||
&& (needs.typecheck.result == 'success' || needs.typecheck.result == 'skipped')
|
||||
&& (needs.knip.result == 'success' || needs.knip.result == 'skipped')
|
||||
&& (needs.cycles.result == 'success' || needs.cycles.result == 'skipped')
|
||||
&& (needs.unit.result == 'success' || needs.unit.result == 'skipped')
|
||||
env:
|
||||
# Self-hosted runner has 8 CPUs; vitest forks ≈ CPUs and each fork holds
|
||||
# a Payload pool (max 10) → overflows the service Postgres' 100 clients
|
||||
# ("too many clients already", CI run 180). 4 forks stay well under.
|
||||
# GitHub's 2–4 vCPU runners keep the CPU-relative default (no env).
|
||||
VITEST_MAX_WORKERS: 4
|
||||
services:
|
||||
postgres:
|
||||
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: Prepare database URL (service via bridge gateway)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route)
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:5433/teqo_test" >> "$GITHUB_ENV"
|
||||
# vitest/playwright re-load .env.test over the process env, so the
|
||||
# service URL must win through the .env.test.local layer (same
|
||||
# mechanism `pnpm worktree next` uses). OPS50.
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:5433/teqo_test" > .env.test.local
|
||||
- run: pnpm migrate
|
||||
- run: pnpm db:seed:minimal
|
||||
|
||||
- name: Integration tests (full suite)
|
||||
if: needs.scope.outputs.test_mode == 'full'
|
||||
run: pnpm test:int
|
||||
|
||||
- name: Integration tests (changed since PR base)
|
||||
if: needs.scope.outputs.test_mode == 'changed'
|
||||
run: pnpm test:int -- --changed origin/${{ github.base_ref }} --passWithNoTests
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
needs: [scope, lint, format, typecheck, knip, cycles, unit]
|
||||
if: |
|
||||
always()
|
||||
&& !cancelled()
|
||||
&& needs.scope.outputs.build_mode != 'none'
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs.format.result == 'success'
|
||||
&& (needs.typecheck.result == 'success' || needs.typecheck.result == 'skipped')
|
||||
&& (needs.knip.result == 'success' || needs.knip.result == 'skipped')
|
||||
&& (needs.cycles.result == 'success' || needs.cycles.result == 'skipped')
|
||||
&& (needs.unit.result == 'success' || needs.unit.result == 'skipped')
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_USER: teqo
|
||||
POSTGRES_PASSWORD: teqo
|
||||
POSTGRES_DB: teqo_test
|
||||
ports:
|
||||
- 5434:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U teqo"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 12
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Prepare database URL (service via bridge gateway)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route)
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:5434/teqo_test" >> "$GITHUB_ENV"
|
||||
- name: Build (against the local service database)
|
||||
run: pnpm build
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs: [scope, lint, format, typecheck, knip, cycles, unit]
|
||||
if: |
|
||||
always()
|
||||
&& !cancelled()
|
||||
&& needs.scope.outputs.e2e_mode != 'none'
|
||||
&& needs.lint.result == 'success'
|
||||
&& needs.format.result == 'success'
|
||||
&& (needs.typecheck.result == 'success' || needs.typecheck.result == 'skipped')
|
||||
&& (needs.knip.result == 'success' || needs.knip.result == 'skipped')
|
||||
&& (needs.cycles.result == 'success' || needs.cycles.result == 'skipped')
|
||||
&& (needs.unit.result == 'success' || needs.unit.result == 'skipped')
|
||||
# OPS34: full mode splits across 2 runners; selected mode stays on shard 1
|
||||
# (shard 2 skips build+test via step `if`). Static [1, 2] — Forgejo 1.22
|
||||
# does not pass job results into matrix expansion, so fromJson(needs.*)
|
||||
# yields an empty shard. Keep the literal in sync with e2eShardConfig in
|
||||
# scripts/lib/test-affected-core.mjs (ci.yml pins the same [1, 2] / 2).
|
||||
# fail-fast: false keeps both shards' logs.
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
env:
|
||||
# Explicit CI workers; the local default stays 2 (playwright.config.ts).
|
||||
PLAYWRIGHT_WORKERS: 2
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_USER: teqo
|
||||
POSTGRES_PASSWORD: teqo
|
||||
POSTGRES_DB: teqo_test
|
||||
ports:
|
||||
- ${{ matrix.shard == 1 && '5435' || '5436' }}: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: Cache Playwright browsers
|
||||
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
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Prepare database URL (service via bridge gateway)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route)
|
||||
DB_PORT=${{ matrix.shard == 1 && '5435' || '5436' }}
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" >> "$GITHUB_ENV"
|
||||
# vitest/playwright re-load .env.test over the process env, so the
|
||||
# service URL must win through the .env.test.local layer (same
|
||||
# mechanism `pnpm worktree next` uses). OPS50.
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" > .env.test.local
|
||||
- run: pnpm migrate
|
||||
- run: pnpm db:seed:minimal
|
||||
- name: Build for e2e
|
||||
# Selected mode only needs shard 1 (a second build for a handful of
|
||||
# specs is waste).
|
||||
if: needs.scope.outputs.e2e_mode == 'full' || matrix.shard == 1
|
||||
run: pnpm build
|
||||
env:
|
||||
# Same dir the Playwright webServer serves (playwright.config.ts
|
||||
# falls back to '.next-e2e'; gate-ci.mjs builds there too).
|
||||
NEXT_DIST_DIR: .next-e2e
|
||||
|
||||
- name: E2E tests (full suite, shard ${{ matrix.shard }})
|
||||
if: needs.scope.outputs.e2e_mode == 'full'
|
||||
# No `--` separator: pnpm would forward it verbatim to playwright and
|
||||
# the flag would be parsed as a file pattern ("No tests found").
|
||||
run: pnpm test:e2e --shard=${{ matrix.shard }}/${{ needs.scope.outputs.e2e_shards }}
|
||||
|
||||
- name: E2E tests (manifest-selected specs)
|
||||
# Selected specs run once, on shard 1; shard 2 skips (no test step).
|
||||
if: needs.scope.outputs.e2e_mode == 'selected' && matrix.shard == 1
|
||||
run: pnpm test:e2e -- ${{ needs.scope.outputs.e2e_specs }}
|
||||
|
||||
docs-guards:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# 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 }}
|
||||
|
|
@ -384,44 +105,112 @@ jobs:
|
|||
env:
|
||||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||||
run: node scripts/check-docs-conflict-markers.mjs
|
||||
|
||||
plans-only-closes:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- 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
|
||||
|
||||
checks:
|
||||
name: checks
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- scope
|
||||
- lint
|
||||
- format
|
||||
- typecheck
|
||||
- knip
|
||||
- cycles
|
||||
- unit
|
||||
- int
|
||||
- build
|
||||
- e2e
|
||||
- docs-guards
|
||||
- plans-only-closes
|
||||
if: always()
|
||||
steps:
|
||||
- name: All cascade checks green
|
||||
env:
|
||||
NEEDS: ${{ toJSON(needs) }}
|
||||
- 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 via bridge gateway)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "$NEEDS" | jq -e '
|
||||
to_entries
|
||||
| all(.value.result == "success" or .value.result == "skipped")
|
||||
'
|
||||
DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route)
|
||||
DB_PORT=${{ 5433 + (github.run_id % 100) }}
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/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). OPS50.
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/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:
|
||||
# Self-hosted runner has 8 CPUs; vitest forks ≈ CPUs and each fork
|
||||
# holds a Payload pool (max 10) → overflows the service Postgres' 100
|
||||
# clients ("too many clients already", CI run 180). 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 via bridge gateway)
|
||||
if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode != 'none'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route)
|
||||
DB_PORT=${{ 5533 + (github.run_id % 100) }}
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" >> "$GITHUB_ENV"
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" > .env.test.local
|
||||
- name: Migrate (build/e2e database)
|
||||
if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode != 'none'
|
||||
run: pnpm migrate
|
||||
- name: Seed minimal (build/e2e database)
|
||||
if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode != 'none'
|
||||
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 != 'none'
|
||||
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 != 'none'
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Build for e2e
|
||||
# Selected mode needs the build too (the Playwright webServer serves
|
||||
# .next-e2e; the fallback `.next-e2e` dir is what gate-ci.mjs builds).
|
||||
if: steps.scope.outputs.e2e_mode != 'none'
|
||||
run: pnpm build
|
||||
env:
|
||||
NEXT_DIST_DIR: .next-e2e
|
||||
- name: E2E tests (full suite, single job, 4 workers)
|
||||
if: steps.scope.outputs.e2e_mode == 'full'
|
||||
# Explicit CI workers; the local default stays 2 (playwright.config.ts).
|
||||
# 4 workers in one process replace the former 2 shards × 2 workers.
|
||||
# No `--` separator: pnpm would forward it verbatim to playwright and
|
||||
# the flag would be parsed as a file pattern ("No tests found").
|
||||
env:
|
||||
PLAYWRIGHT_WORKERS: 4
|
||||
run: pnpm test:e2e
|
||||
- name: E2E tests (manifest-selected specs)
|
||||
if: steps.scope.outputs.e2e_mode == 'selected'
|
||||
env:
|
||||
PLAYWRIGHT_WORKERS: 4
|
||||
run: pnpm test:e2e -- ${{ steps.scope.outputs.e2e_specs }}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
# Post-merge verifier on `main`: always the FULL suite (no affected skips).
|
||||
# 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, the run fails and the slot is released
|
||||
# immediately. No cancel-run API (Forgejo 9.0.3 has none), no watchdog job,
|
||||
# no upgrades. `deploy` (needs: [checks]) never starts on a red run.
|
||||
#
|
||||
# When the suite is green, the `deploy` job publishes teqo-1313 on the
|
||||
# homeserver (OPS53): it streams scripts/deploy-homeserver.sh to the homeserver
|
||||
# over SSH; the remote script builds the standalone image at the pushed SHA,
|
||||
# pushes it to the local registry (localhost:5000), applies pending migrations
|
||||
# through the compose maintenance service and rolls the container out.
|
||||
#
|
||||
# Forgejo port (OPS50): Postgres services publish to per-job HOST ports on the
|
||||
# runner machine (act/forgejo runner semantics — the canonical 5432 is the
|
||||
# workstation's own dev database), and jobs reach them through the bridge
|
||||
# gateway: DB_HOST = gateway IP from /proc/net/route. No `permissions:`
|
||||
# (Forgejo issues one native GITHUB_TOKEN) and no `concurrency:` (unsupported).
|
||||
# `gh workflow run` requeues became `node scripts/forgejo-dispatch.mjs`.
|
||||
# The GitHub fail-fast action was dropped (Forgejo has no cancel-run plumbing);
|
||||
# job timeouts cover that role.
|
||||
|
||||
# Forgejo port (OPS50): Postgres services publish to per-port HOST ports on
|
||||
# the runner machine (act/forgejo runner semantics — the canonical 5432 is the
|
||||
# workstation's own dev database), and steps reach them through the bridge
|
||||
# gateway: DB_HOST = gateway IP from /proc/net/route. Host ports derive from
|
||||
# the run id (OPS62 X1) so concurrent runs never collide; two isolated
|
||||
# services — int and build+e2e — keep e2e from inheriting int fixture residue.
|
||||
# No `permissions:` (Forgejo issues one native GITHUB_TOKEN) and no
|
||||
# `concurrency:` (unsupported). `gh workflow run` requeues became
|
||||
# `node scripts/forgejo-dispatch.mjs`.
|
||||
name: CI
|
||||
|
||||
on:
|
||||
|
|
@ -26,43 +32,36 @@ env:
|
|||
NEXT_PUBLIC_SITE_URL: http://localhost:3000
|
||||
|
||||
jobs:
|
||||
static:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: node scripts/check-test-locations.mjs
|
||||
- run: pnpm lint
|
||||
- run: pnpm format:check
|
||||
- run: pnpm typecheck
|
||||
- run: pnpm knip
|
||||
- run: pnpm check:cycles
|
||||
- run: pnpm test:unit
|
||||
|
||||
int:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
env:
|
||||
# Self-hosted runner has 8 CPUs; 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. GitHub's 2–4
|
||||
# vCPU runners keep the CPU-relative default (no env).
|
||||
VITEST_MAX_WORKERS: 4
|
||||
timeout-minutes: 50
|
||||
services:
|
||||
postgres:
|
||||
# Host ports derive from the run id (OPS62 X1): the single job holds
|
||||
# both services for the whole run, and concurrent runs on the shared
|
||||
# runner would collide on fixed ports ("port is already allocated") —
|
||||
# a ~1% residual collision when two runs share run_id % 100 is loud,
|
||||
# harmless and fixed by the next push.
|
||||
postgres-int:
|
||||
image: postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_USER: teqo
|
||||
POSTGRES_PASSWORD: teqo
|
||||
POSTGRES_DB: teqo_test
|
||||
ports:
|
||||
- 5433:5432
|
||||
- ${{ 5433 + (github.run_id % 100) }}: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:
|
||||
- ${{ 5533 + (github.run_id % 100) }}:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U teqo"
|
||||
--health-interval 5s
|
||||
|
|
@ -76,90 +75,60 @@ jobs:
|
|||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Prepare database URL (service via bridge gateway)
|
||||
|
||||
- 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
|
||||
run: pnpm typecheck
|
||||
- name: Knip
|
||||
run: pnpm knip
|
||||
- name: Check cycles
|
||||
run: pnpm check:cycles
|
||||
- name: Unit tests (full suite)
|
||||
run: pnpm test:unit
|
||||
|
||||
- name: Prepare database URL (int service via bridge gateway)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route)
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:5433/teqo_test" >> "$GITHUB_ENV"
|
||||
DB_PORT=${{ 5433 + (github.run_id % 100) }}
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/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). OPS50.
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:5433/teqo_test" > .env.test.local
|
||||
- run: pnpm migrate
|
||||
- run: pnpm db:seed:minimal
|
||||
- run: pnpm test:int
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/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)
|
||||
env:
|
||||
POSTGRES_USER: teqo
|
||||
POSTGRES_PASSWORD: teqo
|
||||
POSTGRES_DB: teqo_test
|
||||
ports:
|
||||
- 5434:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U teqo"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 12
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Prepare database URL (service via bridge gateway)
|
||||
# Self-hosted runner has 8 CPUs; 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: Prepare database URL (build/e2e service via bridge gateway)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route)
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:5434/teqo_test" >> "$GITHUB_ENV"
|
||||
DB_PORT=${{ 5533 + (github.run_id % 100) }}
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" >> "$GITHUB_ENV"
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" > .env.test.local
|
||||
- name: Migrate (build/e2e database)
|
||||
run: pnpm migrate
|
||||
- name: Seed minimal (build/e2e database)
|
||||
run: pnpm db:seed:minimal
|
||||
|
||||
- name: Build (against the service database)
|
||||
run: pnpm build
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
# Main always runs the full suite: split it across 2 jobs (OPS34), each
|
||||
# building its own `.next-e2e` (measured 2026-08-10: build 172s + tests
|
||||
# 196s + ~54s fixed → ≈ 5.5 min per shard vs 435s serial). `fail-fast:
|
||||
# false` so one shard's failure keeps the other's diagnostics. Keep the
|
||||
# [1, 2] / 2 literal in sync with `e2eShardConfig` in
|
||||
# scripts/lib/test-affected-core.mjs (ci-pr pins the same literal).
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
env:
|
||||
# Explicit CI workers; the local default stays 2 (playwright.config.ts).
|
||||
PLAYWRIGHT_WORKERS: 2
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_USER: teqo
|
||||
POSTGRES_PASSWORD: teqo
|
||||
POSTGRES_DB: teqo_test
|
||||
ports:
|
||||
- ${{ matrix.shard == 1 && '5435' || '5436' }}:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U teqo"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 12
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
|
@ -169,44 +138,20 @@ jobs:
|
|||
playwright-${{ runner.os }}-
|
||||
- name: Install Playwright chromium
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Prepare database URL (service via bridge gateway)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB_HOST=$(awk '$2 == "00000000" {a=$3; print strtonum("0x" substr(a,7,2)) "." strtonum("0x" substr(a,5,2)) "." strtonum("0x" substr(a,3,2)) "." strtonum("0x" substr(a,1,2)); exit}' /proc/net/route)
|
||||
DB_PORT=${{ matrix.shard == 1 && '5435' || '5436' }}
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" >> "$GITHUB_ENV"
|
||||
# playwright 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). OPS50.
|
||||
echo "DATABASE_URL=postgresql://teqo:teqo@${DB_HOST}:${DB_PORT}/teqo_test" > .env.test.local
|
||||
- run: pnpm migrate
|
||||
- run: pnpm db:seed:minimal
|
||||
- name: Build for e2e
|
||||
# Same dir the Playwright webServer serves (playwright.config.ts
|
||||
# falls back to '.next-e2e'; gate-ci.mjs builds there too).
|
||||
run: pnpm build
|
||||
env:
|
||||
# Same dir the Playwright webServer serves (playwright.config.ts
|
||||
# falls back to '.next-e2e'; gate-ci.mjs builds there too).
|
||||
NEXT_DIST_DIR: .next-e2e
|
||||
- name: E2E tests (full suite, shard ${{ matrix.shard }})
|
||||
- name: E2E tests (full suite, single job, 4 workers)
|
||||
# Explicit CI workers; the local default stays 2 (playwright.config.ts).
|
||||
# 4 workers in one process replace the former 2 shards × 2 workers.
|
||||
# No `--` separator: pnpm would forward it verbatim to playwright and
|
||||
# the flag would be parsed as a file pattern ("No tests found").
|
||||
run: pnpm test:e2e --shard=${{ matrix.shard }}/2
|
||||
|
||||
checks:
|
||||
name: checks
|
||||
runs-on: ubuntu-latest
|
||||
needs: [static, int, build, e2e]
|
||||
if: always()
|
||||
steps:
|
||||
- name: All main verifier checks green
|
||||
env:
|
||||
NEEDS: ${{ toJSON(needs) }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "$NEEDS" | jq -e '
|
||||
to_entries
|
||||
| all(.value.result == "success" or .value.result == "skipped")
|
||||
'
|
||||
PLAYWRIGHT_WORKERS: 4
|
||||
run: pnpm test:e2e
|
||||
|
||||
deploy:
|
||||
name: deploy homeserver (teqo-1313)
|
||||
|
|
|
|||
|
|
@ -69,16 +69,17 @@ Arquivos de "lista" que N agentes escrevem em paralelo (changelog, plans, migra
|
|||
|
||||
## CI por alvo
|
||||
|
||||
| Workflow | Trigger | Banco | Passos |
|
||||
| ------------------------------------ | -------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ci-pr.yml` | PR → `main` | service Postgres 17 | Fase 1 barata → fase 2 cara; skips via `ci-scope.mjs`; fail-fast cancela o run no 1º job vermelho; rollup `checks` |
|
||||
| `ci.yml` | push / dispatch `main` | service Postgres | Full sempre; fail-fast cancela o run no 1º job vermelho; rollup `checks`; job `deploy` (runs-on `host`) publica no homeserver via `scripts/deploy-homeserver.sh` (OPS53) |
|
||||
| `issue-done-on-main-merge.yml` | PR merged → `main` (same-repo) | — | `Closes`/`Fixes` → `done` + `in-prod` (PAT `FORGEJO_API_TOKEN`; `GITHUB_TOKEN` 403 em Issues) |
|
||||
| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` (same-repo) | — | `Related #N` aguardando plano → `ready` (OPS18; soft-skip; mesmo PAT) |
|
||||
| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `forgejo-pr-automerge.mjs` (plain Node): espera o rollup `checks` e mergea por rebase com `FORGEJO_API_TOKEN` (o `GITHUB_TOKEN` nativo não tem write no repo — o push do merge é rejeitado com 409); draft `cursor/*` → Ready; draft não-`cursor/*` = veto, skip |
|
||||
| `agent-pool.yml` | schedule / PR closed `main` / dispatch | — | Supervisor do pool (`POOL_GITHUB_TOKEN`) |
|
||||
| Workflow | Trigger | Banco | Passos |
|
||||
| ------------------------------------ | -------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `ci-pr.yml` | PR → `main` | 2× service Postgres 17 | Job único `checks` sequencial (OPS62 X1): guards → lint→format→typecheck→knip→cycles→unit→int→build→e2e; skips via `ci-scope.mjs` (step `scope`); **fail-fast estrutural** — step vermelho interrompe o job, o run falha e o slot é liberado; e2e 1 processo com `PLAYWRIGHT_WORKERS=4`; 2 services por nome na rede do job (`postgres-int`/`postgres-build`, sem publish host) |
|
||||
| `ci.yml` | push / dispatch `main` | 2× service Postgres 17 | Full sempre; job único `checks` sequencial (mesma mecânica X1); job `deploy` (runs-on `host`) publica no homeserver via `scripts/deploy-homeserver.sh` (OPS53) |
|
||||
| `issue-done-on-main-merge.yml` | PR merged → `main` (same-repo) | — | `Closes`/`Fixes` → `done` + `in-prod` (PAT `FORGEJO_API_TOKEN`; `GITHUB_TOKEN` 403 em Issues) |
|
||||
| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` (same-repo) | — | `Related #N` aguardando plano → `ready` (OPS18; soft-skip; mesmo PAT) |
|
||||
| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `forgejo-pr-automerge.mjs` (plain Node): espera o rollup `checks` e mergea por rebase com `FORGEJO_API_TOKEN` (o `GITHUB_TOKEN` nativo não tem write no repo — o push do merge é rejeitado com 409); draft `cursor/*` → Ready; draft não-`cursor/*` = veto, skip |
|
||||
| `agent-pool.yml` | schedule / PR closed `main` / dispatch | — | Supervisor do pool (`POOL_GITHUB_TOKEN`) |
|
||||
|
||||
Action runtimes: `actions/checkout@v5`, `actions/setup-node@v5`, `pnpm/action-setup@v6` e `styfle/cancel-workflow-action@0.13.1` usam Node 24 nativo — sem `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24`.
|
||||
Action runtimes: `actions/checkout@v5`, `actions/setup-node@v5` e `pnpm/action-setup@v6` usam Node 24 nativo — sem `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24`.
|
||||
Action runtimes: `actions/checkout@v5`, `actions/setup-node@v5` e `pnpm/action-setup@v6` usam Node 24 nativo — sem `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24`.
|
||||
|
||||
Fast gate: `pnpm gate:fast`. Push: `pnpm push`.
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
**Recently resolved (2026-08-18):** **OPS63** — os `docs-guards` (OPS44) rodam também no pre-push local: o espelho `scripts/gate-ci.mjs` ganhou os três checks na fase 1 (changelog append-only, agregado sync `--check`, marcadores de conflito em markdown) como chamadas diretas dos mesmos scripts do CI — labels espelhando os steps do job `docs-guards`; header do arquivo e linha final atualizados. `package.json`, `.husky/pre-push`, `git-push.mjs` e workflows `.forgejo/*` intocados: `gate:push` = `gate:ci` continua a cadeia única, o job `docs-guards` segue dono do escape `changelog-rewrite:` (body do PR — CI-only; restauração legítima D8 falha local com a mesma mensagem do CI e usa o bypass documentado `git push --no-verify`, com o CI como via do escape). Docs sincronizadas: `.agents/rules/engineering-standards.mdc` e `agent-pr-workflow.mdc` (gate descrito como "espelho do ci-pr.yml sem e2e, com docs-guards locais"), `docs/AGENT-OPS.md` (contrato item 4 — OPS44: checks no pre-push + nota do escape CI-only). Verificado por grep: `AGENTS.md` e skills não descreviam o gate sem os checks de docs — sem mudança. Anti-goals respeitados: sem bypass novo (falha local honesta), sem `plans-only-closes` local (contrato de PR body). Sem migration/access/Consent/UI/schema. Verificação: três checks green na branch; caminho de falha validado (commit descartável removendo linha do agregado → append-only falha com mensagem acionável); `pnpm gate:ci` local full; CI roda full nesta PR por HIGH_RISK (`gate-ci.mjs`).
|
||||
|
||||
**Recently resolved (2026-08-18):** **OPS62** — fail-fast estrutural do CI: `ci-pr.yml` e `ci.yml` deixaram de ter jobs irmãos. Cada workflow agora tem **um único job `checks` sequencial** (ci-pr: guards de conteúdo → lint → format → typecheck → knip → cycles → unit → int → build → e2e, com skips do `ci-scope.mjs` agora como step com outputs; ci.yml: mesma mecânica full + deploy OPS53 intacto), então **step vermelho interrompe o job, o run falha e o slot do runner é liberado imediatamente** — sem API de cancel (o Forgejo 9.0.3 não tem), sem watchdog, sem upgrade do Forgejo no homeserver (decisão X1 do gate de execução substitui o watchdog+upgrade da intenção; o aceite "API de cancel/upgrade" e "mesmos jobs, mesma cascata" foi revisado pelo humano — o que o CI _verifica_ permanece idêntico, só o envelope muda). Diagnóstico preservado: steps têm logs próprios. O job id `checks` mantém o contexto `CI (PR) / checks` que o automerger/`waitForChecks` e a branch protection esperam (OPS61). Paralelismo do e2e recuperado por `PLAYWRIGHT_WORKERS=4` num único processo (substitui os 2 shards × 2 workers; mesma carga); **2 services Postgres isolados** (5433 int, 5434 build+e2e) preservam a topologia de hoje (e2e nunca herda resíduo de fixtures do int); 1 checkout + 1 `pnpm install` por run (antes ~10). Plumbing morto removido: `e2eShardConfig` e o output `e2e_shards` do classifier (sem consumidor). Guards de conteúdo movidos para o início do pipeline (falham rápido e são PR-scoped). Docs: tabela CI do AGENT-OPS reescrita + linha morta do `styfle/cancel-workflow-action` (removida no OPS50) apagada. Sem migration/access/Consent/UI. Testes: invariantes novos em `ciSkipInvariants.unit.spec.ts` (um único job `checks`, sem `matrix:`/`fail-fast:`/`runs-on: host` no ci-pr, `PLAYWRIGHT_WORKERS: 4`, guards antes do lint, deploy gated em `needs: [checks]`); `testAffected.unit.spec.ts` sem o describe do shard config. **Validação ao vivo na própria entrega:** o ci-pr deste PR já é o envelope novo — o run verde prova a suíte inteira no job único; a medição do tempo de parede fica registrada no run. Custo aceito (X1): run verde ~21 min esperados (vs ~15–18 com jobs paralelos) e e2e vermelho é o caso mais lento de veredito; débito registrado no impl plan (matrix fail-fast Y) se a fila do runner apertar. Impl: `docs/plans/ops62-ci-fail-fast-forgejo-impl.md` (a intenção fica como histórico do gate).
|
||||
|
||||
**Recently resolved (2026-08-18):** **OPS62** — fail-fast estrutural do CI: `ci-pr.yml` e `ci.yml` deixaram de ter jobs irmãos. Cada workflow agora tem **um único job `checks` sequencial** (ci-pr: guards de conteúdo → lint → format → typecheck → knip → cycles → unit → int → build → e2e, com skips do `ci-scope.mjs` agora como step com outputs; ci.yml: mesma mecânica full + deploy OPS53 intacto), então **step vermelho interrompe o job, o run falha e o slot do runner é liberado imediatamente** — sem API de cancel (o Forgejo 9.0.3 não tem), sem watchdog, sem upgrade do Forgejo no homeserver (decisão X1 do gate de execução substitui o watchdog+upgrade da intenção; o aceite "API de cancel/upgrade" e "mesmos jobs, mesma cascata" foi revisado pelo humano — o que o CI _verifica_ permanece idêntico, só o envelope muda). Diagnóstico preservado: steps têm logs próprios. O job id `checks` mantém o contexto `CI (PR) / checks` que o automerger/`waitForChecks` e a branch protection esperam (OPS61). Paralelismo do e2e recuperado por `PLAYWRIGHT_WORKERS=4` num único processo (substitui os 2 shards × 2 workers; mesma carga); **2 services Postgres isolados** (int e build+e2e) com **portas host derivadas do `run_id`** (`5433/5533 + run_id % 100` — o job único segura as duas portas pelo run inteiro; runs concorrentes colidiriam em portas fixas) preservam a topologia de hoje (e2e nunca herda resíduo de fixtures do int); 1 checkout + 1 `pnpm install` por run (antes ~10). Plumbing morto removido: `e2eShardConfig` e o output `e2e_shards` do classifier (sem consumidor). Guards de conteúdo movidos para o início do pipeline (falham rápido e são PR-scoped). `/simplify` (3 reviewers): P1 aplicados — `set -euo pipefail` no step `scope` (crash do classifier com outputs vazios silenciaria skips → false green) e portas por run (acima); P2 aplicados — pins de invariantes ancorados em chaves YAML (comentários não satisfazem `needs: [checks]`/`matrix:`/`fail-fast:`), pin do wiring crítico `id: scope` + `if: e2e_mode == 'full'` (perdê-lo pula a suíte em silêncio), comentário morto do sharding em `playwright.config.ts` e `docs/TESTING.md` corrigidos (ainda citavam `.github/workflows` e jobs paralelos); P3 — JSDoc do `ci-scope.mjs` e redação do impl plan; o P2 do `waitForChecks` (timeout 30 min vs job 50 min) fica como débito de CI anotado no impl. Docs: tabela CI do AGENT-OPS reescrita + linha morta do `styfle/cancel-workflow-action` (removida no OPS50) apagada. Sem migration/access/Consent/UI. Testes: invariantes novos em `ciSkipInvariants.unit.spec.ts` (um único job `checks`, sem `matrix:`/`fail-fast:`/`runs-on: host` no ci-pr, `PLAYWRIGHT_WORKERS: 4`, guards antes do lint, deploy gated em `needs: [checks]`); `testAffected.unit.spec.ts` sem o describe do shard config. **Validação ao vivo na própria entrega:** o ci-pr deste PR já é o envelope novo — o run verde prova a suíte inteira no job único; a medição do tempo de parede fica registrada no run. Custo aceito (X1): run verde ~21 min esperados (vs ~15–18 com jobs paralelos) e e2e vermelho é o caso mais lento de veredito; débito registrado no impl plan (matrix fail-fast Y) se a fila do runner apertar. Impl: `docs/plans/ops62-ci-fail-fast-forgejo-impl.md` (a intenção fica como histórico do gate).
|
||||
|
||||
**Recently resolved (2026-08-17):** **OPS51** — o banco de produção saiu do Neon para o Postgres do homeserver (`teqo_1313`, container `teqo-1313`), sem mudança de config: o `~/stack/teqo-1313.env` já apontava para o banco preparado (criado 16/08 com role própria + cadeia completa de 56 migrations + zero conteúdo — verificado), então restaurar = a produção "apareceu" no container. **Divergência deliberada da intenção (questão A/B validada como B):** rejeitado criar `teqo_prod` novo — o argumento "o banco do 1313 tem vida própria" não se sustenta (o site 1313 É a produção deste repo) e o OPS53 já trata `teqo_1313` como produção. Execução 100% no homeserver (PII nunca tocou a workstation): URL do Neon puxada do Vercel (`vercel env pull`, projeto `jorgesolla`, `DATABASE_URL_UNPOOLED` → `NEON_DATABASE_URL` em `~/stack/.env` chmod 600), `pg_dump -Fc` (Neon PG 17.10) → `/srv/hdd/backups/teqo-neon-pre-migracao/teqo-neon-full-20260817-204800.dump` (5,8 MB, sha256 `f12196f2…`), `pg_restore --no-owner --no-privileges` como a role `teqo_1313` (drop schema + grant, padrão do `db-pull`), verificado: contagens por tabela idênticas ao Neon (1919 contatos, 39 posts, 1487 assinaturas, 1484 subscrições, 35 campaign_users, 392 lideranças, 2 pledges, 435 municípios, 6 atividades; supporter segue 0 — blocker LGPD ainda de pé), sequences no topo (`last_value == max(id)`), objetos com dono `teqo_1313`, `payload migrate` no-op (56 = 56, serviço de maintenance `teqo-1313-migrate`), 3 credenciais WebAuthn + 34 sessions + 5 chaves Consent restauradas (RP ID não mudou — biometria segue valendo). Smoke sem deploy: `/`, artigo com conteúdo restaurado, `/campanha/login`, `/admin`, `POST /api/revalidate` (`posts` + `global_privacy-policy`), WebAuthn `login-options`, barreira 307 sem sessão — verdes; smoke de browser (login, biometria, convites) validado manualmente. **Rollback documentado:** Neon nunca foi escrito (só lido); voltar = re-apontar `DATABASE_URL` do `teqo-1313.env` para o Neon + `compose up -d`. **Débito registrado (→ OPS52):** as 40 rows de `media` têm URLs relativas (`/api/media/file/…`) sem arquivos em lugar nenhum — capas dão 500 no homeserver e **404 no próprio Vercel/produção atual** (estado pré-existente; migração manteve paridade). Repo: plano da intenção concluído, `AGENTS.md` e help de `db:pull`/`check-push-chain` sem a referência ao Neon; infra-solla `STATE.md` atualizado à parte.
|
||||
|
||||
**Recently resolved (2026-08-17):** **OPS53** — o deploy do site 1313 virou etapa do CI: mergir em `main` com a suite verde == site atualizado em `jorgesolla1313.com.br`, sem passo manual. O job `deploy` em `.forgejo/workflows/ci.yml` roda no host do runner (workstation, `runs-on: host`) e streama `scripts/deploy-homeserver.sh` para o homeserver via SSH (`ssh homeserver "bash -s -- <sha>"`); o script remoto (bash puro, homeserver sem dependências novas) faz: guard de HEAD (`git ls-remote` de `main` == SHA do job, senão skip "stale"), `flock` de serialização, workspace `~/teqo-deploy` (clone do Forgejo local), `docker login localhost:5000` (REGISTRY_USER/PASSWORD de `~/stack/.env`), build dos stages `runner`+`migrator` no homeserver (`--network stack_default` para a geração estática alcançar o `postgres:5432` do compose; secrets de `~/stack/teqo-1313.env` via BuildKit, sem camadas/logs), push no registry local, swap dos tags de imagem no compose (com backup `docker-compose.yml.pre-<sha>`), **migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout**, `docker compose up -d`, espera do healthcheck e smoke (`/`, `/campanha/login`, `/admin`, barreira 307, WebAuthn `login-options` com body JSON, `POST /api/revalidate` com o secret real — rehearsal validado contra o site vivo). Falha pós-swap → rollback automático best-effort (restore do backup + `up -d` da imagem anterior) + job vermelho. **Divergência deliberada da intenção (fato novo):** a intenção assumia o runner no homeserver; verificado (`infra-solla/STATE.md` + ao vivo) que o runner é a `workstation-runner` na workstation e o stack de produção vive no homeserver — a abordagem A foi corrigida para "job `runs-on: host` na workstation + todo o deploy no homeserver via SSH", o que manteve **zero secrets novos no Forgejo** e zero mudança de infra no compose. Nenhum secret no repo; envs seguem chmod-600 no homeserver. Runbook (rollback manual, falhas conhecidas, decisão de segurança do host label): `docs/ops/teqo-1313-deploy.md` (novo). Guarda estrutural nova: `tests/unit/deployScript.unit.spec.ts` (bash -n + invariantes do pipeline: HEAD guard, BuildKit secrets, ordem migrate→rollout, smoke). AGENTS.md/AGENT-OPS.md atualizados. Primeiro deploy real = o merge deste próprio item (tracer bullet); infra-solla `STATE.md` anotado à parte.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Three layers, all local-database-only (see `local-database` skill; tests refuse
|
|||
- **Integration** (`tests/int/**/*.int.spec.ts`, 71 specs): real Payload + Postgres (`teqo_test`); campaign fixtures allocate seeded municipalities via a Postgres sequence so parallel spec files never share one, and purge residue on claim. Since the miss #73 guardrail (Pass 4), specs that mutate municipalities must use the allocator (no pinned slugs) — enforced by `testMunicipalityAllocatorConventions`. Shared consent rows with stable keys are leased (`tests/helpers/testDatabaseLease.ts`), never owned.
|
||||
- **E2E** (`tests/e2e`, Playwright, 17 specs / 51 cases (Pass 5, 2026-08-03; was 48 at Pass 4)): smoke-level; most campaign behavior is pinned at the int layer.
|
||||
|
||||
CI (`.github/workflows/ci-pr.yml`) runs parallel jobs — lint (zero warnings), format, typecheck, knip, cycles, unit/int (**affected by the PR diff**: `vitest --changed` against the PR base via `scripts/test-affected.mjs`, full-suite fallback on high-risk paths), e2e (manifest path→spec via `scripts/e2e-affected.mjs` + `scripts/lib/e2e-affected-manifest.mjs`, blocking; production build + `list`/`github` reporters in CI) and build — against a Postgres 17 service with **`db:seed:minimal`**. Push to `main` (`ci.yml`) always runs the full net including e2e.
|
||||
CI (`ci-pr.yml`) is a **single sequential job `checks`** (OPS62 X1): guards → lint (zero warnings) → format → typecheck → knip → cycles → unit/int (**affected by the PR diff**: `vitest --changed` against the PR base via `scripts/test-affected.mjs`, full-suite fallback on high-risk paths) → build → e2e (manifest path→spec via `scripts/e2e-affected.mjs` + `scripts/lib/e2e-affected-manifest.mjs`, blocking; production build + `list`/`github` reporters in CI) — against two isolated Postgres 17 services (int / build+e2e) with **`db:seed:minimal`**. Fail-fast is structural: a failed step stops the run; e2e runs with `PLAYWRIGHT_WORKERS=4`. Push to `main` (`ci.yml`) always runs the full net including e2e.
|
||||
|
||||
**Local e2e before push (same blast radius as PR CI):** `pnpm test:e2e:affected` — runs `e2e-affected.mjs` against `origin/stage`, migrates/seeds, then `pnpm test:e2e` with the selected specs (or the full suite on high-risk diffs). Forward extra Playwright args after `--` for a narrowed repro. Opt into production mode (after fixing the known municipalities-list prod client bug) with `E2E_PROD=1 pnpm test:e2e:affected`.
|
||||
|
||||
|
|
|
|||
1
docs/changelog/2026-08-18-ops62.md
Normal file
1
docs/changelog/2026-08-18-ops62.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**Recently resolved (2026-08-18):** **OPS62** — fail-fast estrutural do CI: `ci-pr.yml` e `ci.yml` deixaram de ter jobs irmãos. Cada workflow agora tem **um único job `checks` sequencial** (ci-pr: guards de conteúdo → lint → format → typecheck → knip → cycles → unit → int → build → e2e, com skips do `ci-scope.mjs` agora como step com outputs; ci.yml: mesma mecânica full + deploy OPS53 intacto), então **step vermelho interrompe o job, o run falha e o slot do runner é liberado imediatamente** — sem API de cancel (o Forgejo 9.0.3 não tem), sem watchdog, sem upgrade do Forgejo no homeserver (decisão X1 do gate de execução substitui o watchdog+upgrade da intenção; o aceite "API de cancel/upgrade" e "mesmos jobs, mesma cascata" foi revisado pelo humano — o que o CI _verifica_ permanece idêntico, só o envelope muda). Diagnóstico preservado: steps têm logs próprios. O job id `checks` mantém o contexto `CI (PR) / checks` que o automerger/`waitForChecks` e a branch protection esperam (OPS61). Paralelismo do e2e recuperado por `PLAYWRIGHT_WORKERS=4` num único processo (substitui os 2 shards × 2 workers; mesma carga); **2 services Postgres isolados** (int e build+e2e) com **portas host derivadas do `run_id`** (`5433/5533 + run_id % 100` — o job único segura as duas portas pelo run inteiro; runs concorrentes colidiriam em portas fixas) preservam a topologia de hoje (e2e nunca herda resíduo de fixtures do int); 1 checkout + 1 `pnpm install` por run (antes ~10). Plumbing morto removido: `e2eShardConfig` e o output `e2e_shards` do classifier (sem consumidor). Guards de conteúdo movidos para o início do pipeline (falham rápido e são PR-scoped). `/simplify` (3 reviewers): P1 aplicados — `set -euo pipefail` no step `scope` (crash do classifier com outputs vazios silenciaria skips → false green) e portas por run (acima); P2 aplicados — pins de invariantes ancorados em chaves YAML (comentários não satisfazem `needs: [checks]`/`matrix:`/`fail-fast:`), pin do wiring crítico `id: scope` + `if: e2e_mode == 'full'` (perdê-lo pula a suíte em silêncio), comentário morto do sharding em `playwright.config.ts` e `docs/TESTING.md` corrigidos (ainda citavam `.github/workflows` e jobs paralelos); P3 — JSDoc do `ci-scope.mjs` e redação do impl plan; o P2 do `waitForChecks` (timeout 30 min vs job 50 min) fica como débito de CI anotado no impl. Docs: tabela CI do AGENT-OPS reescrita + linha morta do `styfle/cancel-workflow-action` (removida no OPS50) apagada. Sem migration/access/Consent/UI. Testes: invariantes novos em `ciSkipInvariants.unit.spec.ts` (um único job `checks`, sem `matrix:`/`fail-fast:`/`runs-on: host` no ci-pr, `PLAYWRIGHT_WORKERS: 4`, guards antes do lint, deploy gated em `needs: [checks]`); `testAffected.unit.spec.ts` sem o describe do shard config. **Validação ao vivo na própria entrega:** o ci-pr deste PR já é o envelope novo — o run verde prova a suíte inteira no job único; a medição do tempo de parede fica registrada no run. Custo aceito (X1): run verde ~21 min esperados (vs ~15–18 com jobs paralelos) e e2e vermelho é o caso mais lento de veredito; débito registrado no impl plan (matrix fail-fast Y) se a fila do runner apertar. Impl: `docs/plans/ops62-ci-fail-fast-forgejo-impl.md` (a intenção fica como histórico do gate).
|
||||
83
docs/plans/ops62-ci-fail-fast-forgejo-impl.md
Normal file
83
docs/plans/ops62-ci-fail-fast-forgejo-impl.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Impl: OPS62 — CI: fail-fast estrutural por job único sequencial (X1)
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-18
|
||||
Issue: #59
|
||||
Intenção: docs/plans/ops62-ci-fail-fast-forgejo.md
|
||||
Appetite restante: ~1 dia eng (corte: nada de watchdog/API/upgrade — decidido no gate de execução)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** um run de PR (ou de `main`) com **qualquer verificação vermelha** falha o run imediatamente — nada continua rodando depois da primeira falha; o runner é liberado cedo; `checks` vermelho sem esperar o fim dos demais; logs do que já rodou preservados; a suíte **verificada** não muda (mesmas ferramentas e conteúdo: lint, format, typecheck, knip, cycles, unit, int, build, e2e, guards); sem bypass novo além do `--no-verify` documentado.
|
||||
- **O que NÃO negociar:** anti-goals — não perder diagnóstico (steps preservam logs próprios), não mudar o que o CI _verifica_ (só o envelope de jobs muda), `checks` como gate único (contexto `CI (PR) / checks` preservado para waitForChecks/branch protection); aceitar a perda dos logs do segundo shard de e2e (o e2e perde o paralelismo de jobs — 4 workers num único processo; o gate aceitou perder o shard irmão, e aqui o sharding em si é compensado por workers).
|
||||
- **Divergência do aceite da intenção (aprovada pelo humano no gate de execução, 2026-08-18):** o aceite "Forgejo do homeserver roda versão com a API de cancel" e "mesmos jobs, mesma cascata" são **substituídos** — a arquitetura escolhida (X1: 1 job sequencial) torna a API de cancel e o upgrade **desnecessários** (fail-fast é estrutural) e muda o agrupamento de jobs (o que é verificado permanece idêntico). O plano de intenção fica como histórico; este impl plan é a forma aprovada.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph job[Um único job `checks` por workflow<br/>1 container runner · 1 checkout · 1 pnpm install · 1 slot]
|
||||
S1[scope classifier] --> S2[guards de conteúdo]
|
||||
S2 --> S3[check-test-locations] --> L[lint] --> F[format] --> T[typecheck]
|
||||
T --> K[knip] --> C[cycles] --> U[unit]
|
||||
U --> M1[migrate + seed 5433] --> I[int]
|
||||
I --> M2[migrate + seed 5434] --> B1[build] --> B2[build .next-e2e]
|
||||
B2 --> E[e2e · PLAYWRIGHT_WORKERS=4]
|
||||
end
|
||||
L -->|"falha em qualquer step<br/>(if: steps.scope.outputs …)"| FIM
|
||||
F -->|"steps seguintes não rodam"| FIM[run falha imediato<br/>slot liberado · checks vermelho<br/>logs dos steps rodados preservados]
|
||||
```
|
||||
|
||||
**Opções consideradas (gate de execução):** X1 | Y | Z
|
||||
**Recomendação (escolhida pelo humano):** **X1** — 1 job único por workflow, steps sequenciais, fail-fast **estrutural**: falhou um step → os seguintes nem começam → job falha → run falha → slot liberado. Zero dependência de API de cancel (a rota nem existe no Forgejo 9.0.3), zero watchdog (não há slot extra, token, upgrade, nem risco de infra no homeserver — o Forgejo **não** é atualizado). O paralelismo do e2e é recuperado por `PLAYWRIGHT_WORKERS=4` (o Playwright roda workers dentro de um processo — mesma carga de 2 shards × 2 workers de hoje).
|
||||
|
||||
**Rejeitadas:**
|
||||
|
||||
- **Y — fase 1 sequencial + fase 2 em matrix `fail-fast: true`:** preserva verde ≈ atual e cancela irmãos nativamente, mas depende de o Forgejo 9.0.3 cancelar de fato os irmãos de matrix (comportamento de servidor a verificar ao vivo; se falhasse, virava "tudo roda até o fim" na fase 2), mantém 2 slots em vez de 1, e o YAML de matrix por `mode` é mais complexo de manter que steps em série. **Rejeitada** por simplicidade máxima pedida.
|
||||
- **Z — watchdog + upgrade do Forgejo 16:** o risco de infra (upgrade de produção no homeserver, compatibilidade runner × v16) deixou de se justificar quando o fail-fast estrutural cobre o aceite com menos peças. **Rejeitada.**
|
||||
- **Watchdog em `runs-on: host`:** ci-pr jamais roda host (pin OPS53). **Rejeitada.**
|
||||
- **Paralelizar steps com processos `&`/`wait`:** logs interleaved + fail-fast manual = mini-watchdog em bash. **Rejeitada.**
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`.forgejo/workflows/ci-pr.yml`** (reescrito — X1): 1 job `checks` (nome default → contexto `CI (PR) / checks` preservado; **o nome do job é o contrato** do waitForChecks OPS61-2b e da branch protection). Steps em série, cada um com `if:` de skip do classifier: `scope` (`id: scope`; outputs `code_mode/build_mode/test_mode/e2e_mode/e2e_specs` via `$GITHUB_OUTPUT` — mesmo comando do job atual, com `set -euo pipefail`), guards de conteúdo (changelog append-only, aggregate, conflict markers, plans-only — movidos para o começo: falham rápido e são sobre o PR), `check-test-locations`, `lint`, `format:check`, `typecheck`/`knip`/`cycles` (`if: steps.scope.outputs.code_mode != 'none'`), `unit` (full/changed), `migrate`+`seed:minimal` (DB int), `int` (full/changed), `migrate`+`seed:minimal` (DB build/e2e), `build` (if `build_mode != 'none'`), `build .next-e2e` + `e2e` (if `e2e_mode != 'none'`; full = `pnpm test:e2e` sem shard com `PLAYWRIGHT_WORKERS=4`; selected = specs do output). **2 services Postgres** com **portas host derivadas do `run_id`** (`5433 + run_id % 100` int, `5533 + run_id % 100` build/e2e — o job único segura as duas portas pelo run inteiro e runs concorrentes no runner compartilhado colidiriam em portas fixas; resíduo ~1% é ruidoso e resolvido no push seguinte; mesma topologia gateway OPS50) preservando o isolamento de hoje (o e2e nunca herda a sujeira dos testes int; o build de prod e o de e2e compartilham o service build/e2e). `timeout-minutes: 50` (soma esperada ~21 min + margem; o run nunca segura o slot além disso). Sem `needs`/`if: always()`/matrix/jq de rollup (não há irmãos). Comentário de cabeçalho reescrito: OPS62 substitui o parágrafo "Forgejo has no cancel-run plumbing; job timeouts cover that role" — o fail-fast é estrutural (1 job), sem API de cancel. **Trade-off assumido:** `migrate`+`seed:minimal` do int rodam também em PR docs-only (mode none) — intencional (a cadeia stale é pega em todo PR), custo ~1–2 min por PR de docs.
|
||||
- **`.forgejo/workflows/ci.yml`** (reescrito — X1): mesma estrutura no `main`; `static` (já era job único sequencial) se funde ao job `checks` com int/build/e2e; `deploy` permanece `needs: [checks]` (inalterado — OPS53). `timeout-minutes: 50`.
|
||||
- **`tests/unit/ciSkipInvariants.unit.spec.ts`** (editar — pin da arquitetura): invariantes novos — os dois workflows têm **exatamente um job** cujo id é `checks`; os steps-chave existem na ordem (lint antes de e2e; guards antes de lint); o ci-pr não contém `runs-on: host` (pin OPS53), nem `matrix:`, nem `fail-fast:`; `PLAYWRIGHT_WORKERS: 4` presente; `.next-e2e` continua pinado (invariante existente); o ci.yml mantém o job `deploy` com `needs: [checks]`.
|
||||
- **`docs/AGENT-OPS.md`** (editar): tabela de CI — ci-pr/ci agora 1 job `checks` sequencial com fail-fast estrutural; remover menção a "fase 1→2 em cascata" se houver; nota "Dono do PR, dono do CI" intacta (o próprio PR define seu CI).
|
||||
- **`docs/changelog/2026-08-18-ops62.md`** (novo) + `pnpm changelog:build`.
|
||||
- **Migration / Access / Consent / UI:** N/A (Impeccable A).
|
||||
- **Fora do repo:** nada — **o upgrade do Forgejo é cancelado** (decisão X1); o homeserver não é tocado.
|
||||
|
||||
### Dados → forma (se aplicável)
|
||||
|
||||
N/A — chore de DX/processo.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Workflows X1** — reescrever `ci-pr.yml` e `ci.yml` (job único, 2 services, steps condicionais); `bash`/YAML válido (sem parser YAML no repo — validar por leitura + os invariantes). Gates parciais: `pnpm lint`, `format:check`, `typecheck`, `knip`, `check:cycles`.
|
||||
2. **Invariantes** — `ciSkipInvariants.unit.spec.ts` (pin da arquitetura: 1 job `checks`, ordem dos steps, sem host/matrix/fail-fast no ci-pr, deploy no ci.yml); `pnpm test:unit` (spec novo).
|
||||
3. **Docs + fechamento** — AGENT-OPS, changelog + `changelog:build`; gates completos (`pnpm test` int incluso, `pnpm build`); push via `pnpm push`.
|
||||
4. **Validação ao vivo (no próprio PR do OPS62)** — o ci-pr deste PR **já é o X1**: (a) run verde prova que a suíte inteira roda no envelope novo; (b) o CI do PR com um step quebrado (ex.: antes do merge, um push deliberado com `lint` quebrado) prova o fail-fast: run falha no primeiro vermelho, steps seguintes nem começam, logs preservados — verificação documentada no fechamento. Sem passo de infra pós-merge.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- **Watchdog, API de cancel, upgrade do Forgejo, token PAT:** fora — X1 não precisa de nenhum (a intenção fica como histórico).
|
||||
- **Mudar o que o CI verifica:** fora — mesmas ferramentas, mesmo conteúdo, mesma ordem de suíte; só o envelope muda.
|
||||
- **Rerun granular de steps** (re-tentar só o que falhou): o Forgejo não tem rerun por API (session-only); re-push continua sendo o caminho — como hoje.
|
||||
- **Voltar a shards de e2e em jobs separados:** se o tempo de e2e full com 4 workers incomodar (medida no run deste PR), o caminho é Y (matrix fail-fast), não o watchdog — registrar como débito se medir > ~10 min.
|
||||
- **Timeout do `waitForChecks` (30 min) × job `checks` (50 min)** — débito de CI (revisão /simplify P2): um run verde pode passar dos 30 min com fila do runner/retries de e2e e o automerger lançar "Timeout esperando checks" num PR verde. Gatilho para corrigir: primeiro timeout observado. Alternativa curta (1 linha) registrada: alinhar `timeoutMs` do `waitForChecks`/`autoMerge` no `forgejo-api.mjs` ao timeout do job (50–55 min).
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Tempo de parede do run verde aumenta** (~15–18 → ~21 min esperados: perde a paralelização da fase 1 e os 2 shards de e2e; ganha 1 checkout/install e 1 migrate/seed). **Mitigação:** medido no run deste PR; e2e com 4 workers compensa os shards; o aceite X1 do humano cobre o custo.
|
||||
- **E2E vermelho é o caso mais lento** (veredito só após todos os steps anteriores). **Mitigação:** guards/lint/format/typecheck falham cedo (os casos comuns); e2e é a falha menos frequente e o report mostra o spec que falhou. Débito registrado (Y) se a frequência exigir.
|
||||
- **Step `scope` com outputs e steps condicionais** — troca `needs.scope.outputs` por `steps.scope.outputs`: mesma semântica, semântica de skip idêntica (none → steps skipped, não falham). Validado por um PR de docs (mode none) no próprio repo.
|
||||
- **1 service compartilhado int/e2e** — rejeitado no plano (2 services 5433/5434 preservam o isolamento; risco zero de fixture colidir).
|
||||
- **`checks` como job único muda o status check de "rollup que espera needs" para "o próprio run"** — contexto e nome preservados (`checks`), então waitForChecks (OPS61-2b) e branch protection continuam vendo o contexto certo; quando o job falha, o status do sha é `failure` — veredito mais direto ainda.
|
||||
- **Run de main (`ci.yml`)**: com deploy `needs: checks`, qualquer falha agora impede o deploy **imediatamente** (hoje também, mas só no fim dos irmãos) — mesma garantia, veredito mais cedo.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto coberto (forma X1 aprovada pelo humano): qualquer verificação vermelha falha o run no ato; runner liberado cedo; `checks` vermelho sem esperar o fim dos demais; logs preservados (steps com logs próprios); suíte verificada idêntica; sem bypass novo.
|
||||
- [x] Invariantes AGENTS/engineering-standards: sem migration/access/Consent/UI; pin da arquitetura em spec (1 job `checks`, sem host no ci-pr, sem matrix/fail-fast); identifiers em inglês; copy pt-BR só em labels (inalterado).
|
||||
- [x] Testes previstos: unit dos invariantes de workflow (não há write path de app).
|
||||
|
|
@ -48,8 +48,9 @@ if (!Number.isInteger(workers) || workers < 2) {
|
|||
* compiles; against a production build they are pure cost — measured 2026-08-10
|
||||
* (OPS34): the 25 prewarm requests plus the 4-test frontend/admin tail behind
|
||||
* the ~30-file campaign family. Prod mode therefore drops the setup project and
|
||||
* the dependencies so all families run in parallel; CI shards the suite across
|
||||
* runners instead (ci.yml / ci-pr.yml pass `--shard`). Dev mode is unchanged.
|
||||
* the dependencies so all families run in parallel; CI runs the suite in one
|
||||
* process with `PLAYWRIGHT_WORKERS` (ci.yml / ci-pr.yml, OPS62 X1). Dev mode
|
||||
* is unchanged.
|
||||
* Note the chain is an ordering guarantee for dev cold compiles, not a data
|
||||
* dependency: specs are self-contained in both modes (fixtures + advisory
|
||||
* locks), so dropping it in prod cannot reorder shared fixtures.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* ci-scope — single classifier for PR job-level skips. Prints JSON:
|
||||
* ci-scope — single classifier for PR step-level skips. Prints JSON:
|
||||
* { base, code, build, test, e2e } where each of code/build/test/e2e is the
|
||||
* classify* / selectE2eSpecs result. Used by the `scope` job in ci-pr.yml.
|
||||
* classify* / selectE2eSpecs result. Used by the `scope` step (id: scope) in
|
||||
* ci-pr.yml — its outputs drive `if:` on the heavy steps.
|
||||
*
|
||||
* Base ref: $GITHUB_BASE_REF (PR target) or origin/main locally.
|
||||
*/
|
||||
|
|
@ -13,7 +14,6 @@ import {
|
|||
classifyBuildScope,
|
||||
classifyStaticScope,
|
||||
classifyTestScope,
|
||||
e2eShardConfig,
|
||||
selectE2eSpecs,
|
||||
} from './lib/test-affected-core.mjs'
|
||||
|
||||
|
|
@ -34,9 +34,6 @@ try {
|
|||
build: { mode: 'build', reason: full.reason },
|
||||
test: { ...full },
|
||||
e2e: { mode: 'full', specs: [], reason: full.reason, unmapped: [] },
|
||||
// The scope job always reads e2e_shards.matrix/total — keep the fallback
|
||||
// total (ci-pr.yml's fromJson must never receive a null/empty matrix).
|
||||
e2e_shards: e2eShardConfig('full'),
|
||||
}),
|
||||
)
|
||||
process.exit(0)
|
||||
|
|
@ -67,6 +64,5 @@ console.log(
|
|||
build: classifyBuildScope(files),
|
||||
test: classifyTestScope(files),
|
||||
e2e,
|
||||
e2e_shards: e2eShardConfig(e2e.mode),
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -255,21 +255,6 @@ export function selectE2eSpecs(files, manifest) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CI job-level parallelism for the e2e suite (OPS34). The full run splits
|
||||
* across 2 shards (`--shard=N/total`); selected and none stay on a single
|
||||
* runner — a second build for a handful of specs is pure waste. Measured
|
||||
* 2026-08-10 (ci.yml run 31412425553): build 172s + tests 196s + ~54s fixed,
|
||||
* so shard 1 and 2 each land ≈ 5–5.5 min. Revisit at 3 shards if the sharded
|
||||
* wall drifts past ~6.5 min or the test split gets lopsided; ci.yml hardcodes
|
||||
* the same [1, 2] / 2 literal with a comment pointing back here.
|
||||
* @param {'full' | 'selected' | 'none'} mode
|
||||
* @returns {{ matrix: number[], total: number }}
|
||||
*/
|
||||
export function e2eShardConfig(mode) {
|
||||
return mode === 'full' ? { matrix: [1, 2], total: 2 } : { matrix: [1], total: 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a list of repo-relative paths and return misplaced spec/test files.
|
||||
* @param {string[]} paths
|
||||
|
|
|
|||
|
|
@ -75,6 +75,43 @@ describe('ciSkipInvariants', () => {
|
|||
expect(CANONICAL_E2E_SPEC_SUFFIX).toBe('.e2e.spec.ts')
|
||||
})
|
||||
|
||||
it('OPS62 X1: single sequential `checks` job with structural fail-fast', () => {
|
||||
for (const file of ['.forgejo/workflows/ci.yml', '.forgejo/workflows/ci-pr.yml']) {
|
||||
const source = readFileSync(join(repoRoot, file), 'utf8')
|
||||
// Exactly one job id `checks` — the status context automerge/branch
|
||||
// protection wait on. Fail-fast is structural: no siblings, no matrix,
|
||||
// no strategy fail-fast (anchored as YAML keys so comments can't trip).
|
||||
expect(source.match(/^jobs:\n checks:\n/m) ?? [], file).toHaveLength(1)
|
||||
expect(source, file).not.toMatch(/^ {2,4}(?:matrix|fail-fast):/m)
|
||||
// e2e runs as one process with 4 workers (replaces the 2-shard matrix).
|
||||
expect(source, file).toContain('PLAYWRIGHT_WORKERS: 4')
|
||||
// Steps keep their own logs so diagnosis survives a failure.
|
||||
expect(source, file).toContain('name: Lint')
|
||||
expect(source, file).toContain('name: E2E tests')
|
||||
}
|
||||
|
||||
const pr = readFileSync(join(repoRoot, '.forgejo/workflows/ci-pr.yml'), 'utf8')
|
||||
// Content guards fail before the suite (cheap, PR-scoped, fail fast).
|
||||
const guard = pr.indexOf('name: Changelog is append-only')
|
||||
const lint = pr.indexOf('name: Lint')
|
||||
const e2e = pr.indexOf('name: E2E tests (full suite')
|
||||
expect(guard).toBeGreaterThan(-1)
|
||||
expect(lint).toBeGreaterThan(guard)
|
||||
expect(e2e).toBeGreaterThan(lint)
|
||||
// ci-pr never runs on the host (OPS53 pin).
|
||||
expect(pr).not.toContain('runs-on: host')
|
||||
// The skip wiring is the critical piece: losing the `scope` step id or
|
||||
// the e2e gate silently skips the suite with a green run (the opposite
|
||||
// of the fail-fast this delivery sells).
|
||||
expect(pr).toMatch(/id: scope\n/)
|
||||
expect(pr).toMatch(/if: steps\.scope\.outputs\.e2e_mode == 'full'/)
|
||||
|
||||
// main keeps the deploy gated on the checks job (OPS53) — anchored as a
|
||||
// YAML key so the header comment cannot satisfy the pin.
|
||||
const main = readFileSync(join(repoRoot, '.forgejo/workflows/ci.yml'), 'utf8')
|
||||
expect(main).toMatch(/^ needs: \[checks\]\n/m)
|
||||
})
|
||||
|
||||
it('keeps local e2e build artifacts outside the development dist directory', () => {
|
||||
for (const file of [
|
||||
'.forgejo/workflows/ci.yml',
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
classifyBuildScope,
|
||||
classifyStaticScope,
|
||||
classifyTestScope,
|
||||
e2eShardConfig,
|
||||
HIGH_RISK_EXACT,
|
||||
HIGH_RISK_PREFIXES,
|
||||
selectE2eSpecs,
|
||||
|
|
@ -153,21 +148,3 @@ describe('selectE2eSpecs (OPS5)', () => {
|
|||
expect(result.specs).toContain('admin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('e2eShardConfig (OPS34)', () => {
|
||||
it('splits the full suite across 2 shards', () => {
|
||||
expect(e2eShardConfig('full')).toEqual({ matrix: [1, 2], total: 2 })
|
||||
})
|
||||
|
||||
it('keeps selected and skipped runs on a single runner', () => {
|
||||
expect(e2eShardConfig('selected')).toEqual({ matrix: [1], total: 1 })
|
||||
expect(e2eShardConfig('none')).toEqual({ matrix: [1], total: 1 })
|
||||
})
|
||||
|
||||
it('pins the ci.yml matrix literal against the single source', () => {
|
||||
const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '../..')
|
||||
const workflow = readFileSync(join(repoRoot, '.forgejo/workflows/ci.yml'), 'utf8')
|
||||
const { matrix } = e2eShardConfig('full')
|
||||
expect(workflow).toContain(`shard: [${matrix.join(', ')}]`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue