mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
chore(quality): release-green pre-flight validator + nightly signal (C+D) (#4622)
C — scripts/quality/validate-release-green.mjs (npm run check:release-green): reproduces the release-equivalent validation (typecheck, eslint, db-rules, public-creds, full unit, vitest, ratchets, optional --with-build package-artifact) against the current working tree and classifies each red as HARD (real defect, exit 1) vs DRIFT (ratchet — reported, never affects exit / never blocks). Pure helpers exported + orchestration behind a direct-run guard; unit-tested. D — .github/workflows/nightly-release-green.yml: runs C on the active release branch nightly (and on workflow_dispatch) and opens/updates a single tracking issue on HARD failures. Never a required check, never touches a contributor PR. Closes the gap where the full gate (ci.yml) only ran on the release PR, so reds accrued silently on release/** and surfaced in 40-min layers at release time. Non-blocking by construction; drift is the maintainer's to rebaseline at release. Co-authored-by: Diego Rodrigues de Sa e Souza <diego.souza@cdwasolutions.com.br>
This commit is contained in:
committed by
GitHub
parent
5cb7621764
commit
e2522e2e0a
137
.github/workflows/nightly-release-green.yml
vendored
Normal file
137
.github/workflows/nightly-release-green.yml
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
name: Nightly Release-Green
|
||||
|
||||
# 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
|
||||
# nightly reproduces the release-equivalent validation on the active release branch
|
||||
# HEAD and, when there are HARD failures, opens/updates a single tracking issue.
|
||||
#
|
||||
# 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:
|
||||
schedule:
|
||||
- cron: "23 5 * * *" # 05:23 UTC daily — off-peak, distinct from other nightlies
|
||||
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:
|
||||
group: nightly-release-green
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release-green:
|
||||
name: Validate active release branch
|
||||
runs-on: 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 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "${INPUT_BRANCH:-}" ]; then
|
||||
TARGET="$INPUT_BRANCH"
|
||||
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@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
|
||||
- name: Release-green validation (full)
|
||||
id: validate
|
||||
run: |
|
||||
set +e
|
||||
node scripts/quality/validate-release-green.mjs --json --with-build \
|
||||
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 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TITLE="🔴 Release branch not green: ${TARGET}"
|
||||
{
|
||||
echo "The nightly **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}"
|
||||
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" --body-file issue-body.md
|
||||
fi
|
||||
|
||||
- name: Upload report artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-green-report
|
||||
path: |
|
||||
release-green.json
|
||||
release-green.log
|
||||
if-no-files-found: ignore
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
_In development — bullets added per PR; finalized at release._
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(quality): release-green pre-flight validator + nightly signal** — new `npm run check:release-green` (`scripts/quality/validate-release-green.mjs`) reproduces the release-equivalent validation (full unit + vitest + ratchets + typecheck + lint, optional `--with-build` package-artifact) against the current working tree and classifies each red as **HARD** (real defect) vs **DRIFT** (ratchet, rebaselined at release) — purely diagnostic, never blocking contributors. A new `nightly-release-green` workflow runs it on the active release branch and opens/updates a tracking issue on hard failures. Closes the gap where the full gate (`ci.yml`) only ran on the release PR, so reds accrued silently on `release/**` and surfaced in layers at release time. (thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.33] — 2026-06-21
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
"check:complexity": "node scripts/check/check-complexity.mjs",
|
||||
"check:dead-code": "node scripts/check/check-dead-code.mjs",
|
||||
"check:cognitive-complexity": "node scripts/check/check-cognitive-complexity.mjs",
|
||||
"check:release-green": "node scripts/quality/validate-release-green.mjs",
|
||||
"check:type-coverage": "node scripts/check/check-type-coverage.mjs",
|
||||
"check:lockfile": "node scripts/check/check-lockfile.mjs",
|
||||
"check:bundle-size": "node scripts/check/check-bundle-size.mjs",
|
||||
|
||||
250
scripts/quality/validate-release-green.mjs
Normal file
250
scripts/quality/validate-release-green.mjs
Normal file
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/quality/validate-release-green.mjs
|
||||
//
|
||||
// "Release-green" pre-flight validator (Solution C).
|
||||
//
|
||||
// WHY: the full gate (ci.yml — unit shards, vitest, ratchets, package-artifact)
|
||||
// runs ONLY on the release PR (PR → main). PRs into release/** only get the
|
||||
// fast-gates (quality.yml: TIA-impacted tests + typecheck + lint checks). So
|
||||
// reds accumulate silently on the release branch and explode — in layers — at
|
||||
// release time. This script reproduces the release-equivalent validation against
|
||||
// the CURRENT working tree so the maintainer (or the nightly, Solution D) can see
|
||||
// the real state of the release branch at any time.
|
||||
//
|
||||
// DESIGN — never blocking to contributors:
|
||||
// • HARD checks (typecheck, lint errors, unit, vitest, db-rules, public-creds,
|
||||
// optionally package-artifact) → a failure here is a real defect; exit 1.
|
||||
// • DRIFT checks (eslint WARNINGS, cognitive-complexity, file-size) → ratchet
|
||||
// drift accrued across the cycle is NOT a contributor's fault; it is reported
|
||||
// and rebaselined by the maintainer at release. Drift NEVER changes the exit
|
||||
// code, so wiring this as a check can never block anyone on drift.
|
||||
//
|
||||
// This script DIAGNOSES + REPORTS only (no auto-fix). The fix-to-green
|
||||
// orchestration lives in the (future) /green-prs + review-prs flows that call it.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/quality/validate-release-green.mjs [--json] [--with-build] [--quick]
|
||||
// --json emit machine-readable JSON to stdout (report goes to stderr)
|
||||
// --with-build also run check:pack-artifact (needs a dist/ build — slow)
|
||||
// --quick skip the slow unit + vitest suites (drift + typecheck + lint only)
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, "..", "..");
|
||||
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
|
||||
// ─── Pure helpers (exported for tests) ──────────────────────────────────────
|
||||
|
||||
/** Read the committed ratchet baseline value for a metric (null if unknown). */
|
||||
export function baselineValue(metric, root = ROOT) {
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(join(root, "config/quality/quality-baseline.json"), "utf8"));
|
||||
const metrics = raw.metrics || raw;
|
||||
const v = metrics?.[metric]?.value;
|
||||
return typeof v === "number" ? v : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Best-effort "first meaningful failure line" from captured command output. */
|
||||
export function firstFailureLine(out) {
|
||||
const lines = String(out || "")
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const hit = lines.find((l) => /✖|not ok|AssertionError|error TS|FAIL|Error:|REGRESS/i.test(l));
|
||||
return (hit || lines[lines.length - 1] || "failed").slice(0, 200);
|
||||
}
|
||||
|
||||
/** Sum {errorCount,warningCount} across an eslint --format json result array. */
|
||||
export function eslintCounts(parsed) {
|
||||
let errors = 0;
|
||||
let warnings = 0;
|
||||
for (const f of parsed || []) {
|
||||
errors += f.errorCount || 0;
|
||||
warnings += f.warningCount || 0;
|
||||
}
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
/** Parse the eslint JSON array out of mixed stdout (tolerates a leading banner). */
|
||||
export function parseEslintJson(out) {
|
||||
const start = String(out || "").indexOf("[");
|
||||
if (start < 0) return null;
|
||||
try {
|
||||
return JSON.parse(String(out).slice(start));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pull the cognitive-complexity violation count from the gate's output. */
|
||||
export function parseCognitiveCount(out) {
|
||||
const m = String(out || "").match(/(\d+)\s+(?:function\(s\) exceed|violações|violations)/i);
|
||||
return m ? Number(m[1]) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drift verdict for a ratchet: a metric that grew past its committed baseline is
|
||||
* "drift" (reported, never blocking). `direction:"down"` metrics (warnings,
|
||||
* complexity, file-size counts) regress when current > baseline.
|
||||
*/
|
||||
export function isDrift(current, baseline) {
|
||||
if (typeof current !== "number" || typeof baseline !== "number") return false;
|
||||
return current > baseline;
|
||||
}
|
||||
|
||||
/** releaseGreen iff there are zero failing HARD checks (drift never blocks). */
|
||||
export function computeVerdict(results) {
|
||||
const hardFailures = results.filter((r) => r.kind === "hard" && !r.ok);
|
||||
const drift = results.filter((r) => r.kind === "drift" && !r.ok);
|
||||
return { releaseGreen: hardFailures.length === 0, hardFailures, drift };
|
||||
}
|
||||
|
||||
// ─── Orchestration (only when run directly) ─────────────────────────────────
|
||||
|
||||
function run(cmd, cmdArgs) {
|
||||
try {
|
||||
const out = execFileSync(cmd, cmdArgs, {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
maxBuffer: 256 * 1024 * 1024,
|
||||
env: { ...process.env, FORCE_COLOR: "0" },
|
||||
});
|
||||
return { code: 0, out };
|
||||
} catch (err) {
|
||||
return {
|
||||
code: typeof err.status === "number" ? err.status : 1,
|
||||
out: `${err.stdout || ""}${err.stderr || ""}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const JSON_OUT = args.has("--json");
|
||||
const WITH_BUILD = args.has("--with-build");
|
||||
const QUICK = args.has("--quick");
|
||||
|
||||
const results = [];
|
||||
const record = (r) => {
|
||||
results.push(r);
|
||||
const icon = r.ok ? "✅" : r.kind === "drift" ? "🟡" : "❌";
|
||||
process.stderr.write(`${icon} [${r.kind}] ${r.label}${r.detail ? ` — ${r.detail}` : ""}\n`);
|
||||
};
|
||||
|
||||
const hardCmd = (id, label, cmd, cmdArgs) => {
|
||||
const { code, out } = run(cmd, cmdArgs);
|
||||
record({ id, label, kind: "hard", ok: code === 0, detail: code === 0 ? "pass" : firstFailureLine(out) });
|
||||
};
|
||||
|
||||
process.stderr.write("🔎 Release-green validation (current working tree)\n\n");
|
||||
|
||||
hardCmd("typecheck", "Typecheck (core)", npmCmd, ["run", "typecheck:core"]);
|
||||
|
||||
// ESLint: ONE pass → errors (hard) + warnings (drift)
|
||||
{
|
||||
const { out } = run("npx", ["eslint", ".", "--format", "json"]);
|
||||
const parsed = parseEslintJson(out);
|
||||
if (!parsed) {
|
||||
record({ id: "lint", label: "ESLint", kind: "hard", ok: false, detail: "could not parse eslint json" });
|
||||
} else {
|
||||
const { errors, warnings } = eslintCounts(parsed);
|
||||
record({ id: "lint-errors", label: "ESLint errors", kind: "hard", ok: errors === 0, detail: `${errors} error(s)` });
|
||||
const base = baselineValue("eslintWarnings");
|
||||
const over = isDrift(warnings, base);
|
||||
record({
|
||||
id: "eslint-warnings",
|
||||
label: "ESLint warnings (ratchet)",
|
||||
kind: "drift",
|
||||
ok: !over,
|
||||
detail:
|
||||
base == null
|
||||
? `${warnings} (no baseline)`
|
||||
: `${warnings} vs baseline ${base}${over ? ` (+${warnings - base} drift → rebaseline at release)` : ""}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hardCmd("db-rules", "DB rules", npmCmd, ["run", "check:db-rules"]);
|
||||
hardCmd("public-creds", "Public creds", npmCmd, ["run", "check:public-creds"]);
|
||||
|
||||
// Cognitive-complexity (drift)
|
||||
{
|
||||
const { out } = run(npmCmd, ["run", "check:cognitive-complexity"]);
|
||||
const current = parseCognitiveCount(out);
|
||||
const base = baselineValue("cognitiveComplexity");
|
||||
const over = isDrift(current, base);
|
||||
record({
|
||||
id: "cognitive-complexity",
|
||||
label: "Cognitive complexity (ratchet)",
|
||||
kind: "drift",
|
||||
ok: !over,
|
||||
detail:
|
||||
current == null
|
||||
? "could not parse count"
|
||||
: `${current} vs baseline ${base}${over ? ` (+${current - base} drift → rebaseline at release)` : ""}`,
|
||||
});
|
||||
}
|
||||
|
||||
// file-size (drift)
|
||||
{
|
||||
const { code, out } = run(npmCmd, ["run", "check:file-size"]);
|
||||
record({
|
||||
id: "file-size",
|
||||
label: "File-size ratchet",
|
||||
kind: "drift",
|
||||
ok: code === 0,
|
||||
detail: code === 0 ? "within frozen caps" : firstFailureLine(out),
|
||||
});
|
||||
}
|
||||
|
||||
if (!QUICK) {
|
||||
hardCmd("unit", "Unit tests (full, CI concurrency)", npmCmd, ["run", "test:unit:ci"]);
|
||||
hardCmd("vitest", "Vitest (MCP / autoCombo / cache)", npmCmd, ["run", "test:vitest"]);
|
||||
}
|
||||
if (WITH_BUILD) {
|
||||
hardCmd("pack-artifact", "Package artifact (npm pack policy)", npmCmd, ["run", "check:pack-artifact"]);
|
||||
}
|
||||
|
||||
const { releaseGreen, hardFailures, drift } = computeVerdict(results);
|
||||
|
||||
process.stderr.write("\n──────── verdict ────────\n");
|
||||
process.stderr.write(`HARD failures (block — real defects): ${hardFailures.length}\n`);
|
||||
hardFailures.forEach((r) => process.stderr.write(` ❌ ${r.label}: ${r.detail}\n`));
|
||||
process.stderr.write(`Ratchet drift (non-blocking — rebaseline at release): ${drift.length}\n`);
|
||||
drift.forEach((r) => process.stderr.write(` 🟡 ${r.label}: ${r.detail}\n`));
|
||||
process.stderr.write(
|
||||
releaseGreen
|
||||
? "\n✅ RELEASE-GREEN (no hard failures). Any drift above is rebaselined at release, not a contributor concern.\n"
|
||||
: "\n❌ NOT release-green — hard failures must be fixed (in the originating PR branch, via co-authorship).\n"
|
||||
);
|
||||
|
||||
if (JSON_OUT) {
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
{
|
||||
releaseGreen,
|
||||
hardFailures: hardFailures.map((r) => ({ id: r.id, label: r.label, detail: r.detail })),
|
||||
drift: drift.map((r) => ({ id: r.id, label: r.label, detail: r.detail })),
|
||||
checks: results.map((r) => ({ id: r.id, kind: r.kind, ok: r.ok, detail: r.detail })),
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + "\n"
|
||||
);
|
||||
}
|
||||
|
||||
process.exit(releaseGreen ? 0 : 1);
|
||||
}
|
||||
|
||||
// Run only when invoked directly (so tests can import the pure helpers).
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
main();
|
||||
}
|
||||
74
tests/unit/validate-release-green.test.ts
Normal file
74
tests/unit/validate-release-green.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// Pure helpers of the release-green validator (Solution C). The orchestration is
|
||||
// guarded behind a direct-run check, so importing the module here is side-effect-free.
|
||||
const mod = await import("../../scripts/quality/validate-release-green.mjs");
|
||||
const {
|
||||
firstFailureLine,
|
||||
eslintCounts,
|
||||
parseEslintJson,
|
||||
parseCognitiveCount,
|
||||
isDrift,
|
||||
computeVerdict,
|
||||
} = mod;
|
||||
|
||||
test("eslintCounts sums errors + warnings across files", () => {
|
||||
const parsed = [
|
||||
{ errorCount: 2, warningCount: 5 },
|
||||
{ errorCount: 0, warningCount: 3 },
|
||||
{},
|
||||
];
|
||||
assert.deepEqual(eslintCounts(parsed), { errors: 2, warnings: 8 });
|
||||
});
|
||||
|
||||
test("parseEslintJson tolerates a leading non-JSON banner", () => {
|
||||
const out = "npm warn something\n[{\"errorCount\":0,\"warningCount\":1}]";
|
||||
assert.deepEqual(parseEslintJson(out), [{ errorCount: 0, warningCount: 1 }]);
|
||||
assert.equal(parseEslintJson("no json here"), null);
|
||||
});
|
||||
|
||||
test("parseCognitiveCount reads the gate's count (en + pt)", () => {
|
||||
assert.equal(parseCognitiveCount("[cognitive-complexity] 797 function(s) exceed the threshold (15)."), 797);
|
||||
assert.equal(parseCognitiveCount("[cognitive-complexity] REGRESSÃO — 801 violações > baseline 797"), 801);
|
||||
assert.equal(parseCognitiveCount("no number"), null);
|
||||
});
|
||||
|
||||
test("isDrift flags only growth past the committed baseline (down-direction ratchets)", () => {
|
||||
assert.equal(isDrift(3900, 3867), true); // grew → drift
|
||||
assert.equal(isDrift(3867, 3867), false); // equal → ok
|
||||
assert.equal(isDrift(3800, 3867), false); // improved → ok
|
||||
assert.equal(isDrift(10, null), false); // no baseline → never drift
|
||||
assert.equal(isDrift(null, 10), false); // unparsed → never drift
|
||||
});
|
||||
|
||||
test("firstFailureLine surfaces the meaningful failure, not boilerplate", () => {
|
||||
const out = [
|
||||
"> omniroute@3.8.34 typecheck:core",
|
||||
"src/x.ts(10,5): error TS2322: Type 'string' is not assignable to 'number'.",
|
||||
"done",
|
||||
].join("\n");
|
||||
assert.match(firstFailureLine(out), /error TS2322/);
|
||||
});
|
||||
|
||||
test("computeVerdict: releaseGreen iff zero HARD failures (drift never blocks)", () => {
|
||||
const onlyDrift = computeVerdict([
|
||||
{ kind: "hard", ok: true },
|
||||
{ kind: "drift", ok: false },
|
||||
]);
|
||||
assert.equal(onlyDrift.releaseGreen, true);
|
||||
assert.equal(onlyDrift.drift.length, 1);
|
||||
|
||||
const hardFail = computeVerdict([
|
||||
{ kind: "hard", ok: false },
|
||||
{ kind: "drift", ok: false },
|
||||
]);
|
||||
assert.equal(hardFail.releaseGreen, false);
|
||||
assert.equal(hardFail.hardFailures.length, 1);
|
||||
|
||||
const allGreen = computeVerdict([
|
||||
{ kind: "hard", ok: true },
|
||||
{ kind: "drift", ok: true },
|
||||
]);
|
||||
assert.equal(allGreen.releaseGreen, true);
|
||||
});
|
||||
Reference in New Issue
Block a user