mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
ci: release gate judges the latest non-cancelled Validate run of the SHA
The gate used to require every Validate run on the tag SHA to be green: a cancelled duplicate or a red flake that a later re-run had fixed kept the stable release blocked (v1.73.0, 09.09 — released by hand). Now the verdict comes from the newest run that was not cancelled: not completed → wait, success → pass, anything else → fail, no run → wait. The same rule is documented for the perf workflow and the release runbook. Mutants: release-gate-counts-cancelled-runs, release-gate-oldest-run-wins. Issue: #511 User-Visible: no
This commit is contained in:
@@ -8087,6 +8087,28 @@ const MUTANT_DEFINITIONS = [
|
||||
+ ' # The store is the durable authority (#335): a drop that\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'release-gate-counts-cancelled-runs',
|
||||
guard: 'node --test test/release-gate.test.mjs',
|
||||
because: 'a cancelled twin on the tag SHA proves nothing and must not block the assets; the '
|
||||
+ 'verdict is the latest non-cancelled run (#511 AC1)',
|
||||
patches: [{
|
||||
file: 'scripts/release-gate.mjs',
|
||||
find: " const relevant = (Array.isArray(runs) ? runs : []).filter((run) => run && run.conclusion !== 'cancelled');",
|
||||
replace: " const relevant = (Array.isArray(runs) ? runs : []).filter((run) => !!run); // mutant: cancelled counts",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'release-gate-oldest-run-wins',
|
||||
guard: 'node --test test/release-gate.test.mjs',
|
||||
because: 'the latest run is the verdict: a re-run or another-baseline comparison must be able to '
|
||||
+ 'refresh an older result on the same SHA (#511 AC1)',
|
||||
patches: [{
|
||||
file: 'scripts/release-gate.mjs',
|
||||
find: " return relevant.sort((a, b) => stamp(b) - stamp(a) || Number(b.id || 0) - Number(a.id || 0))[0] || null;",
|
||||
replace: " return relevant.sort((a, b) => stamp(a) - stamp(b) || Number(a.id || 0) - Number(b.id || 0))[0] || null; // mutant: oldest",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'summary-runtime-attaches-after-first-render',
|
||||
guard: 'node demo/smoke_summary_warm_attach.mjs',
|
||||
|
||||
@@ -4,11 +4,26 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/**
|
||||
* The verdict is the LATEST run that was not cancelled (#511). A cancelled run
|
||||
* proves nothing either way — concurrency or a hand superseded it — and the
|
||||
* old "every run must be green" rule turned one red or cancelled duplicate on
|
||||
* a SHA into a permanent block (v1.73.0: a cancelled dispatch twin plus a red
|
||||
* comparison next to a green push run left the tag without assets). A later
|
||||
* re-run or a dispatch with another baseline may therefore refresh the verdict
|
||||
* on the same SHA; the owner accepted that trade-off deliberately.
|
||||
*/
|
||||
export function latestRelevantRun(runs) {
|
||||
const relevant = (Array.isArray(runs) ? runs : []).filter((run) => run && run.conclusion !== 'cancelled');
|
||||
const stamp = (run) => Date.parse(run.run_started_at || run.created_at || 0) || 0;
|
||||
return relevant.sort((a, b) => stamp(b) - stamp(a) || Number(b.id || 0) - Number(a.id || 0))[0] || null;
|
||||
}
|
||||
|
||||
export function classifyValidateRuns(runs) {
|
||||
if (!Array.isArray(runs) || runs.length === 0) return 'wait';
|
||||
if (runs.some((run) => run?.status === 'completed' && run?.conclusion !== 'success')) return 'fail';
|
||||
if (runs.some((run) => run?.status !== 'completed')) return 'wait';
|
||||
return 'success';
|
||||
const latest = latestRelevantRun(runs);
|
||||
if (!latest) return 'wait';
|
||||
if (latest.status !== 'completed') return 'wait';
|
||||
return latest.conclusion === 'success' ? 'success' : 'fail';
|
||||
}
|
||||
|
||||
export const workflowRunsUrl = ({ repo, workflow, sha }) => (
|
||||
@@ -37,14 +52,14 @@ export async function waitForGreenWorkflow({
|
||||
const body = await response.json();
|
||||
const runs = Array.isArray(body?.workflow_runs) ? body.workflow_runs : [];
|
||||
const state = classifyValidateRuns(runs);
|
||||
const latest = latestRelevantRun(runs);
|
||||
if (state === 'fail') {
|
||||
const failed = runs.filter((run) => run?.status === 'completed' && run?.conclusion !== 'success');
|
||||
throw new Error(`${label} is not green for ${sha}: ${JSON.stringify(failed.map((run) => ({
|
||||
conclusion: run.conclusion, url: run.html_url,
|
||||
})))}`);
|
||||
throw new Error(`${label} is not green for ${sha}: latest run ${JSON.stringify({
|
||||
conclusion: latest.conclusion, url: latest.html_url,
|
||||
})}`);
|
||||
}
|
||||
if (state === 'success') {
|
||||
console.log(`${label} is green for ${sha} (${runs.length} run(s))`);
|
||||
console.log(`${label} is green for ${sha}: latest run ${latest.html_url || latest.id} (${runs.length} run(s) on the SHA)`);
|
||||
return;
|
||||
}
|
||||
if (Date.now() >= deadline) throw new Error(`No completed green ${label} for ${sha} within the deadline`);
|
||||
|
||||
Reference in New Issue
Block a user