mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 18:52:18 +03:00
* fix(ci): clear base-reds on release/v3.8.50 (round 3) - CHANGELOG.md: restore the top [Unreleased] section dropped by the #10189 reconcile (docs-sync gate: first section must be Unreleased) - env-doc-sync: document CONDUCTOR_ORCHESTRATOR_TOKEN + CONDUCTOR_SPOKESPERSON_URL in .env.example/ENVIRONMENT.md; allowlist the CI-only GITHUB_STEP_SUMMARY and TS7_BASE_REF (ts7 ratchet signals); drop a stray merge artifact line - providers: restore the audited chatanywhere metadata entry that base-reds round 2 dropped together with its duplicate — the provider was half-wired (registry+endpoint without APIKEY metadata), which is what the wave3 test catches; re-pin providers-constants-split at the measured 228 - docs counts: 338 -> 339 (today's +2 void-ai/helixmind, -1 Puter) via gen:provider-reference + README/AGENTS/llm.txt/package.json/diagrams/i18n mirrors - file-size ratchet: annotated rebaseline for the two pre-existing drifts (ModelSelectModal 1138, gateways 1250) following the 2026-08-11 precedent Refs #9985 * fix(ci): base-reds round 3b — stale sibling tests + mode-pack weight contract - check-docs-counts-sync.test.ts: drop the imports/subtests of the four helpers #10196 removed from the gate script (readMcpFactsFromSource, listLocalizedDocs, makeRequiredCountsValidator, checkFreeTierInventory) — the new-API tests that #10196 added stay; the file now loads again under the node runner - quota-connection-recovery.test.ts: convert from vitest APIs to node:test — the file lives in tests/unit/*.test.ts (node-runner glob) and the vitest runtime crashes when imported outside vitest, killing the whole shard entry - modePacks.ts: re-normalize all six mode packs to sum 1.0 — #8940 added sessionAvailability: 0.05 to every pack without rebalancing (1.05 total); ratios preserved exactly (÷1.05), so post-normalizeScoringWeights behavior is unchanged; restores the declared sum-to-1.0 contract the 4235 test pins Refs #9985 * fix(ci): base-reds round 3c — vitest siblings, weights default, secrets FP, mutation tap - DistributeProxiesButton.test.tsx: wrap renders in NextIntlClientProvider — #9245 localized the component (useTranslations) and left the test without the intl context, failing all 14 cases - scoring.ts: re-normalize DEFAULT_WEIGHTS to sum 1.0 (same #8940 class as the mode packs — sessionAvailability added without rebalancing; ratios preserved) - .gitleaks.toml: generalize the kimi sponsor-banner localStorage-key allowlist to -v\d+ — #10200 bumped v1→v2 and the stale regex regressed the secrets ratchet with a false positive - stryker.conf.json: register 6 covering unit tests in tap.testFiles (4 modules) so their mutant kills count — unblocks check:mutation-test-coverage --strict Refs #9985 * fix(ci): base-reds round 3d — inspector factor gap, stale registry/gap tests, i18n key sync - comboScoringInspector: add cacheAffinity/sessionAvailability/connectionDensity to FACTOR_KEYS + the factor-key type — calculateScore() weighs them but the breakdown omitted them, so the explained contributions never summed to the reported score (inspector bug, red on the pure tip) - combo-scoring-inspector.test: make the explicit-weights override sum-neutral (±0.05 shift) so it stays valid for any DEFAULT_WEIGHTS values — the hardcoded override only summed to 1.0 against the pre-#8940 defaults, which is also why explicit weights silently fell back to 'default' on the tip - unorouter-registry.test: align to the canonical .com host (api.unorouter.ai 301-redirects there, verified live) and to wave4's live model discovery (passthrough, no static seed) — the .ai/auto-model expectations were stale - check-migration-numbering.test: 147 left KNOWN_GAPS when 147_api_keys_model_access_mode.sql landed — assert absent (same as 143) - i18n: sync-ui pass — 35,914 missing UI keys stamped as __MISSING__ placeholders across 42 locales (mechanical; greens the pt-BR key-presence integrity test; coverage pct unchanged by design — translation is a separate workstream) Refs #9985 * fix(ci): base-reds round 3e — 2 real defects + 14 stale sibling tests (waves A-E) Real defects fixed: - src/lib/db/apiKeys.ts: #9313's empty-allowlist early return bypassed the group permission check, silently disabling group deny rules (#8817) for every key without a per-key allowlist; fall-through restored, restricted+[] deny-all kept - open-sse/utils/proxyFetch.ts: #10032 re-appended the raw transport error to the propagated message, reintroducing the proxy user:password leak #9837 closed; new redactProxyDetailsInMessage() keeps the reason, redacts URL/credentials - .github/workflows/quality.yml: #10134 added the TS7 ratchet as a separate blocking step AFTER the aggregated gates — the exact #8542 masking mechanism; folded into the non-fail-fast loop (still blocking, still PR-only) ⚠️ CI edit, gate-strengthening — explicit owner sign-off requested on the PR - src/i18n/messages/ko.json: 3 machine-mistranslation regressions caught by the #8244 glossary checker (장애인→비활성화됨, 양말5://→socks5://, 비클로드→Claude가 아닌) Stale sibling tests aligned to deliberately-moved contracts (each cites its mover): request-log-detail-layout + -stream (#9245 intl provider), repro-8542 pin update, quality-rail-gate-membership (#10134 shape), agentSkills-routes 45→46 (#9058), cloudflare-ai-catalog-8717 (#8804 supersedes #8808), executor-xai (#9994), vision-bridge-claude-wire (#9463 minimax→openai), sse-auth forced-pin (#8893), tls-proxy-context (strengthened leak guards), rate-limit-local-error-classification (#9164/#9342), minimax-thinking-signature (#9463), codebuddy-cn (#9723 +1 test), github-copilot-custom-model (#9050), providers-g4f-batch3 (#9584), synced-capability-warmup (#9199, stricter), sidebar-tools-group (#8221), oauth-modal-grok-cli-paste (#9245); agentSkills/catalog.ts comment 45→46; file-size rebaseline for proxyFetch (+19, annotated) Refs #9985 * fix(ci): base-reds round 3f — waves F-J: 9 more real defects + stale sibling sweep Real production defects fixed (all red on the pure tip, each with its origin): - routeGuard.ts: #8949 accidentally DELETED the /api/providers/[id]/login local-only pattern — the route spawns a browser, so the loopback gate for a process-spawning route was gone (Hard Rules #15/#17); restored (314 guard tests green) - agentSkills generator: #9058's category dispatch gave the config category an empty body, wiping skills/config-codex-cli/SKILL.md at the #10131 sync; fixed + SKILL.md regenerated via the official generator - imageRegistry: #9982 broke same-provider bare aliasing (antigravity preview id sent upstream unresolved); new resolveSameProviderBareAlias() keeps the fal cross-provider fix intact - imageRegistry: #9982's prefix strip handed the bare nano-banana ids to fal-ai, violating the pinned 2026-07-31 operator decision (adobe-firefly owns them); fal entries made prefix-only (dispatch already re-prefixes) - mediaGeneration/fal.ts: the missing-credential 401 guard was lost when #10198 deleted the superseded falHandler — tests were hitting the live network - bottleneckPatch/rateLimitManager: #9041's merge clobbered #9604, resurrecting the Bottleneck v2.19.5 heartbeat bug (reservoir never refills); patched the library defect at the root and re-aligned chat-rate-limit-body-lock to the working reservoir contract - processSupervisor.mjs: #9761 regressed the Node spawn to bare "node" (the #9156 launchd bug) and dropped #9209's ipv4first args; both restored - openai-responses/pureHelpers: #9423's Agent null-sentinel was unreachable on the schemaless JSON-string path; gate extended - i18n en.json: #8222's regen reverted the #9976 unclosed-tag fix and #8559's combo-cooldown copy; #9038 shipped 40 t() calls with no messages (runtime MISSING_MESSAGE); all restored/added + official sync-ui stamps, and vi's zero-marker policy re-established via the sanctioned translation backend Stale sibling tests aligned (movers cited inline): chat-helpers (#9447), executor-antigravity (#9351), video-fal-grok (#9982), visionBridge (#9759), web-session-credentials (#8974), production-build-module-integrity (positive anchor added), agentSkills-generator/skillManifestsLint/skills-injection/ agentSkillTools-mcp/listCapabilities-a2a (#9058), memory-settings (#10010), model-catalog-policy-invalidation (#8906), model-alias-seed (#9485), reactive-context-compaction (#8949), combo-provider-wildcard (broken upsert helper), oauth-google-loopback (43-locale resurrected-key removal) Validation: 501/501 across the 47 touched test files; typecheck:core, lint, file-size, docs-sync all green. Refs #9985 * fix(ci): base-reds round 3g — wave K/L: 4 more real defects + stale alignments Real defects: - base/reasoningEffort.ts: the stale duplicate cherry-pick #9612 re-added the codex minimal→low rewrite that #9883 had deliberately removed (OMP minimal passthrough); block removed again - cursorImages.ts: #9840 wired prepareCursorImageForWire (sharp re-encode, fail-closed) into the SHARED resolveCursorImages, breaking zai-web and conol-web image uploads (HTTP 400 'undecodable'); new prepareForWire opt-out, Cursor default path unchanged (8 cursor suites green) - modelCapabilities/snapshot: catalog prepare still issued 323 per-model reads of model_context_overrides + max_input_tokens overrides, violating #9199's bulk-load contract; both now resolve from the snapshot single pass - v1-models-discovery-conformance: re-pinned to the bounded 30s SWR window (#9199/#10198) — the old 'stale-first regardless of age' contract is gone Stale tests aligned (movers cited inline): codex-tools-strict-default (#9828 redundant-oneOf strip), devin-providers (#9245 i18n), db-migrationrunner- constants-split (147→151 renumber #8228), gitlab-duo-oauth-setup (#9245), chatcore-extracted-modules (#9161 outbound-protocol keying) compression-api CI failures were cascade artifacts of codex-tools-strict-default failing in the same force-exit shard process — no own defect (171/171 local). Refs #9985 * fix(test): compression-api — register both describes before the runner starts The DATA_DIR setup + route/db top-level awaits sat BETWEEN the two describes; under --test-force-exit (the CI unit-runner flag) the process exits once the already-registered tests finish, so on slow CI machines the whole second describe died as 'Promise resolution is still pending' — the recurring CI-only shard-2 failure that never reproduced locally without the flag. Moved to the top of the file; 10/10 under --test-force-exit locally. Refs #9985 * fix(quality): freeze modelCapabilities.ts at 1006 (annotated) — snapshot routing growth Refs #9985 * fix(quality): move the modelCapabilities freeze into the frozen map (nested schema) Refs #9985 * fix(i18n): translate all 39,718 pending UI keys across 42 locales (owner-approved) Mass-translated every __MISSING__ placeholder via the official i18n:sync-ui --translate-markers pipeline (operator backend), restoring i18nUiCoverage to the 100 baseline (was 89.9 after the merge-storm UI landings + the 42 keys #9038 never shipped). Post-pass repairs, all caught by the existing gates: - glossary: retired renderings the machine reintroduced normalized again (提供商→提供者 zh-CN/zh-TW, 鏈接→連結, 文檔→文件, 調用→呼叫, 供應商→提供者, 響應→回應, 不活躍→未啟用 zh-TW; 클로드→Claude, 옴니루트→OmniRoute ko); DATA_DIR forbidden rendering avoided via 数据文件夹 rephrase - ICU integrity: 120 values with renamed/dropped {params} repaired (39 positional renames, 81 reset to the en source — functional over fluent) Validation: glossary/pt-BR/vi/deno-relay/settings-keys/value-drift/google- loopback suites 76/76; placeholder diff en×42 locales = 0; worst-locale coverage = 100.0%. Refs #9985 --------- Co-authored-by: backryun <bakryun0718@proton.me>
551 lines
29 KiB
YAML
551 lines
29 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 }}-
|
|
# Security scanners — same hardened install as ci.yml quality-extended
|
|
# (gh release download = authenticated, 5000 req/hr; curl to api.github.com
|
|
# is rate-limited to 60/hr and silently no-ops when throttled). The blocking
|
|
# gates below SKIP (exit 0) when their binary is absent — only a measured
|
|
# regression vs config/quality/quality-baseline.json blocks.
|
|
- name: Install security scanners (gitleaks/osv/actionlint/zizmor/oasdiff)
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set +e
|
|
mkdir -p "$HOME/.local/bin"
|
|
# Ratchets compare scanner COUNTS across runs. Pin every auditor: a rule-set
|
|
# update must be an explicit PR that re-measures/rebaselines, never a random
|
|
# red (or green) caused by whatever "latest" served that morning.
|
|
GITLEAKS_VERSION=v8.30.1
|
|
OSV_SCANNER_VERSION=v2.3.8
|
|
ACTIONLINT_VERSION=v1.7.12
|
|
ZIZMOR_VERSION=1.25.2
|
|
OASDIFF_VERSION=v1.19.1
|
|
# gitleaks — pinned linux x64 tarball via gh (authed), extract binary
|
|
rm -rf /tmp/gl && mkdir -p /tmp/gl
|
|
gh release download "$GITLEAKS_VERSION" --repo gitleaks/gitleaks --pattern '*linux_x64.tar.gz' --dir /tmp/gl
|
|
tar -xzf /tmp/gl/*linux_x64.tar.gz -C "$HOME/.local/bin" gitleaks
|
|
# osv-scanner — pinned linux amd64 bare binary via gh (authed)
|
|
rm -rf /tmp/osv && mkdir -p /tmp/osv
|
|
gh release download "$OSV_SCANNER_VERSION" --repo google/osv-scanner --pattern '*linux_amd64' --dir /tmp/osv
|
|
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
|
|
# actionlint — official installer from a pinned release tag (never main)
|
|
bash <(curl -fsSL "https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_VERSION}/scripts/download-actionlint.bash") "$ACTIONLINT_VERSION" "$HOME/.local/bin"
|
|
# zizmor — pinned PyPI package (same version as ci.yml quality-extended)
|
|
pipx install "zizmor==$ZIZMOR_VERSION" || pip install --user "zizmor==$ZIZMOR_VERSION"
|
|
# oasdiff — pinned linux amd64 tarball via gh (authed), extract binary
|
|
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
|
|
gh release download "$OASDIFF_VERSION" --repo Tufin/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
|
|
tar -xzf /tmp/oasd/*linux_amd64.tar.gz -C "$HOME/.local/bin" oasdiff
|
|
# ALWAYS export the bin dir (even if any step above failed)
|
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
"$HOME/.local/bin/gitleaks" version || true
|
|
"$HOME/.local/bin/actionlint" -version || true
|
|
"$HOME/.local/bin/osv-scanner" --version || true
|
|
"$HOME/.local/bin/oasdiff" --version || true
|
|
zizmor --version || true
|
|
- name: Forgotten sibling tests (advisory)
|
|
env:
|
|
GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
run: |
|
|
node scripts/quality/build-test-impact-map.mjs
|
|
node scripts/check/check-forgotten-sibling-tests.mjs \
|
|
--summary-file forgotten-sibling-tests.md \
|
|
--json-file forgotten-sibling-tests.json
|
|
cat forgotten-sibling-tests.md >> "$GITHUB_STEP_SUMMARY"
|
|
- name: Upload forgotten sibling report
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: forgotten-sibling-tests
|
|
path: |
|
|
forgotten-sibling-tests.md
|
|
forgotten-sibling-tests.json
|
|
if-no-files-found: ignore
|
|
retention-days: 30
|
|
# Quality gates (all, non-fail-fast) — #8542: replaces 17 bare check:* steps,
|
|
# 6 G0 gates, 4 ratchet gates, and 3 typecheck steps with a single aggregation
|
|
# step. Each gate runs in a loop with ::group::; failures are collected and
|
|
# reported at the end. set -uo pipefail (NOT set -e) so one failing gate does
|
|
# not abort the job and mask every later gate. Release-added gates are folded
|
|
# in: open-sse typecheck (#8781) and file-size base-relative mode (#8522).
|
|
- name: Quality gates (all, non-fail-fast)
|
|
env:
|
|
# #8522: base-relative file-size mode on PR events — inherited drift (base
|
|
# already over frozen cap) must not red an innocent PR. Unset on
|
|
# workflow_dispatch (no PR base) → absolute comparison.
|
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
|
|
run: |
|
|
set -uo pipefail
|
|
gates=(
|
|
provider-consistency fetch-targets deps file-size error-helper
|
|
migration-numbering public-creds db-rules known-symbols
|
|
route-guard-membership test-discovery test-runner-api
|
|
mutation-test-coverage any-budget:t11 build-scope pack-policy
|
|
complexity-ratchets
|
|
cycles lockfile duplication dead-code type-coverage compression-budget
|
|
# #8781: open-sse workspace typecheck gate — the workspace imports @/ which
|
|
# escapes to src/ via undeclared path aliases. See check-open-sse-typecheck.mjs.
|
|
open-sse-typecheck
|
|
)
|
|
ratchet_gates=(
|
|
secrets vuln-ratchet workflows openapi-breaking
|
|
)
|
|
failed=()
|
|
for g in "${gates[@]}"; do
|
|
echo "::group::check:$g"
|
|
# #8522: file-size is base-relative on PR events (compare against
|
|
# max(frozen, base)) so inherited drift doesn't red an innocent PR;
|
|
# workflow_dispatch (no PR base) falls back to absolute comparison.
|
|
if [ "$g" = "file-size" ] && [ -n "${PR_BASE_SHA:-}" ]; then
|
|
npm run "check:$g" -- --base-ref "$PR_BASE_SHA" || failed+=("$g")
|
|
else
|
|
npm run "check:$g" || failed+=("$g")
|
|
fi
|
|
echo "::endgroup::"
|
|
done
|
|
for g in "${ratchet_gates[@]}"; do
|
|
echo "::group::check:$g (ratchet)"
|
|
npm run "check:$g" -- --ratchet || failed+=("$g")
|
|
echo "::endgroup::"
|
|
done
|
|
echo "::group::typecheck:core"
|
|
npm run typecheck:core || failed+=("typecheck:core")
|
|
echo "::endgroup::"
|
|
echo "::group::check:dashboard-typecheck"
|
|
npm run check:dashboard-typecheck || failed+=("check:dashboard-typecheck")
|
|
echo "::endgroup::"
|
|
# #10134: TS7 zero-new-diagnostics ratchet — folded into this non-fail-fast
|
|
# loop (never a separate blocking step) so an earlier red gate cannot abort
|
|
# the job and mask it (#8542 mechanism). PR-only: the base-relative
|
|
# comparison needs the PR base SHA (empty on workflow_dispatch).
|
|
if [ -n "${PR_BASE_SHA:-}" ]; then
|
|
echo "::group::check:ts7-diagnostics-ratchet"
|
|
npm run check:ts7-diagnostics-ratchet -- --base-ref "$PR_BASE_SHA" || failed+=("ts7-diagnostics-ratchet")
|
|
echo "::endgroup::"
|
|
fi
|
|
if (( ${#failed[@]} )); then
|
|
printf '::error::%d gate(s) failed: %s\n' "${#failed[@]}" "${failed[*]}"
|
|
exit 1
|
|
fi
|
|
# 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.
|
|
# The full result stays advisory while #8484 has a backlog. The blocking
|
|
# base-relative ratchet (folded into the non-fail-fast gates step above)
|
|
# rejects only diagnostics added by the PR, so existing release debt does
|
|
# not block unrelated work.
|
|
- name: Typecheck (core) — TS7 native shadow (advisory)
|
|
continue-on-error: true
|
|
run: |
|
|
RC=0
|
|
START=$(date +%s)
|
|
npm exec --yes --package=typescript@7.0.2 -- 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
|
|
# The advisory sibling-test step generates the same map earlier in this job.
|
|
[ -f config/quality/test-impact-map.json ] || 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 }}
|
|
# G0 (trilho .50): security-events:read lets the CodeQL ratchet below read open
|
|
# code-scanning alerts via `gh api .../code-scanning/alerts` (same as ci.yml's
|
|
# quality-gate job). contents: read keeps checkout working.
|
|
permissions:
|
|
contents: read
|
|
security-events: read
|
|
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
|
|
# ── G0 (trilho .50): motor de ratchet também no trilho B ─────────────────────
|
|
# This job just wrote .artifacts/eslint-results.json — collect-metrics prefers
|
|
# that file, so the ratchet engine lands here at ZERO extra ESLint cost (one
|
|
# inventory, two consumers; same reason ci.yml chains lint → quality-gate).
|
|
# The coverage-report artifact does not exist on this rail, so both ratchet
|
|
# invocations run --allow-missing: coverage.* metrics skip gracefully while
|
|
# the deterministic ones (eslint / openapi-coverage / i18n-ui) stay BLOCKING.
|
|
# Coverage authority remains on the main rail (ci.yml test-coverage → quality-gate).
|
|
- run: npm run quality:collect
|
|
- name: Ratchet check (blocking)
|
|
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --summary .artifacts/quality-ratchet.md
|
|
- name: Require-tighten (blocking)
|
|
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --require-tighten
|
|
# CodeQL alerts ratchet — same semantics as ci.yml quality-gate: exits 1 ONLY
|
|
# on a real regression (open alerts > baseline in quality-baseline.json);
|
|
# a measurement failure (gh/auth/api) self-skips with exit 0.
|
|
- name: CodeQL alerts ratchet (blocking)
|
|
run: npm run check:codeql-ratchet
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
- name: Append ratchet summary
|
|
if: always()
|
|
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY" || true
|
|
|
|
# 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
|