teqo/.github/workflows/ci-pr.yml
Cursor Agent 1ad8863994
OPS12 — CI bloqueia Closes/Fixes em PR só docs/plans
- Lógica pura em plansOnlyClosesGuard.mjs + CLI check-plans-only-pr-closes
- Job plans-only-closes em ci-pr.yml (rollup checks)
- Unit tests com fixture do incidente #115
- plan-issue, agent-pr-workflow, PR template e GUARDRAILS atualizados

Closes #116

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 11:26:19 +00:00

398 lines
13 KiB
YAML

# 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. `migration-lock`
# stays parallel to phase 1 (cheap, independent).
#
# 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).
name: CI (PR)
on:
pull_request:
branches: [main]
concurrency:
group: ci-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
actions: write
pull-requests: read
env:
DATABASE_URL: postgresql://teqo:teqo@localhost:5432/teqo_test
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 }}
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"
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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 exec knip
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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')
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: teqo
POSTGRES_PASSWORD: teqo
POSTGRES_DB: teqo_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U teqo"
--health-interval 5s
--health-timeout 5s
--health-retries 12
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v5
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- 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
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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:
- 5432: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: Build (against the local service database)
run: pnpm build
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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')
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: teqo
POSTGRES_PASSWORD: teqo
POSTGRES_DB: teqo_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U teqo"
--health-interval 5s
--health-timeout 5s
--health-retries 12
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v5
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Install Playwright chromium
run: pnpm exec playwright install --with-deps chromium
- run: pnpm migrate
- run: pnpm db:seed:minimal
- name: Build for e2e
run: pnpm build
env:
NEXT_DIST_DIR: .next/e2e
- name: E2E tests (full suite)
if: needs.scope.outputs.e2e_mode == 'full'
run: pnpm test:e2e
- name: E2E tests (manifest-selected specs)
if: needs.scope.outputs.e2e_mode == 'selected'
run: pnpm test:e2e -- ${{ needs.scope.outputs.e2e_specs }}
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
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
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
migration-lock:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v5
- name: Count open PRs touching schema files
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
count=$(gh pr list --state open --limit 100 --json number,files \
--jq '[.[] | select(.files != null and ([.files[].path | test("^(src/migrations/|src/payload-types.ts$|payload-types.ts$)")] | any))] | length')
echo "Open schema-touching PRs: $count"
if [ "$count" -gt 1 ]; then
echo "::error::More than one open PR touches src/migrations/ or payload-types.ts. Merge or close the other first."
exit 1
fi
- name: Cancel workflow on failure
if: failure()
uses: ./.github/actions/cancel-workflow-on-failure
checks:
name: checks
runs-on: ubuntu-latest
needs:
- scope
- lint
- format
- typecheck
- knip
- cycles
- unit
- int
- build
- e2e
- migration-lock
- 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")
'