teqo/.forgejo/workflows/ci-pr.yml
Francisco Solla b51f745f66
Some checks failed
Agent PR Ready + auto-merge / ready-automerge (pull_request) Has been skipped
CI (PR) / docs-guards (pull_request) Successful in 6s
CI (PR) / plans-only-closes (pull_request) Successful in 6s
CI (PR) / scope (pull_request) Successful in 31s
CI (PR) / lint (pull_request) Successful in 40s
CI (PR) / format (pull_request) Successful in 57s
CI (PR) / typecheck (pull_request) Successful in 33s
CI (PR) / knip (pull_request) Successful in 33s
CI (PR) / cycles (pull_request) Successful in 34s
CI (PR) / unit (pull_request) Successful in 45s
CI (PR) / build (pull_request) Has been skipped
CI (PR) / int (pull_request) Successful in 1m58s
CI (PR) / e2e (1) (pull_request) Failing after 3m8s
CI (PR) / e2e (2) (pull_request) Failing after 3m11s
CI (PR) / checks (pull_request) Failing after 1s
fix(ci OPS50): matrix estática [1,2] no e2e (Forgejo não passa job results à expansão de matrix — fromJson(needs.*) vira shard vazio) e VITEST_MAX_WORKERS=4 no int (8 CPUs do runner × pool-10 estoura os 100 clients do Postgres do serviço)
2026-08-17 04:17:56 -03:00

425 lines
16 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. 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.
#
# 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.
name: CI (PR)
on:
pull_request:
branches: [main]
env:
PAYLOAD_SECRET: test-only-secret-not-used-in-production
NEXT_PUBLIC_SITE_URL: http://localhost:3000
jobs:
scope:
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 }}
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: classify
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
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"
# 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:
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
- name: Changelog is append-only
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
PR_BODY: ${{ github.event.pull_request.body }}
run: node scripts/check-changelog-append-only.mjs
- name: Aggregate is up to date with docs/changelog
run: node scripts/build-changelog.mjs --check
- name: No conflict markers in markdown diffs
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: node scripts/check-docs-conflict-markers.mjs
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) }}
run: |
set -euo pipefail
echo "$NEEDS" | jq -e '
to_entries
| all(.value.result == "success" or .value.result == "skipped")
'