mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-22 06:42:19 +03:00
fix(ci): enforce exact-SHA release-green evidence and verdicts
This commit is contained in:
93
.github/workflows/nightly-release-green.yml
vendored
93
.github/workflows/nightly-release-green.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Release-Green (continuous)
|
||||
|
||||
# Solution D — continuous, NON-BLOCKING drift signal for the active release branch.
|
||||
# Continuous validation: artifacts, issue state and job exit must agree.
|
||||
#
|
||||
# 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
|
||||
@@ -31,6 +31,8 @@ on:
|
||||
- "scripts/**"
|
||||
- "tests/**"
|
||||
- "config/**"
|
||||
- ".github/workflows/**"
|
||||
- ".github/actions/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- "tsconfig*.json"
|
||||
@@ -53,7 +55,7 @@ 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
|
||||
cancel-in-progress: ${{ github.event_name == 'push' }}
|
||||
|
||||
env:
|
||||
OMNIROUTE_SKIP_SYSTEM_TRUST: "1"
|
||||
@@ -111,11 +113,21 @@ jobs:
|
||||
- name: Checkout the release branch
|
||||
env:
|
||||
TARGET: ${{ steps.branch.outputs.target }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PUSH_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git checkout "$TARGET"
|
||||
if [ "$EVENT_NAME" = "push" ]; then
|
||||
git checkout --detach "$PUSH_SHA"
|
||||
else
|
||||
git checkout --detach "origin/$TARGET"
|
||||
fi
|
||||
git log -1 --oneline
|
||||
|
||||
- name: Record validated revision
|
||||
id: revision
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
@@ -127,6 +139,7 @@ jobs:
|
||||
id: validate
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
VALIDATED_SHA: ${{ steps.revision.outputs.sha }}
|
||||
run: |
|
||||
set +e
|
||||
# --hermetic: scrub live-test trigger vars (self-hosted runner may carry
|
||||
@@ -147,7 +160,8 @@ jobs:
|
||||
# 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"
|
||||
validation_exit=$?
|
||||
node scripts/ci/release-green-result.mjs release-green.json "$validation_exit" "$VALIDATED_SHA" "$EVENT_NAME" >> release-green.log
|
||||
echo "------- report -------"
|
||||
cat release-green.log
|
||||
|
||||
@@ -165,23 +179,26 @@ jobs:
|
||||
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 "The required validation failed or its evidence was incomplete. Compare the"
|
||||
echo "signature on the exact base before attributing a defect to a contributor."
|
||||
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.
|
||||
# This is an observed range, not proof of causality. Compare with the base.
|
||||
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 "**Observed 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
|
||||
if grep -q '──────── verdict ────────' release-green.log; then
|
||||
sed -n '/──────── verdict ────────/,$p' release-green.log
|
||||
else
|
||||
tail -40 release-green.log
|
||||
fi
|
||||
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._"
|
||||
@@ -197,13 +214,19 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Close tracking issue when the branch is green again
|
||||
if: steps.validate.outputs.exit == '0'
|
||||
if: steps.validate.outputs.exit == '0' && github.event_name != 'push'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET: ${{ steps.branch.outputs.target }}
|
||||
VALIDATED_SHA: ${{ steps.revision.outputs.sha }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CURRENT_SHA=$(git ls-remote origin "refs/heads/$TARGET" | cut -f1)
|
||||
if [ "$CURRENT_SHA" != "$VALIDATED_SHA" ]; then
|
||||
echo "Branch advanced; retaining the tracking issue."
|
||||
exit 0
|
||||
fi
|
||||
# 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.
|
||||
@@ -212,7 +235,7 @@ jobs:
|
||||
--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)."
|
||||
--comment "✅ \`${TARGET}\` is release-green again at \`${VALIDATED_SHA}\` — ${RUN_URL}. Auto-closed after full validation of the current branch."
|
||||
echo "Closed issue #$EXISTING"
|
||||
fi
|
||||
|
||||
@@ -224,7 +247,15 @@ jobs:
|
||||
path: |
|
||||
release-green.json
|
||||
release-green.log
|
||||
if-no-files-found: ignore
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Enforce validation verdict
|
||||
if: always()
|
||||
env:
|
||||
VALIDATION_EXIT: ${{ steps.validate.outputs.exit }}
|
||||
VALIDATED_SHA: ${{ steps.revision.outputs.sha }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: node scripts/ci/release-green-result.mjs release-green.json "$VALIDATION_EXIT" "$VALIDATED_SHA" "$EVENT_NAME"
|
||||
|
||||
# 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
|
||||
@@ -245,10 +276,14 @@ jobs:
|
||||
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
|
||||
ref: ${{ github.event_name == 'push' && github.sha || 'main' }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Record validated revision
|
||||
id: revision
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
@@ -260,6 +295,7 @@ jobs:
|
||||
id: validate
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
VALIDATED_SHA: ${{ steps.revision.outputs.sha }}
|
||||
run: |
|
||||
set +e
|
||||
# push (a merge into main) → --quick fast HARD gates; schedule/dispatch → full sweep.
|
||||
@@ -273,7 +309,8 @@ jobs:
|
||||
# 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"
|
||||
validation_exit=$?
|
||||
node scripts/ci/release-green-result.mjs main-green.json "$validation_exit" "$VALIDATED_SHA" "$EVENT_NAME" >> main-green.log
|
||||
echo "------- report -------"
|
||||
cat main-green.log
|
||||
|
||||
@@ -299,7 +336,11 @@ jobs:
|
||||
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
|
||||
echo ""
|
||||
echo '```'
|
||||
sed -n '/──────── verdict ────────/,$p' main-green.log || tail -40 main-green.log
|
||||
if grep -q '──────── verdict ────────' main-green.log; then
|
||||
sed -n '/──────── verdict ────────/,$p' main-green.log
|
||||
else
|
||||
tail -40 main-green.log
|
||||
fi
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "_Ratchet drift (eslint warnings / cognitive-complexity / file-size) is expected mid-cycle and did NOT, on its own, open this issue._"
|
||||
@@ -315,12 +356,18 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Close tracking issue when the branch is green again
|
||||
if: steps.validate.outputs.exit == '0'
|
||||
if: steps.validate.outputs.exit == '0' && github.event_name != 'push'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VALIDATED_SHA: ${{ steps.revision.outputs.sha }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CURRENT_SHA=$(git ls-remote origin refs/heads/main | cut -f1)
|
||||
if [ "$CURRENT_SHA" != "$VALIDATED_SHA" ]; then
|
||||
echo "Branch advanced; retaining the tracking issue."
|
||||
exit 0
|
||||
fi
|
||||
# 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.
|
||||
@@ -329,7 +376,7 @@ jobs:
|
||||
--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)."
|
||||
--comment "✅ \`main\` is main-green again at \`${VALIDATED_SHA}\` — ${RUN_URL}. Auto-closed after full validation of the current branch."
|
||||
echo "Closed issue #$EXISTING"
|
||||
fi
|
||||
|
||||
@@ -341,7 +388,15 @@ jobs:
|
||||
path: |
|
||||
main-green.json
|
||||
main-green.log
|
||||
if-no-files-found: ignore
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Enforce validation verdict
|
||||
if: always()
|
||||
env:
|
||||
VALIDATION_EXIT: ${{ steps.validate.outputs.exit }}
|
||||
VALIDATED_SHA: ${{ steps.revision.outputs.sha }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: node scripts/ci/release-green-result.mjs main-green.json "$VALIDATION_EXIT" "$VALIDATED_SHA" "$EVENT_NAME"
|
||||
|
||||
# ── Banking lane (#8584) ──────────────────────────────────────────────────
|
||||
# The ratchet is asymmetric: RAISING a cap is a ten-second manual JSON edit made
|
||||
|
||||
74
scripts/ci/release-green-result.mjs
Normal file
74
scripts/ci/release-green-result.mjs
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env node
|
||||
// No npm dependencies: this verifier must still run after installation/validator failure.
|
||||
import { appendFileSync, readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export function verifyReleaseGreenReport(report, exitCode, candidateSha, eventName) {
|
||||
const expectedProfile =
|
||||
eventName === "push"
|
||||
? "quick"
|
||||
: ["schedule", "workflow_dispatch"].includes(eventName)
|
||||
? "full"
|
||||
: undefined;
|
||||
const valid =
|
||||
report?.schemaVersion === 1 &&
|
||||
/^[0-9a-f]{40}$/.test(candidateSha ?? "") &&
|
||||
report.candidateSha === candidateSha &&
|
||||
expectedProfile !== undefined &&
|
||||
report.profile === expectedProfile &&
|
||||
/^\d+$/.test(exitCode ?? "") &&
|
||||
Number(exitCode) <= 255 &&
|
||||
typeof report.releaseGreen === "boolean" &&
|
||||
Array.isArray(report.hardFailures) &&
|
||||
Array.isArray(report.checks) &&
|
||||
report.checks.length > 0 &&
|
||||
report.checks.every(
|
||||
(check) =>
|
||||
typeof check?.id === "string" &&
|
||||
check.id.length > 0 &&
|
||||
["hard", "drift"].includes(check.kind) &&
|
||||
typeof check.ok === "boolean"
|
||||
) &&
|
||||
report.checks.some((check) => check.kind === "hard") &&
|
||||
new Set(report.checks.map((check) => check.id)).size === report.checks.length;
|
||||
if (!valid)
|
||||
return {
|
||||
exit: 1,
|
||||
verdict: "UNKNOWN",
|
||||
evidenceState: "INCOMPLETE",
|
||||
reason: "Invalid or missing report/provenance",
|
||||
};
|
||||
const failed = report.checks.filter((check) => check.kind === "hard" && !check.ok);
|
||||
const consistent =
|
||||
report.releaseGreen === (failed.length === 0) &&
|
||||
report.hardFailures.length === failed.length &&
|
||||
failed.every((check) => report.hardFailures.some((failure) => failure?.id === check.id));
|
||||
if (!consistent || (exitCode === "0") !== report.releaseGreen) {
|
||||
return {
|
||||
exit: 1,
|
||||
verdict: "UNKNOWN",
|
||||
evidenceState: "INCOMPLETE",
|
||||
reason: "Exit and report disagree",
|
||||
};
|
||||
}
|
||||
return {
|
||||
exit: report.releaseGreen ? 0 : 1,
|
||||
verdict: report.releaseGreen ? "PASS" : "FAIL",
|
||||
evidenceState: "COMPLETE",
|
||||
reason: report.releaseGreen ? "Validated report" : "Required validation failed",
|
||||
};
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const [path, exitCode, candidateSha, eventName] = process.argv.slice(2);
|
||||
let report;
|
||||
try {
|
||||
report = JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch {
|
||||
/* verifier fails closed below */
|
||||
}
|
||||
const result = verifyReleaseGreenReport(report, exitCode, candidateSha, eventName);
|
||||
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `exit=${result.exit}\n`);
|
||||
console.log(JSON.stringify(result));
|
||||
process.exitCode = result.exit;
|
||||
}
|
||||
@@ -869,6 +869,13 @@ async function main() {
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
{
|
||||
schemaVersion: 1,
|
||||
candidateSha: execFileSync("git", ["rev-parse", "HEAD"], {
|
||||
cwd: ROOT,
|
||||
encoding: "utf8",
|
||||
}).trim(),
|
||||
profile: QUICK ? "quick" : WITH_BUILD && FULL_CI ? "full" : "standard",
|
||||
completedAt: new Date().toISOString(),
|
||||
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 })),
|
||||
|
||||
157
tests/unit/build/release-green-workflow-verdict.test.ts
Normal file
157
tests/unit/build/release-green-workflow-verdict.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const root = new URL("../../../", import.meta.url);
|
||||
type Step = { name?: string; id?: string; run?: string; if?: string; env?: Record<string, string> };
|
||||
const workflow = parse(
|
||||
readFileSync(new URL(".github/workflows/nightly-release-green.yml", root), "utf8")
|
||||
) as {
|
||||
jobs: Record<string, { steps: Step[] }>;
|
||||
};
|
||||
const sha = "1".repeat(40);
|
||||
|
||||
function execute(job: string, validatorExit: number, report: string, event = "schedule") {
|
||||
const dir = mkdtempSync(join(tmpdir(), "omni-green-workflow-"));
|
||||
try {
|
||||
mkdirSync(join(dir, "scripts/quality"), { recursive: true });
|
||||
mkdirSync(join(dir, "scripts/ci"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, "scripts/quality/validate-release-green.mjs"),
|
||||
"process.stdout.write(process.env.FIXTURE_REPORT); process.exit(Number(process.env.FIXTURE_EXIT));"
|
||||
);
|
||||
const verifier = new URL("scripts/ci/release-green-result.mjs", root);
|
||||
if (existsSync(verifier)) cpSync(verifier, join(dir, "scripts/ci/release-green-result.mjs"));
|
||||
const outputs = join(dir, "outputs");
|
||||
writeFileSync(outputs, "");
|
||||
const env = {
|
||||
...process.env,
|
||||
EVENT_NAME: event,
|
||||
GITHUB_OUTPUT: outputs,
|
||||
FIXTURE_EXIT: String(validatorExit),
|
||||
FIXTURE_REPORT: report,
|
||||
VALIDATED_SHA: sha,
|
||||
};
|
||||
const validate = workflow.jobs[job].steps.find((s) => s.id === "validate")!;
|
||||
const result = spawnSync("bash", ["-euo", "pipefail", "-c", validate.run!], {
|
||||
cwd: dir,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.equal(result.error, undefined);
|
||||
const values = Object.fromEntries(
|
||||
readFileSync(outputs, "utf8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((l) => l.split("="))
|
||||
);
|
||||
const enforce = workflow.jobs[job].steps.find((s) => s.name === "Enforce validation verdict");
|
||||
if (!enforce) return result.status;
|
||||
assert.equal(enforce.if, "always()");
|
||||
const final = spawnSync("bash", ["-euo", "pipefail", "-c", enforce.run!], {
|
||||
cwd: dir,
|
||||
encoding: "utf8",
|
||||
env: { ...env, VALIDATION_EXIT: values.exit ?? "", REPORT_PREFIX: job },
|
||||
});
|
||||
assert.equal(final.error, undefined);
|
||||
return final.status;
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const pass = JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
candidateSha: sha,
|
||||
profile: "full",
|
||||
releaseGreen: true,
|
||||
hardFailures: [],
|
||||
checks: [{ id: "fixture", kind: "hard", ok: true }],
|
||||
});
|
||||
|
||||
for (const job of ["release-green", "main-green"]) {
|
||||
test(`${job}: a failed validator cannot produce a successful workflow`, () => {
|
||||
assert.notEqual(execute(job, 42, ""), 0);
|
||||
});
|
||||
test(`${job}: exit zero with absent or inconsistent evidence fails closed`, () => {
|
||||
for (const report of [
|
||||
"",
|
||||
"{}",
|
||||
"not-json",
|
||||
pass.replace('"releaseGreen":true', '"releaseGreen":false'),
|
||||
pass.replace('"ok":true', '"ok":false'),
|
||||
pass.replace(sha, "2".repeat(40)),
|
||||
pass.replace('"profile":"full"', '"profile":"quick"'),
|
||||
pass.replace('"id":"fixture"', '"id":""'),
|
||||
pass.replace('"checks":[', '"checks":[{"id":"fixture","kind":"hard","ok":true},'),
|
||||
]) {
|
||||
assert.notEqual(execute(job, 0, report), 0, report);
|
||||
}
|
||||
});
|
||||
test(`${job}: a complete consistent PASS retains success`, () => {
|
||||
assert.equal(execute(job, 0, pass), 0);
|
||||
assert.equal(execute(job, 0, pass.replace('"profile":"full"', '"profile":"quick"'), "push"), 0);
|
||||
});
|
||||
test(`${job}: quick runs cannot close a full-gate incident`, () => {
|
||||
const close = workflow.jobs[job].steps.find((s) => s.name?.startsWith("Close tracking issue"))!;
|
||||
assert.match(close.if ?? "", /github\.event_name != 'push'/);
|
||||
assert.match(close.run ?? "", /refs\/heads\//);
|
||||
assert.match(close.run ?? "", /VALIDATED_SHA/);
|
||||
});
|
||||
test(`${job}: only the still-current validated SHA can close an incident`, () => {
|
||||
const close = workflow.jobs[job].steps.find((s) => s.name?.startsWith("Close tracking issue"))!;
|
||||
const dir = mkdtempSync(join(tmpdir(), "omni-green-close-"));
|
||||
try {
|
||||
const calls = join(dir, "gh-calls");
|
||||
for (const current of ["2".repeat(40), sha]) {
|
||||
writeFileSync(calls, "");
|
||||
const result = spawnSync(
|
||||
"bash",
|
||||
[
|
||||
"-euo",
|
||||
"pipefail",
|
||||
"-c",
|
||||
`
|
||||
git() { printf '%s\\trefs/heads/main\\n' "$FIXTURE_CURRENT_SHA"; }
|
||||
gh() {
|
||||
printf '%s\\n' "$*" >> "$FIXTURE_GH_CALLS"
|
||||
if [ "$2" = "list" ]; then printf '123\\n'; fi
|
||||
}
|
||||
${close.run}
|
||||
`,
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
VALIDATED_SHA: sha,
|
||||
FIXTURE_CURRENT_SHA: current,
|
||||
FIXTURE_GH_CALLS: calls,
|
||||
TARGET: "release/v3.8.51",
|
||||
RUN_URL: "https://example.invalid/runs/1",
|
||||
GITHUB_REPOSITORY: "fixture/repo",
|
||||
},
|
||||
}
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const recorded = readFileSync(calls, "utf8");
|
||||
if (current === sha) assert.match(recorded, /issue close 123/);
|
||||
else assert.equal(recorded, "", "stale validation must not mutate issues");
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user