Files
OmniRoute/.github/workflows/quality.yml
Diego Rodrigues de Sa e Souza 9b3efef806 fix(ci): five workflow defects, one of them shipping the wrong dmg to Intel Macs (#8988)
* fix(ci): five workflow defects, one of them shipping the wrong dmg to Intel Macs

Gaps 31, 19, 16, 30 and 12 of the v3.8.49 process dossier.

## 31 — LIVE BUG: an Intel Mac downloads the ARM dmg

electron-builder runs once per macOS job and each run emits its own
`latest-mac.yml` listing only its own dmg — measured at 338 and 350 bytes,
different content, identical filename. `download-artifact` with
`merge-multiple: true` resolves that collision by ARRIVAL ORDER, so one silently
overwrites the other. arm64 won in the published v3.8.48.

Why that breaks Intel, from electron-updater's own selection code
(out/providers/Provider.js):

    files.find(it => [...].some(n => n.includes(process.arch))) ?? files.shift()

The Intel dmg is `OmniRoute-X.Y.Z.dmg` — no arch suffix. On Intel `process.arch`
is "x64", nothing matches, and the fallback takes the FIRST entry. With an
arm64-only manifest that is the ARM build.

So ORDER is the fix, not tidiness: the un-suffixed entry must be first, because
it is the only one reachable through that fallback. `merge-multiple` is now off
(per-artifact subdirectories) and a new
`scripts/release/merge-mac-update-manifest.mjs` merges them deliberately. It
refuses to write when the inputs disagree on version — a manifest stitched from
two builds points at files that were never published together, which is worse
than no manifest.

Validated against the REAL v3.8.49 manifests, not just fixtures: the script
reproduces byte-for-byte the manifest I hand-merged and published, including
both sha512 values and the newer releaseDate.

## 19 — one variable, two opposite machines

`USE_VPS_RUNNER` governed the build and the test jobs together. The build needs
the .113's RAM; the tests need the hosted runner's link. Measured 2026-07-29:
`actions/setup-node` took 20m06s on .113 with 4 concurrent runners versus 16s
hosted (npm cache restore saturating the link), while the tests themselves tied
— 2m54 vs 2m31.

Self-hosted is therefore strictly worse for tests, so rather than add a second
variable to configure, `test-unit`, `test-vitest`, `fast-unit` and `fast-vitest`
are pinned to `ubuntu-latest`. `quality.yml`'s `fast-gates` deliberately keeps
the variable — I have no measurement for it, and guessing is what produced this
gap.

## 16 — a flaky shard sent the publish into the 40-minute build

The artifact reuse filter required `conclusion == "success"` on the whole run, so
any unrelated red shard discarded a perfectly good tree. The artifact is only
uploaded if the Build job succeeded, so its PRESENCE is the accurate signal. Now
it takes the 5 most recent candidate runs and tries each download until one
works. `head_repository.full_name == env.REPO` stays — that clause is the
artifact-poisoning guard, not a filter refinement.

## 30 — the gate that could be bypassed at merge

`check:agent-skills-sync` lived only in quality.yml's PR-only Merge-integrity
job, because the CHANGELOG half of that job needs a base to diff against. This
half does not. Keeping it PR-only left a real hole: this cycle's merge trains
landed with `--admin`, which bypasses required checks, so three SKILL.md files
drifted, rode the release squash into `main`, and the sync-back turned them into
a base-red blocking EVERY PR into release/v3.8.50 until #8954. It now also runs
in ci.yml's lint job, which runs on push to `main`.

## 12 — a cancelled gate reads like a passing one

The dashboard already renders ` CANCELLED` per job, so my dossier entry was
imprecise: they do not vanish, they sit buried mid-table. A cancelled job
reported no verdict at all, and this cycle the Vitest job was cancelled in rounds
1, 2 and 3 — it finished only in round 4, revealing a suite broken the whole
cycle plus two production bugs. The summary now opens with a banner naming every
cancelled job and saying plainly that nothing was checked.

    node --import tsx/esm --test tests/unit/mac-update-manifest-merge.test.ts   # 11 pass
    merge against the real v3.8.49 manifests → both dmgs, Intel first
    all four workflows parse; check:workflows --ratchet → 178, baseline 190

* docs(changelog): fragment for #8988

* test(ci): align the artifact-provenance guard with the gap-16 criterion

My own assertion from #8953 encoded the criterion this PR deliberately removes:
it required `.conclusion == "success"` on the whole CI run, which discarded a
perfectly good build tree whenever any unrelated shard went red — pushing the
publish into the 40-minute build the fast path exists to avoid.

Inverted rather than deleted, and the replacement is strictly stronger. It now
pins three things where the old one pinned one: that the loose criterion is gone,
that the step actually probes for the artifact (the accurate signal, since it is
only uploaded when the Build job succeeded), and that it probes MORE THAN ONE
candidate run — without which a single miss still falls back to a full build.

The provenance clause it was originally written to protect
(head_repository.full_name == env.REPO) is untouched and still asserted above.

* fix(ci): finish gap 19 — pin fast-gates and give USE_VPS_RUNNER one meaning

This was left deliberately partial because `fast-gates` had never been measured,
and guessing is what produced gap 19 in the first place. Measured now, and the
evidence is cleaner than expected:

    fast-gates, 160 quality.yml runs .... ZERO self-hosted samples
                                          every non-skipped one is "GitHub Actions NNNN"
    median duration, 72 successful runs .. 5.6 min hosted

The classifier is not at fault — in the same window ci.yml's Build demonstrably
ran on omniroute-113-7 and omniroute-113-6, so self-hosted runs are visible when
they happen. The USE_VPS_RUNNER expression on this job was dead configuration.

And had it ever fired it would have inherited the measured penalty, because this
job's first two steps are exactly the bottleneck:

    actions/setup-node on .113 with 4 concurrent runners .... 20m06s
    actions/setup-node hosted ..............................     16s

So it is pinned rather than switched, and the second variable the gap proposed
(USE_VPS_RUNNER_BUILD / _TESTS) turns out to be unnecessary. After this the
variable governs exactly five jobs, all of them build-like:

    ci.yml:build · quality.yml:build · npm-publish:publish
    nightly-release-green: release-green, main-green

One variable, one meaning: "this job needs the .113's memory". A guard test pins
that — it fails if the variable is ever attached to a test-like job again, and it
also asserts the build KEEPS it, so nobody closes this gap by removing the
variable outright.

    node --import tsx/esm --test tests/unit/vps-runner-variable-scope.test.ts   # 3 pass
    check:workflows --ratchet → 178, baseline 190

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-07-30 12:49:52 -03:00

424 lines
22 KiB
YAML

name: Quality Gates
on:
pull_request:
branches: ["release/**"]
types: [opened, synchronize, reopened, ready_for_review]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
# CI must never mutate the runner's OS trust store (2026-07-05: a cert-flow
# test installed a fake PEM on a persistent self-hosted runner and broke all
# system TLS). Belt-and-suspenders with tests/_setup/isolateDataDir.ts.
OMNIROUTE_SKIP_SYSTEM_TRUST: "1"
CI_NODE_VERSION: "24"
jobs:
# Same classifier as ci.yml (scripts/quality/classify-pr-changes.mjs) so PR→release
# path filters share existence reasons: code / docs / i18n / workflow.
changes:
name: Change Classification
runs-on: ubuntu-latest
outputs:
code: ${{ steps.classify.outputs.code }}
docs: ${{ steps.classify.outputs.docs }}
i18n: ${{ steps.classify.outputs.i18n }}
workflow: ${{ steps.classify.outputs.workflow }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "$EVENT_NAME" != "pull_request" ]; then
{
echo "code=true"
echo "docs=true"
echo "i18n=true"
echo "workflow=true"
} >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
build:
name: Build (advisory)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — same fork-safe rule as ci.yml / fast-gates.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
# #7307: advisory for the first week of release-PR runs; remove
# continue-on-error after the production-build signal is stable.
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run build
env:
OMNIROUTE_USE_TURBOPACK: "1"
# No artifact upload here: the PR-to-release quality workflow has no
# downstream package/e2e jobs that consume the Next.js build output.
# Docs/OpenAPI contract gates only — existence reason is doc accuracy + route refs.
# Split out of fast-gates so pure-docs PRs skip typecheck/unit while still validating docs.
docs-gates:
name: Docs Gates (fast-path)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
- run: npm run check:api-docs-refs
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
run: npm run check:docs-all
fast-gates:
name: Fast Quality Gates
needs: changes
# Code surface only — pure docs/i18n PRs skip this bag (docs-gates covers docs).
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner (same rule as ci.yml): use the self-hosted VPS pool only when the
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
# PINNED to hosted (gap 19). This job carried the USE_VPS_RUNNER expression, and that
# expression was DEAD CONFIGURATION: across 160 quality.yml runs the job never once landed on
# a self-hosted runner — every non-skipped sample is `GitHub Actions NNNN`. The classifier is
# not at fault: in the same window ci.yml's Build demonstrably ran on omniroute-113-7 and
# omniroute-113-6, so self-hosted runs are visible when they happen.
#
# And if it ever HAD fired it would have inherited the measured penalty, because this job's
# first two steps are exactly the bottleneck: actions/setup-node + npm ci took 20m06s on .113
# with 4 concurrent runners versus 16s hosted (npm cache restore saturating the link). Median
# here is 5.6 min hosted across 72 successful runs.
#
# With this pinned, USE_VPS_RUNNER governs ONLY build-like jobs — one variable, one coherent
# purpose. That is what gap 19 asked for; a second variable turned out to be unnecessary.
runs-on: ubuntu-latest
# tsx gates (known-symbols, route-guard-membership) import modules that open
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
- run: npm run check:provider-consistency
- run: npm run check:fetch-targets
# docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered).
- run: npm run check:deps
- run: npm run check:file-size
- run: npm run check:error-helper
- run: npm run check:migration-numbering
- run: npm run check:public-creds
- run: npm run check:db-rules
- run: npm run check:known-symbols
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:test-runner-api
# Guards tap.testFiles drift: a covering unit test absent from stryker.conf.json
# tap.testFiles makes its module's mutants survive on a cold nightly-mutation run,
# false-failing the blocking mutationScore ratchet. See check-mutation-test-coverage.mjs.
- run: npm run check:mutation-test-coverage
- run: npm run check:any-budget:t11
# Build-scope guard: fails if worktrees/cruft leak into the tsconfig include
# scope (would OOM `next build`). Instant. See incident 2026-06-25 / #5031.
- run: npm run check:build-scope
# Pack-policy (unexpected-files allowlist) WITHOUT a build — catches a stray file
# leaking into the npm tarball (v3.8.36: 6 ops bin/*.sh) per-PR instead of only on
# the release PR's heavy Package Artifact job.
- run: npm run check:pack-policy
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
- run: npm run check:complexity-ratchets
- name: Typecheck (core)
run: npm run typecheck:core
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
# covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs.
- name: Typecheck (dashboard)
run: npm run check:dashboard-typecheck
# WS4.2 (v3.8.49 plan): TypeScript 7 native-compiler SHADOW — advisory only.
# TS7 went GA 2026-07-08 with 8-12x type-check speedups; its Compiler API only
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
# (the hybrid is the officially documented pattern). Isolated npx on purpose:
# installing an alias package could collide node_modules/.bin/tsc with 6.x.
# Promote to the blocking gate after ~1 week of parity with the step above.
- name: Typecheck (core) — TS7 native shadow (advisory)
continue-on-error: true
run: |
RC=0
START=$(date +%s)
npx -y -p typescript@7 tsc --pretty false -p tsconfig.typecheck-core.json || RC=$?
echo "[ts7-shadow] exit=$RC elapsed=$(( $(date +%s) - START ))s — the 6.x step above stays authoritative"
exit $RC
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
# unit tests impacted by this PR's changed files. On hub/unmapped changes the
# selector returns __RUN_ALL__ — full-suite authority is the parallel
# `fast-unit` 4-shard job (test:unit:ci:shard; was 2-shard, #6781), NOT an
# unsharded re-run here. Stacking unsharded test:unit:ci on top of fast-unit
# doubled wall time (~16 min extra on ubuntu-latest) without extra coverage.
#
# BLOCKING for the *impacted subset* (flipped 2026-06-17). Fail-safe full
# coverage remains required via `Unit Tests fast-path` (fast-unit).
- name: Impacted unit tests (TIA subset; blocking)
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
git fetch --no-tags origin "$GITHUB_BASE_REF" || true
node scripts/quality/build-test-impact-map.mjs
SEL="$(node scripts/quality/select-impacted-tests.mjs)"
# Shadow evidence (#8084): persist every selection so TIA false negatives can
# be measured against fast-unit's full-suite verdict across releases BEFORE
# any gate authority moves off ordinary PRs. Artifact uploaded below.
printf '%s\n' "$SEL" > tia-selection.txt
if [ -z "$SEL" ]; then
echo "TIA selection: empty (no source/test changes)" >> "$GITHUB_STEP_SUMMARY"
echo "No source/test changes — skipping unit tests"; exit 0
fi
# CI runners are 4-vCPU; run at --test-concurrency=4 (matching the ci.yml unit
# job) rather than test:unit's local-tuned concurrency=20. Oversubscribing the
# runner makes timing-sensitive tests (db-backup, upstream-timeout, ...) flake,
# which must not happen on a blocking gate. DATA_DIR isolation keeps the parallel
# run race-free regardless of concurrency.
if echo "$SEL" | grep -q "__RUN_ALL__"; then
echo "TIA selection: __RUN_ALL__ (fail-safe) — full-suite authority stays with fast-unit" >> "$GITHUB_STEP_SUMMARY"
echo "Fail-safe: __RUN_ALL__ — deferring FULL unit suite to fast-unit (4-shard)."
echo "Not re-running unsharded test:unit:ci here (duplicate of fast-unit coverage)."
exit 0
fi
echo "TIA selection: $(grep -c . tia-selection.txt) impacted test file(s) — full suite still runs in fast-unit (shadow-evidence phase, #8084)" >> "$GITHUB_STEP_SUMMARY"
echo "Running impacted tests:"; echo "$SEL"
mapfile -t FILES <<< "$SEL"
# Loader parity with test:unit:ci:shard (#6787): tests/unit/dashboard/** runs
# under `--import tsx` (CJS transform — required for ESM-only deep imports like
# @lobehub/icons/es/* reached via lobeProviderIcons.ts); everything else under
# `--import tsx/esm`. A single tsx/esm invocation false-reds every dashboard
# module-shape test the impact map selects ("Unexpected token 'export'").
DASH=(); REST=()
for f in "${FILES[@]}"; do
case "$f" in
tests/unit/dashboard/*) DASH+=("$f") ;;
*) REST+=("$f") ;;
esac
done
RC=0
if [ ${#REST[@]} -gt 0 ]; then
node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${REST[@]}" || RC=$?
fi
if [ ${#DASH[@]} -gt 0 ]; then
node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${DASH[@]}" || RC=$?
fi
exit $RC
# #8084 shadow evidence: keep the raw selection downloadable so TIA misses can be
# audited against fast-unit failures on the same run (gate moves need this data).
- name: Upload TIA selection (shadow evidence)
if: always()
uses: actions/upload-artifact@v7
with:
name: tia-selection
path: tia-selection.txt
if-no-files-found: ignore
retention-days: 30
fast-vitest:
name: Vitest (fast-path)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast-path runs on EVERY PR,
# which is where flaky-detection volume actually comes from (ci.yml's heavy
# jobs only run on the release PR). Advisory upload, own-origin only.
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
- name: Upload test results to Trunk (advisory)
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
continue-on-error: true
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
with:
junit-paths: trunk-junit/**/*.xml
org-slug: omniroute
token: ${{ secrets.TRUNK_TOKEN }}
fast-unit:
name: Unit Tests fast-path (${{ matrix.shard }}/4)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
# runner box). Node's native --test-shard=N/total takes any denominator — only
# this matrix and the TEST_SHARD env below encode the shard count.
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# QW-d: fonte única — o mesmo npm script do CI pesado/local. Fecha dois drifts do
# comando inline antigo: os dirs `memory` e `usage` estavam FORA do glob (testes
# silenciosamente não rodavam no fast path) e o setupPolyfill não era importado.
- run: npm run test:unit:ci:shard
env:
TEST_SHARD: ${{ matrix.shard }}/4
# ── Pacote 4 (plano mestre testes+CI, aprovado 2026-07-04) ─────────────────────────
# No-new-warnings por PR via ESLint bulk suppressions nativo (>=9.24). O baseline
# config/quality/eslint-suppressions.json congela as violações EXISTENTES por
# arquivo+regra; qualquer warning NOVO aparece e o --max-warnings 0 falha o job — o
# drift de +41/+88 warnings por ciclo passa a morrer no PR que o introduz, em vez de
# ser rebaselinado às cegas na release. Aperto do baseline (na reconciliação da
# release): npx eslint . --prune-suppressions --suppressions-location config/quality/eslint-suppressions.json
#
# Princípio Zero: bloqueante SÓ para branches internas (as campanhas/sessões são a
# origem do drift). PR de FORK roda em modo report (continue-on-error → o job fica
# verde com anotação; a campanha /green-prs aplica o fix via co-autoria — o
# contribuidor NUNCA é bloqueado nem cobrado).
lint-guard:
name: No new ESLint warnings
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
- name: ESLint (baseline congelado — warning novo = vermelho)
# lint:json writes the report; --max-warnings 0 keeps no-new-warnings policy.
run: npm run lint:json -- --max-warnings 0
# Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só
# explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come
# bullets vizinhos/seções inteiras (incidente #6193, 2026-07-05: 212 linhas /
# 130 bullets); o checkout de PR é refs/pull/N/merge, então comparar contra a
# base detecta o eat ANTES do merge. (2) SKILL.md gerado stale vs o catálogo de
# agent-skills (#6186 mergeou um id de catálogo sem rodar o gerador → 8 reds de
# integration invisíveis até a release).
#
# Princípio Zero: bloqueante SÓ para branches internas; PR de FORK roda em modo
# report (continue-on-error) — a campanha corrige via co-autoria, o contribuidor
# nunca é bloqueado.
merge-integrity:
name: Merge integrity (changelog + generated skills)
# Always on non-draft PRs — CHANGELOG/skills can break on docs-only merges too.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: CHANGELOG integrity (nenhum bullet da base pode sumir no merge-result)
run: npm run check:changelog-integrity
- name: Agent-skills generator sync (SKILL.md gerado ≡ catálogo)
run: npm run check:agent-skills-sync