mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
ci: unify Validate proof across gates (#541)
Issue: #541 User-Visible: no
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
// performance workflow.
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
CI_PROOF_POLICIES, evaluateCiProof, githubCandidateTree,
|
||||
loadGithubProofContext, selectCiProofVerdict,
|
||||
} from './ci-proof.mjs';
|
||||
|
||||
/**
|
||||
* The verdict is the LATEST run that was not cancelled (#511). A cancelled run
|
||||
@@ -26,6 +30,39 @@ export function classifyValidateRuns(runs) {
|
||||
return latest.conclusion === 'success' ? 'success' : 'fail';
|
||||
}
|
||||
|
||||
const newestFirst = (runs) => [...(Array.isArray(runs) ? runs : [])].sort((a, b) => {
|
||||
const stamp = (run) => Date.parse(run?.run_started_at || run?.startedAt || run?.created_at || run?.createdAt || 0) || 0;
|
||||
return stamp(b) - stamp(a) || Number(b?.id || b?.databaseId || 0) - Number(a?.id || a?.databaseId || 0);
|
||||
});
|
||||
|
||||
/** #541: proof-aware verdict shared with review and merge. */
|
||||
export async function classifyValidateProofs({
|
||||
runs, repo, sha, tree, token, fetchImpl = fetch,
|
||||
loadContext = (run) => loadGithubProofContext({ repo, run, token, fetchImpl }),
|
||||
}) {
|
||||
const evaluations = [];
|
||||
for (const run of newestFirst(runs)) {
|
||||
if (run?.status !== 'completed' || run?.conclusion === 'cancelled') {
|
||||
evaluations.push(evaluateCiProof({ run, candidate: { sha, tree }, policy: CI_PROOF_POLICIES.release }));
|
||||
} else {
|
||||
try {
|
||||
const context = await loadContext(run);
|
||||
evaluations.push(evaluateCiProof({
|
||||
run, ...context, candidate: { sha, tree }, policy: CI_PROOF_POLICIES.release,
|
||||
}));
|
||||
} catch (error) {
|
||||
evaluations.push({
|
||||
status: 'missing', url: run.html_url || run.url || null,
|
||||
note: `proof could not be loaded: ${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const current = evaluations.at(-1);
|
||||
if (current.status !== 'cancelled' && current.status !== 'stale') break;
|
||||
}
|
||||
return selectCiProofVerdict(evaluations);
|
||||
}
|
||||
|
||||
export const workflowRunsUrl = ({ repo, workflow, sha }) => (
|
||||
`https://api.github.com/repos/${repo}/actions/workflows/${encodeURIComponent(workflow)}`
|
||||
+ `/runs?head_sha=${encodeURIComponent(sha)}&per_page=100`
|
||||
@@ -39,6 +76,8 @@ export async function waitForGreenWorkflow({
|
||||
if (!repo || !sha || !token || !workflow) throw new Error('repo, sha, token and workflow are required');
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const url = workflowRunsUrl({ repo, workflow, sha });
|
||||
const proofRequired = workflow === 'validate.yml';
|
||||
const tree = proofRequired ? await githubCandidateTree({ repo, sha, token }) : null;
|
||||
while (true) {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
@@ -51,21 +90,22 @@ export async function waitForGreenWorkflow({
|
||||
if (!response.ok) throw new Error(`GitHub Actions API ${response.status}: ${await response.text()}`);
|
||||
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') {
|
||||
throw new Error(`${label} is not green for ${sha}: latest run ${JSON.stringify({
|
||||
conclusion: latest.conclusion, url: latest.html_url,
|
||||
})}`);
|
||||
const verdict = proofRequired
|
||||
? await classifyValidateProofs({ runs, repo, sha, tree, token })
|
||||
: { status: classifyValidateRuns(runs) === 'success' ? 'green'
|
||||
: classifyValidateRuns(runs) === 'fail' ? 'failed' : 'pending', url: latest?.html_url, note: '' };
|
||||
if (verdict.status === 'failed') {
|
||||
throw new Error(`${label} is not green for ${sha}: ${verdict.note}${verdict.url ? ` (${verdict.url})` : ''}`);
|
||||
}
|
||||
if (state === 'success') {
|
||||
console.log(`${label} is green for ${sha}: latest run ${latest.html_url || latest.id} (${runs.length} run(s) on the SHA)`);
|
||||
if (verdict.status === 'green') {
|
||||
console.log(`${label} proof is green for ${sha}: ${verdict.url || 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`);
|
||||
if (Date.now() >= deadline) throw new Error(`No complete ${label} proof for ${sha} within the deadline: ${verdict.status} (${verdict.note})`);
|
||||
const running = runs.filter((run) => run?.status !== 'completed').length;
|
||||
console.log(runs.length
|
||||
? `waiting: ${running} ${label} run(s) still going`
|
||||
? `waiting: ${label} proof is ${verdict.status}; ${running} run(s) still going (${verdict.note})`
|
||||
: `waiting: no ${label} run for ${sha} yet`);
|
||||
await sleep(30_000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user