mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 04:12:17 +03:00
Owner decision (2026-08-30): shipping speed matters more than holding the debt line
until the v4.0 LTS modularization; the base was going red on every merge batch and
each red baseline cost a sweep.
Relaxation (one auditable pass, scripts/quality/relax-baselines.mjs):
- quality-baseline.json metrics: lower-is-better ×1.2, higher-is-better ÷1.2
(coverage floor 60 kept; eslintErrors stays 0; eslintWarnings 0 → 1050 = 20% of
the 5,247 frozen suppressions). Adds `_policy {phase: velocity, until: 4.0.0,
relaxPct: 20, requireTighten: false}` + a `_relax_velocity_2026_08_30` note
listing every before → after.
- complexity count 2681 → 3218; duplication 5.72 → 6.86; file-size cap/testCap
1000 → 1200 and all 127 frozen caps ×1.2; api/dashboard/open-sse typecheck
per-file counts ×1.2; openapi-coverage THRESHOLD 36 → 30.
- check-quality-ratchet: --require-tighten is advisory while _policy.requireTighten
is false (2 new tests); nightly bank-ratchet-shrinks pauses during the phase (it
would bank the measured shrink and undo the headroom every night).
Monitoring (scripts/quality/baseline-headroom.mjs, npm run quality:headroom):
measures each numeric gate the way CI does, prints live / baseline / headroom per
gate (ok ≥10%, warn <10%, critical <0); the new nightly `baseline-headroom` job
posts the table to the living issue "📈 Baseline headroom (velocity phase)" and
toggles the `headroom-alert` label. 6 unit tests on the pure helpers.
Also aligns the remaining red tests on the tip to contracts already merged:
#11775 (FREE lease-capable connections are ordinary capacity: gate inventory 48/97/99,
sse-auth selection, warmup scheduler), #11794 (dual-loopback readiness probe), and the
8 vi strings #11775 left as __MISSING__.
Docs: QUALITY_GATES.md → "Velocity phase" (what changed, tooling, how to close the
phase at 4.0), AGENTS.md quick reference.
563 lines
26 KiB
YAML
563 lines
26 KiB
YAML
name: Release-Green (continuous)
|
||
|
||
# Solution D — continuous, NON-BLOCKING drift signal for the active release branch.
|
||
#
|
||
# WHY: the full gate (ci.yml) only runs on the release PR (PR → main), so reds
|
||
# accrue silently on release/** and explode — in layers — at release time. This
|
||
# workflow reproduces the release-equivalent validation on the release branch and,
|
||
# when there are HARD failures, opens/updates a single tracking issue.
|
||
#
|
||
# WS5.1 (v3.8.49 quality plan) — two modes:
|
||
# push to release/v* (code paths) → --quick (fast HARD gates, ~5-8min). Catches the
|
||
# captain's direct pushes (sync-back — the one ungated write path) AND the merged
|
||
# COMBINATION right after every PR merge, attributing the offending push range in
|
||
# the issue. Base-red MTTD drops from ≤24h to ≤~15min after the offending push.
|
||
# schedule (3×/day) → full --with-build --full-ci (the deep sweep incl. build+suites).
|
||
#
|
||
# It is NOT a required status check and never touches a contributor PR — it only
|
||
# reports. Ratchet drift (eslint warnings / cognitive-complexity / file-size) is
|
||
# expected mid-cycle and is reported but never raises the alarm on its own; only
|
||
# real defects (typecheck / lint errors / unit / vitest / db-rules / public-creds /
|
||
# package-artifact) flip the issue open.
|
||
|
||
on:
|
||
push:
|
||
branches: ["release/v*", "main"]
|
||
paths:
|
||
- "src/**"
|
||
- "open-sse/**"
|
||
- "bin/**"
|
||
- "electron/**"
|
||
- "scripts/**"
|
||
- "tests/**"
|
||
- "config/**"
|
||
- "package.json"
|
||
- "package-lock.json"
|
||
- "tsconfig*.json"
|
||
schedule:
|
||
- cron: "23 5 * * *" # full sweep — off-peak, distinct from other nightlies
|
||
- cron: "23 12 * * *" # full sweep — midday (WS5.1: 3×/day instead of 1×)
|
||
- cron: "23 18 * * *" # full sweep — evening
|
||
workflow_dispatch:
|
||
inputs:
|
||
branch:
|
||
description: "Release branch to validate (default: highest release/vX.Y.Z)"
|
||
required: false
|
||
type: string
|
||
|
||
permissions:
|
||
contents: read
|
||
issues: write
|
||
|
||
concurrency:
|
||
# push storms during merge campaigns collapse to the newest commit per branch;
|
||
# scheduled full sweeps keep their own single lane.
|
||
group: release-green-${{ github.event_name }}-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
env:
|
||
OMNIROUTE_SKIP_SYSTEM_TRUST: "1"
|
||
|
||
jobs:
|
||
release-green:
|
||
name: Validate active release branch
|
||
# On a push, only run for release/* pushes — a push to main is handled by the
|
||
# main-green job below. Schedule/dispatch always run (they validate the highest release).
|
||
if: ${{ github.event_name != 'push' || startsWith(github.ref_name, 'release/') }}
|
||
# Dynamic runner: with USE_VPS_RUNNER=true (release window / on-demand pre-flight)
|
||
# this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY,
|
||
# no local noauth CLIs => zero machine-specific false positives) and no contention.
|
||
# Nightly cron normally finds the var false (VM off) and falls back to hosted.
|
||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-build"]')) || 'ubuntu-latest' }}
|
||
env:
|
||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 0
|
||
persist-credentials: false
|
||
|
||
- name: Resolve active release branch
|
||
id: branch
|
||
env:
|
||
INPUT_BRANCH: ${{ github.event.inputs.branch }}
|
||
EVENT_NAME: ${{ github.event_name }}
|
||
PUSHED_REF: ${{ github.ref_name }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -n "${INPUT_BRANCH:-}" ]; then
|
||
TARGET="$INPUT_BRANCH"
|
||
elif [ "$EVENT_NAME" = "push" ]; then
|
||
# validate exactly what was pushed, not the highest branch
|
||
TARGET="$PUSHED_REF"
|
||
else
|
||
# highest release/vX.Y.Z by semver among remote branches
|
||
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
|
||
| sed 's#origin/##' \
|
||
| sort -t/ -k2 -V \
|
||
| tail -1)
|
||
fi
|
||
if [ -z "$TARGET" ]; then echo "No release/v* branch found"; exit 1; fi
|
||
# Strict format guard — reject anything that isn't release/vX.Y.Z (blocks
|
||
# ref/command injection via the workflow_dispatch input).
|
||
if ! printf '%s' "$TARGET" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||
echo "Refusing non-canonical branch name: $TARGET"; exit 1
|
||
fi
|
||
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
|
||
echo "Active release branch: $TARGET"
|
||
|
||
- name: Checkout the release branch
|
||
env:
|
||
TARGET: ${{ steps.branch.outputs.target }}
|
||
run: |
|
||
set -euo pipefail
|
||
git checkout "$TARGET"
|
||
git log -1 --oneline
|
||
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: "24"
|
||
cache: npm
|
||
|
||
- uses: ./.github/actions/npm-ci-retry
|
||
|
||
- name: Release-green validation (full)
|
||
id: validate
|
||
env:
|
||
EVENT_NAME: ${{ github.event_name }}
|
||
run: |
|
||
set +e
|
||
# --hermetic: scrub live-test trigger vars (self-hosted runner may carry
|
||
# operator env; hosted ignores the unknown flag before #6300 lands).
|
||
# push → --quick: fast HARD gates only (~5-8min), per-merge signal.
|
||
# schedule/dispatch → --with-build --full-ci: ALSO run every static gate from
|
||
# ci.yml's gate jobs (lint, quality-gate, quality-extended, docs-sync-strict,
|
||
# pr-test-policy) + build + full suites. PRs into release/** only get the
|
||
# fast-gates, so these accrue silently and explode in layers on the release PR
|
||
# (v3.8.46: 11 static base-reds leaked).
|
||
if [ "$EVENT_NAME" = "push" ]; then
|
||
MODE="--quick"
|
||
else
|
||
MODE="--with-build --full-ci"
|
||
fi
|
||
echo "[release-green] mode: $MODE (event: $EVENT_NAME)"
|
||
# MODE is an intentional flag list, so word-splitting is wanted here
|
||
# shellcheck disable=SC2086
|
||
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
|
||
1> release-green.json 2> release-green.log
|
||
echo "exit=$?" >> "$GITHUB_OUTPUT"
|
||
echo "------- report -------"
|
||
cat release-green.log
|
||
|
||
- name: Open / update tracking issue on HARD failure
|
||
if: steps.validate.outputs.exit != '0'
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
TARGET: ${{ steps.branch.outputs.target }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
EVENT_NAME: ${{ github.event_name }}
|
||
BEFORE_SHA: ${{ github.event.before }}
|
||
AFTER_SHA: ${{ github.event.after }}
|
||
run: |
|
||
set -euo pipefail
|
||
TITLE="🔴 Release branch not green: ${TARGET}"
|
||
{
|
||
echo "The **release-green** validation found HARD failures on \`${TARGET}\`."
|
||
echo "These are real defects that would block the release PR — fix them in the"
|
||
echo "originating PR branch (via co-authorship), not by demanding it from contributors."
|
||
echo ""
|
||
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
|
||
# WS5.1 attribution: on push events the offending change IS this push's range
|
||
# (one merge per push in the normal queue), so name it — no bisect needed.
|
||
if [ "$EVENT_NAME" = "push" ] && [ -n "${BEFORE_SHA:-}" ] && \
|
||
git cat-file -e "$BEFORE_SHA" 2>/dev/null; then
|
||
echo ""
|
||
echo "**Offending push range** (\`${BEFORE_SHA:0:9}..${AFTER_SHA:0:9}\`):"
|
||
echo '```'
|
||
git log --no-decorate --oneline "${BEFORE_SHA}..${AFTER_SHA}" | head -20
|
||
echo '```'
|
||
fi
|
||
echo ""
|
||
echo '```'
|
||
sed -n '/──────── verdict ────────/,$p' release-green.log || tail -40 release-green.log
|
||
echo '```'
|
||
echo ""
|
||
echo "_Ratchet drift (eslint warnings / cognitive-complexity / file-size) listed above is expected mid-cycle and is rebaselined at release — it is NOT a contributor concern and did not, on its own, open this issue._"
|
||
} > issue-body.md
|
||
|
||
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||
if [ -n "$EXISTING" ]; then
|
||
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
|
||
echo "Updated existing issue #$EXISTING"
|
||
else
|
||
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
|
||
fi
|
||
|
||
- name: Close tracking issue when the branch is green again
|
||
if: steps.validate.outputs.exit == '0'
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
TARGET: ${{ steps.branch.outputs.target }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
run: |
|
||
set -euo pipefail
|
||
# The open/update step above is the UPWARD half of the loop; without this
|
||
# step a stale "not green" issue outlives the fix and every base-green check
|
||
# (`AGENTS.md` → "Base-green check") keeps stamping new PRs as base-red inherited.
|
||
TITLE="🔴 Release branch not green: ${TARGET}"
|
||
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||
if [ -n "$EXISTING" ]; then
|
||
gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \
|
||
--comment "✅ \`${TARGET}\` is release-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)."
|
||
echo "Closed issue #$EXISTING"
|
||
fi
|
||
|
||
- name: Upload report artifact
|
||
if: always()
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: release-green-report
|
||
path: |
|
||
release-green.json
|
||
release-green.log
|
||
if-no-files-found: ignore
|
||
|
||
# Companion arm for `main`. Under the parallel-cycle model, main only receives merged
|
||
# work at the release squash — so a gate/infra fix that lands only on release leaves
|
||
# main red the whole cycle, and repo-wide gates (CodeQL alert count, ratchet baselines)
|
||
# turn EVERY PR into main red on a check unrelated to its diff. This detects that and
|
||
# opens a "🔴 main not green" tracking issue. The PREVENTION is the companion-PR reflex
|
||
# (Hard Rule #21 area / _shared/merge-gates.md §8); this is the automated backstop.
|
||
main-green:
|
||
name: Validate main branch
|
||
# On a push, only run for a push to main — a push to release/* is handled by
|
||
# release-green above. Schedule/dispatch always run (they also sweep main).
|
||
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
|
||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-build"]')) || 'ubuntu-latest' }}
|
||
env:
|
||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
ref: main # literal — no injection surface; scheduled runs default to the repo default branch (a release/v*), so pin main explicitly
|
||
fetch-depth: 0
|
||
persist-credentials: false
|
||
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: "24"
|
||
cache: npm
|
||
|
||
- uses: ./.github/actions/npm-ci-retry
|
||
|
||
- name: Main-green validation
|
||
id: validate
|
||
env:
|
||
EVENT_NAME: ${{ github.event_name }}
|
||
run: |
|
||
set +e
|
||
# push (a merge into main) → --quick fast HARD gates; schedule/dispatch → full sweep.
|
||
if [ "$EVENT_NAME" = "push" ]; then
|
||
MODE="--quick"
|
||
else
|
||
MODE="--with-build --full-ci"
|
||
fi
|
||
echo "[main-green] mode: $MODE (event: $EVENT_NAME)"
|
||
# MODE is an intentional flag list, so word-splitting is wanted here
|
||
# shellcheck disable=SC2086
|
||
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
|
||
1> main-green.json 2> main-green.log
|
||
echo "exit=$?" >> "$GITHUB_OUTPUT"
|
||
echo "------- report -------"
|
||
cat main-green.log
|
||
|
||
- name: Open / update tracking issue on HARD failure
|
||
if: steps.validate.outputs.exit != '0'
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
EVENT_NAME: ${{ github.event_name }}
|
||
run: |
|
||
set -euo pipefail
|
||
TITLE="🔴 main branch not green"
|
||
{
|
||
echo "The **main-green** validation found HARD failures on \`main\`."
|
||
echo ""
|
||
echo "Because \`main\` only receives merged work at the release squash, a gate/infra"
|
||
echo "fix that landed only on the release branch leaves \`main\` broken for the whole"
|
||
echo "cycle — and repo-wide gates (CodeQL alert count, ratchet baselines) then turn"
|
||
echo "**every open PR into main** red on a check unrelated to its diff. The fix is a"
|
||
echo "companion PR \`--base main\` carrying the release-side fix (see"
|
||
echo "\`_shared/merge-gates.md\` §8), NOT chasing each contributor PR."
|
||
echo ""
|
||
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
|
||
echo ""
|
||
echo '```'
|
||
sed -n '/──────── verdict ────────/,$p' main-green.log || tail -40 main-green.log
|
||
echo '```'
|
||
echo ""
|
||
echo "_Ratchet drift (eslint warnings / cognitive-complexity / file-size) is expected mid-cycle and did NOT, on its own, open this issue._"
|
||
} > issue-body.md
|
||
|
||
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||
if [ -n "$EXISTING" ]; then
|
||
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
|
||
echo "Updated existing issue #$EXISTING"
|
||
else
|
||
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
|
||
fi
|
||
|
||
- name: Close tracking issue when the branch is green again
|
||
if: steps.validate.outputs.exit == '0'
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
run: |
|
||
set -euo pipefail
|
||
# The open/update step above is the UPWARD half of the loop; without this
|
||
# step a stale "not green" issue outlives the fix and every base-green check
|
||
# (`AGENTS.md` → "Base-green check") keeps stamping new PRs as base-red inherited.
|
||
TITLE="🔴 main branch not green"
|
||
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||
if [ -n "$EXISTING" ]; then
|
||
gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \
|
||
--comment "✅ \`main\` is main-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)."
|
||
echo "Closed issue #$EXISTING"
|
||
fi
|
||
|
||
- name: Upload report artifact
|
||
if: always()
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: main-green-report
|
||
path: |
|
||
main-green.json
|
||
main-green.log
|
||
if-no-files-found: ignore
|
||
|
||
# ── Banking lane (#8584) ──────────────────────────────────────────────────
|
||
# The ratchet is asymmetric: RAISING a cap is a ten-second manual JSON edit made
|
||
# under merge pressure, LOWERING one requires someone to run `--update` and commit
|
||
# — which no workflow does. Grep `.github/workflows/` for `--update`: only
|
||
# wiki-sync.yml (unrelated) and ci.yml's check-quality-ratchet.mjs --require-tighten
|
||
# (a different script, a different metric). So a cap outlives the code that earned
|
||
# it and every completed decomposition silently becomes a growth allowance for
|
||
# whoever touches the file next. Measured on 2026-07-25: 18 frozen files already at
|
||
# or under the 800-line new-file cap, up to 132x (schemas.ts, 19 lines / 2523 cap),
|
||
# and 31 unfulfilled "tighten via --update next cycle" notes honoured exactly once
|
||
# (-1 unit) in six weeks.
|
||
#
|
||
# This job closes that loop by making the DOWNWARD direction as automatic as the
|
||
# upward one is easy. It measures the active release branch, runs the shrink-only
|
||
# `--update` paths, and opens ONE always-current PR with the result. It never
|
||
# pushes to release/* — a human still merges, so a bad measurement cannot land
|
||
# unreviewed. verify-ratchet-bank.mjs is the hard guarantee that the automation can
|
||
# only ever write in the shrink direction; if anything was raised, added, or a
|
||
# rebaseline note was touched, the job aborts and opens nothing.
|
||
#
|
||
# Schedule/dispatch only, deliberately NOT on push: banking has no latency
|
||
# requirement (a shrink banked within 8h is fine) and a per-merge run would rebuild
|
||
# the PR branch repeatedly during merge campaigns while paying for a full ESLint
|
||
# walk each time. Detection stays on push (release-green above); banking is batched.
|
||
bank-ratchet-shrinks:
|
||
name: Bank ratchet shrinks
|
||
if: ${{ github.event_name != 'push' }}
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
pull-requests: write
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Resolve active release branch
|
||
id: branch
|
||
env:
|
||
INPUT_BRANCH: ${{ github.event.inputs.branch }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -n "${INPUT_BRANCH:-}" ]; then
|
||
TARGET="$INPUT_BRANCH"
|
||
else
|
||
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
|
||
| sed 's#origin/##' \
|
||
| sort -t/ -k2 -V \
|
||
| tail -1)
|
||
fi
|
||
if [ -z "$TARGET" ]; then echo "No release/v* branch found"; exit 1; fi
|
||
# Same strict guard as the validation job — blocks ref/command injection
|
||
# through the workflow_dispatch input.
|
||
if ! printf '%s' "$TARGET" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||
echo "Refusing non-canonical branch name: $TARGET"; exit 1
|
||
fi
|
||
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
|
||
echo "bank_branch=chore/bank-ratchet-${TARGET#release/}" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Checkout the release branch
|
||
env:
|
||
TARGET: ${{ steps.branch.outputs.target }}
|
||
run: |
|
||
set -euo pipefail
|
||
git checkout "$TARGET"
|
||
git log -1 --oneline
|
||
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: "24"
|
||
cache: npm
|
||
|
||
- uses: ./.github/actions/npm-ci-retry
|
||
|
||
- name: Ratchet the baselines down
|
||
run: |
|
||
# Both --update paths are shrink-only by construction (file-size writes only
|
||
# on `improvements`, complexity-ratchets only when `.improved`), and both exit
|
||
# non-zero while the branch is over baseline — which is exactly when there is
|
||
# nothing to bank. Their exit code is not the signal; the verifier below is.
|
||
# Velocity phase (quality-baseline.json `_policy`, relax-baselines.mjs): the caps
|
||
# were raised on purpose, so banking the measured shrink would silently undo the
|
||
# 20% headroom every night. Pause the downward ratchet until the phase closes.
|
||
if node -e 'process.exit(require("./config/quality/quality-baseline.json")._policy?.phase === "velocity" ? 0 : 1)'; then
|
||
echo "Velocity phase active — ratchet banking paused (see docs/architecture/QUALITY_GATES.md → Velocity phase)."
|
||
exit 0
|
||
fi
|
||
set +e
|
||
node scripts/check/check-file-size.mjs --update
|
||
node scripts/check/check-complexity-ratchets.mjs --update
|
||
exit 0
|
||
|
||
- name: Verify the write only went downward
|
||
id: verify
|
||
run: |
|
||
set -euo pipefail
|
||
# Exits 1 if ANYTHING was raised/added or a rebaseline note was touched.
|
||
# `set -e` then aborts the job before a commit exists — no PR is opened.
|
||
node scripts/quality/verify-ratchet-bank.mjs > bank-summary.md
|
||
if [ -n "$(git status --porcelain config/quality/)" ]; then
|
||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||
echo "Nothing to bank — baselines already match the code."
|
||
fi
|
||
|
||
- name: Open / update the banking PR
|
||
if: steps.verify.outputs.changed == 'true'
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
TARGET: ${{ steps.branch.outputs.target }}
|
||
BANK_BRANCH: ${{ steps.branch.outputs.bank_branch }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
run: |
|
||
set -euo pipefail
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||
git checkout -B "$BANK_BRANCH"
|
||
git add config/quality/
|
||
git commit -m "chore(quality): bank ratchet shrinks measured on ${TARGET}"
|
||
git push --force origin "$BANK_BRANCH"
|
||
|
||
{
|
||
echo "Automated banking of quality-ratchet **shrinks** already present in"
|
||
echo "\`${TARGET}\` — the downward half of the ratchet, which nothing else runs (#8584)."
|
||
echo ""
|
||
echo "Produced by \`check:file-size --update\` + \`check:complexity-ratchets --update\`,"
|
||
echo "then verified by \`scripts/quality/verify-ratchet-bank.mjs\`: **nothing was raised,"
|
||
echo "nothing was added, no rebaseline note was touched** — the job aborts without"
|
||
echo "opening a PR if any of those is violated."
|
||
echo ""
|
||
echo "No product code changes. Merging retires growth allowances that the code no"
|
||
echo "longer needs; not merging leaves them available to whoever edits those files next."
|
||
echo ""
|
||
cat bank-summary.md
|
||
echo ""
|
||
echo "**Run:** ${RUN_URL}"
|
||
} > pr-body.md
|
||
|
||
EXISTING=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BANK_BRANCH" \
|
||
--state open --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||
if [ -n "$EXISTING" ]; then
|
||
gh pr edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file pr-body.md
|
||
echo "Updated existing PR #$EXISTING"
|
||
else
|
||
gh pr create --repo "$GITHUB_REPOSITORY" --base "$TARGET" --head "$BANK_BRANCH" \
|
||
--title "chore(quality): bank ratchet shrinks (${TARGET})" --body-file pr-body.md
|
||
fi
|
||
|
||
# ── Baseline headroom (velocity phase, 2026-08-30 → v4.0) ──────────────────────
|
||
# The ratchets only speak when a baseline is crossed. With every baseline loosened by
|
||
# 20% (scripts/quality/relax-baselines.mjs) the question is how fast the budget is
|
||
# being consumed — this job measures each gate the way CI does and posts the headroom
|
||
# table to one living issue, so a budget that fills in a week is visible before the
|
||
# first red PR. Advisory: never fails the workflow.
|
||
baseline-headroom:
|
||
name: Baseline headroom
|
||
if: ${{ github.event_name != 'push' }}
|
||
timeout-minutes: 60
|
||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-light"]')) || 'ubuntu-latest' }}
|
||
permissions:
|
||
contents: read
|
||
issues: write
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
with:
|
||
fetch-depth: 1
|
||
persist-credentials: false
|
||
|
||
- uses: actions/setup-node@v7
|
||
with:
|
||
node-version: "24"
|
||
cache: npm
|
||
|
||
- uses: ./.github/actions/npm-ci-retry
|
||
|
||
- name: Measure headroom on ${{ github.ref_name }}
|
||
run: |
|
||
set -euo pipefail
|
||
node scripts/quality/baseline-headroom.mjs \
|
||
--json reports/quality/headroom.json --md reports/quality/headroom.md
|
||
cat reports/quality/headroom.md >> "$GITHUB_STEP_SUMMARY"
|
||
|
||
- name: Upload headroom report
|
||
if: always()
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: baseline-headroom-${{ github.run_id }}
|
||
path: reports/quality/headroom.*
|
||
retention-days: 90
|
||
|
||
- name: Post to the living issue
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||
run: |
|
||
set -euo pipefail
|
||
TITLE="📈 Baseline headroom (velocity phase)"
|
||
BAD=$(node -e 'const r=require("./reports/quality/headroom.json").rows;console.log(r.filter(x=>x.status==="critical"||x.status==="warn").length)')
|
||
{
|
||
echo "Branch: \`${GITHUB_REF_NAME}\` · run: ${RUN_URL}"
|
||
echo ""
|
||
cat reports/quality/headroom.md
|
||
} > headroom-comment.md
|
||
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||
if [ -z "$EXISTING" ]; then
|
||
EXISTING=$(gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label quality-gate-finding \
|
||
--body "Living tracker for the velocity-phase baseline budget (docs/architecture/QUALITY_GATES.md → Velocity phase). One comment per nightly run; the newest comment is the current state." \
|
||
| grep -oE '[0-9]+$')
|
||
fi
|
||
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file headroom-comment.md
|
||
if [ "$BAD" != "0" ]; then
|
||
gh issue edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --add-label "headroom-alert" 2>/dev/null || true
|
||
else
|
||
gh issue edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --remove-label "headroom-alert" 2>/dev/null || true
|
||
fi
|