All checks were successful
CI (PR) / checks (pull_request) Successful in 8m4s
Promote plan-issue Related on main merge / plan-issue-ready (pull_request) Has been skipped
Main merge flips claimed issues to done + in-prod / issue-done (pull_request) Successful in 13m48s
PR Ready + auto-merge / ready-automerge (pull_request) Successful in 52m5s
- ci.yml: on push removido → schedule */30 + workflow_dispatch (fundido com a estrutura de job único sequencial do OPS62 X1) - job gate (host, main-only, sem pnpm): revision do container rodando no homeserver (label org.opencontainers.image.revision — o compose pode mentir após rollback falho) + diff + classificador .dockerignore - fail-open: revision desconhecida → suite roda (verde); incapaz de classificar → run vermelho (nunca blackout silencioso); deploy duplo-gated (needs: [checks, gate] + if explícito) - deploy-homeserver.sh: guard 'already deployed' idempotente (mesma revision) - agent-pool.yml removido; pool dormente (144 pnpm install/dia no tick */10) - specs: dockerignoreMatch, ciClassifyProduction (incl. git-diff com repo scratch e exit codes), pins ciSkipInvariants/deployScript; HIGH_RISK_EXACT - docs: runbook (janela/gate/verificação ao vivo/falhas), AGENTS.md, AGENT-OPS.md (tabela X1 fundida), skills pool dormente, changelog OPS65 - impl plan aprovado; intenção marcada entregue
229 lines
10 KiB
YAML
229 lines
10 KiB
YAML
# Post-merge verifier on `main`, in a fixed 30-minute window (OPS65): the
|
||
# workflow no longer fires on every push. The `schedule` trigger polls every
|
||
# 30 min and a cheap gate (`gate`, runs-on host, no pnpm) classifies whether
|
||
# anything that would enter the docker build context changed since the
|
||
# revision running on the homeserver (the `org.opencontainers.image.revision`
|
||
# label of the running container, read over ssh).
|
||
# - Deployed == HEAD → suite + deploy skip; the run stays green.
|
||
# - Anything else (production change, unknown deployed state, dispatch) →
|
||
# full suite; deploy in the same window.
|
||
# - `workflow_dispatch` always runs the full pipeline (explicit operator
|
||
# intent); the deploy is idempotent via the "already deployed" guard in
|
||
# scripts/deploy-homeserver.sh.
|
||
#
|
||
# Fail-open: an unreadable/unknown deployed revision makes the gate report
|
||
# production=true, so the suite ALWAYS verifies main and a quiet day only
|
||
# skips when we KNOW deployed == HEAD. A gate that cannot classify at all
|
||
# (git/.dockerignore error) exits non-zero and turns the run RED — never a
|
||
# silent green blackout. The deploy job only runs when the gate succeeded
|
||
# AND production changed AND checks are green — never deploy blind.
|
||
#
|
||
# 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.
|
||
#
|
||
# 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.
|
||
#
|
||
# Forgejo port (OPS50): Postgres services live on the job network and are
|
||
# reached by NAME (postgres-int / postgres-build on 5432) — no host publish,
|
||
# so concurrent runs never collide on host ports (OPS62 X1). 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:
|
||
schedule:
|
||
- cron: '*/30 * * * *'
|
||
workflow_dispatch:
|
||
|
||
env:
|
||
PAYLOAD_SECRET: test-only-secret-not-used-in-production
|
||
NEXT_PUBLIC_SITE_URL: http://localhost:3000
|
||
|
||
jobs:
|
||
gate:
|
||
name: gate production change
|
||
# Host runner: needs `ssh homeserver` (same environment as the deploy
|
||
# job — the key lives on the workstation host) and a full clone to diff
|
||
# <deployed-rev>..HEAD. Main + dispatch only: a branch dispatch must not
|
||
# run that branch's code on the host — the skipped gate makes the suite
|
||
# run fail-open on ubuntu containers instead (test-only secrets).
|
||
if: github.ref == 'refs/heads/main'
|
||
runs-on: host
|
||
timeout-minutes: 10
|
||
outputs:
|
||
production: ${{ steps.classify.outputs.production }}
|
||
steps:
|
||
- uses: actions/checkout@v5
|
||
with:
|
||
fetch-depth: 0
|
||
- uses: actions/setup-node@v5
|
||
with:
|
||
node-version: 24
|
||
package-manager-cache: false
|
||
- name: Read deployed revision and classify production change
|
||
id: classify
|
||
run: |
|
||
set -euo pipefail
|
||
# Truth = the RUNNING container's revision label (the compose file
|
||
# can lie after a failed rollback — it is swapped before the
|
||
# rollout). Empty/malformed = unknown deployed state → the
|
||
# classifier reports production=true (full run, green).
|
||
deployed_sha="$(ssh homeserver "docker inspect -f '{{index .Config.Labels \"org.opencontainers.image.revision\"}}' teqo-1313 2>/dev/null" || true)"
|
||
if ! printf '%s' "$deployed_sha" | grep -Eq '^[0-9a-f]{40}$'; then
|
||
deployed_sha=""
|
||
fi
|
||
echo "deployed_sha: ${deployed_sha:-<none>}"
|
||
out="$(node scripts/ci-classify-production.mjs --deployed "$deployed_sha")"
|
||
printf '%s\n' "$out"
|
||
echo "production=$(printf '%s' "$out" | node -pe 'JSON.parse(require("fs").readFileSync(0,"utf8")).production')" >> "$GITHUB_OUTPUT"
|
||
|
||
checks:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 50
|
||
needs: [gate]
|
||
# Fail-open: an errored gate never skips the verifier; only a gate that
|
||
# succeeded AND found no production change skips the suite.
|
||
if: needs.gate.result != 'success' || needs.gate.outputs.production == 'true'
|
||
services:
|
||
# Services are reached by NAME on the job network (no host publish —
|
||
# OPS62 X1): concurrent runs never collide on host ports, which the
|
||
# single long-lived job would hold for the whole run. Two isolated
|
||
# services — int and build+e2e — keep e2e from inheriting int fixture
|
||
# residue. Forgejo 9 does not expand expressions in `services.ports`,
|
||
# so a per-run port scheme was dropped in favour of the job-network DNS.
|
||
postgres-int:
|
||
image: postgres:17-alpine
|
||
env:
|
||
POSTGRES_USER: teqo
|
||
POSTGRES_PASSWORD: teqo
|
||
POSTGRES_DB: teqo_test
|
||
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
|
||
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: 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)
|
||
run: |
|
||
set -euo pipefail
|
||
echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/teqo_test" >> "$GITHUB_ENV"
|
||
# vitest re-loads .env.test over the process env, so the service URL
|
||
# must win through the .env.test.local layer (same mechanism
|
||
# `pnpm worktree next` uses). OPS50.
|
||
echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/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:
|
||
# 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)
|
||
run: |
|
||
set -euo pipefail
|
||
echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/teqo_test" >> "$GITHUB_ENV"
|
||
echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/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
|
||
|
||
- 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: 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:
|
||
NEXT_DIST_DIR: .next-e2e
|
||
- 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").
|
||
env:
|
||
PLAYWRIGHT_WORKERS: 4
|
||
run: pnpm test:e2e
|
||
|
||
deploy:
|
||
name: deploy homeserver (teqo-1313)
|
||
# OPS53. Runs directly on the runner HOST (not a job container): the
|
||
# deploy needs the workstation's SSH key to reach the homeserver
|
||
# (`ssh homeserver`) and streams the whole remote build/migrate/rollout
|
||
# to this job's logs. Deliberately wired ONLY here — main window +
|
||
# workflow_dispatch — never in ci-pr.yml: a host job is code execution as
|
||
# `fsolla` on the workstation, and main only ever moves through a merged
|
||
# PR whose full suite was already green.
|
||
# OPS65: double-gated — the verifier must be green AND the gate must have
|
||
# found a production change. The explicit `if` prevents both "deploy
|
||
# blind on a failed gate" and "deploy with a red suite" (a bare output
|
||
# condition would run despite failed needs).
|
||
needs: [checks, gate]
|
||
if: needs.checks.result == 'success' && needs.gate.outputs.production == 'true'
|
||
runs-on: host
|
||
timeout-minutes: 60
|
||
steps:
|
||
- uses: actions/checkout@v5
|
||
- name: Deploy teqo-1313 on the homeserver
|
||
run: |
|
||
set -euo pipefail
|
||
ssh homeserver "bash -s -- $GITHUB_SHA" < scripts/deploy-homeserver.sh
|