ci: reviewed run судится только release-потребителем (#573, ревью r1 M1)

`loadGithubProofContext` спрашивал объявленный `Baseline-Reviewed` run для
любого потребителя, а `evaluateCiProof` судил его при `reviewedRun: null` —
merge и review начинали зависеть от доступности старого run по чужой причине.
Теперь запрос делается только с `withReviewedRun` (release-gate передаёт его
вместе с ожиданиями), а проверка стоит внутри `if (expected)` — рядом со
сверкой evidence, где ей и место. Тест: без ожиданий merge/review green при
reviewedRun undefined/null/пустом; фейковый fetch доказывает, что запроса нет.

Issue: #573
User-Visible: no
This commit is contained in:
Claude
2026-09-17 22:26:19 +03:00
parent da156dd10b
commit 2ea1e5e62a
4 changed files with 86 additions and 20 deletions
+19 -14
View File
@@ -287,12 +287,23 @@ export function evaluateCiProof({
return result('stale', 'run event differs from proof event');
if (policy?.full && !asBool(proof.request?.full)) return result('stale', 'proof is light; full gates were not requested');
if (policy?.mutants && !asBool(proof.request?.mutants)) return result('stale', 'proof has no requested mutant jobs');
// #573: потребитель, у которого есть checkout кандидата, сверяет составное
// evidence, а не верит ему. Proof без блока при наличии ожиданий устарел.
// #573: потребитель, у которого есть checkout кандидата (release), сверяет
// составное evidence, а не верит ему. Proof без блока при наличии ожиданий
// устарел. Объявленный run просмотра кадров проверяется ТОЛЬКО здесь же:
// review и merge ожиданий не передают и лишнего запроса к API не делают
// (ревью r1, M1) — их семантика #541 не меняется.
if (expected) {
if (!proof.evidence) return result('stale', 'proof predates composite evidence (#573)');
const mismatch = evidenceMismatch(proof.evidence, expected);
if (mismatch) return result('failed', `evidence does not match the candidate checkout — ${mismatch}`);
const declared = proof.evidence.baselines?.reviewedRun ?? null;
if (declared) {
const source = reviewedRun?.run;
if (!source || runIdOf(source) !== declared || !/validate\.yml$/.test(String(source.path || source.workflow || 'validate.yml'))
|| source.status !== 'completed' || source.conclusion === 'cancelled') {
return result('failed', `Baseline-Reviewed run ${declared} is missing, cancelled or not a Validate run`);
}
}
}
if (proof.evidence) {
for (const id of REUSE_JOBS) {
@@ -300,14 +311,6 @@ export function evaluateCiProof({
if (claim?.mode === 'reused' && claim.reuse?.key !== proof.evidence.keys?.[id])
return result('failed', `${id}: reused marker key differs from the candidate content key`);
}
const declared = proof.evidence.baselines?.reviewedRun ?? null;
if (declared && reviewedRun !== undefined) {
const source = reviewedRun?.run;
if (!source || runIdOf(source) !== declared || !/validate\.yml$/.test(String(source.path || source.workflow || 'validate.yml'))
|| source.status !== 'completed' || source.conclusion === 'cancelled') {
return result('failed', `Baseline-Reviewed run ${declared} is missing, cancelled or not a Validate run`);
}
}
}
const derived = requiredCheckIds(proof);
if (!sameSet(derived, proof.requiredChecks || []))
@@ -404,7 +407,7 @@ export async function githubCandidateTree({ repo, sha, token, fetchImpl = fetch
return row?.tree?.sha || null;
}
export async function loadGithubProofContext({ repo, run, token, fetchImpl = fetch }) {
export async function loadGithubProofContext({ repo, run, token, fetchImpl = fetch, withReviewedRun = false }) {
const runId = runIdOf(run);
const attempt = runAttemptOf(run);
const name = ciProofArtifactName(runId, attempt);
@@ -438,17 +441,19 @@ export async function loadGithubProofContext({ repo, run, token, fetchImpl = fet
);
reuseRuns.set(sourceKey, { run: sourceRun, jobs: sourceJobs?.jobs || [] });
}
// #573: объявленный человеком run просмотра кадров обязан существовать.
// #573: объявленный человеком run просмотра кадров обязан существовать —
// спрашивает только release-потребитель (`withReviewedRun`); review и merge
// этот запрос не делают и от доступности старого run не зависят.
let reviewedRun;
const declared = proof?.evidence?.baselines?.reviewedRun;
if (declared) {
if (withReviewedRun && declared) {
try {
reviewedRun = { run: await githubJson(`https://api.github.com/repos/${repo}/actions/runs/${declared}`, token, fetchImpl) };
} catch {
reviewedRun = null;
}
}
return { proof, jobs, reuseRuns, reviewedRun };
return { proof, jobs, reuseRuns, ...(reviewedRun !== undefined ? { reviewedRun } : {}) };
}
if (isMainModule(import.meta.url)) {
+2 -1
View File
@@ -55,7 +55,8 @@ const newestFirst = (runs) => [...(Array.isArray(runs) ? runs : [])].sort((a, b)
/** #541: proof-aware verdict shared with review and merge. */
export async function classifyValidateProofs({
runs, repo, sha, tree, token, fetchImpl = fetch, expected = null,
loadContext = (run) => loadGithubProofContext({ repo, run, token, fetchImpl }),
// #573: reviewed run спрашивается у GitHub только вместе с ожиданиями (release)
loadContext = (run) => loadGithubProofContext({ repo, run, token, fetchImpl, withReviewedRun: Boolean(expected) }),
}) {
const evaluations = [];
for (const run of newestFirst(runs)) {