mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
feat(release): let a release lead record an exact-job flake instead of blocking publication (#161515)
* feat(release): accept exact-job recorded flakes in release validation A release lead can classify one failed Normal CI job of a Full Release Validation run as a flake through the trusted classification workflow. The receipt binds the exact job id and attempt, CI child run, FRV parent run and attempt, and Release SHA, and carries a tracking issue or PR plus a reason. Release Decision, the manifest, the publisher's live re-derivation, the step summary, and the GitHub release notes tail treat it as a visible advisory. Required classes and every other child stay blocking. * docs(release): fold recorded flakes into the shared release boundaries * fix(release): scope flake receipt discovery to the CI child run * fix(release): require main lineage for flake receipt producers * test(release): copy the flake classification module into tooling fixtures * fix(release): keep advisory-only release notes verifiable
This commit is contained in:
parent
cc9c535d78
commit
f34c2a21db
34 changed files with 2485 additions and 147 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { validateReleaseManifestAdvisoryJobs } from "./full-release-validation-policy.mjs";
|
||||
import { changelogFormat, findReleaseChangelog } from "./lib/release-changelog.mjs";
|
||||
import {
|
||||
compactReleaseNotes,
|
||||
|
|
@ -36,9 +37,11 @@ type ReleaseNotesTarget = {
|
|||
repository: unknown;
|
||||
regularStableVersion?: unknown;
|
||||
contributionRecordPath?: string;
|
||||
validationManifest?: unknown;
|
||||
};
|
||||
|
||||
const RELEASE_VERIFICATION_HEADING = "### Release verification";
|
||||
const ADVISORY_LINE_PREFIX = "- Advisory job (";
|
||||
const SHIPPED_BASELINE_EXCLUSIONS_PREFIX = "Shipped baseline exclusions:";
|
||||
const MONTH_NAMES = [
|
||||
"January",
|
||||
|
|
@ -80,6 +83,24 @@ function joinBody(notes: string, tail: string | undefined) {
|
|||
return normalizedTail ? `${normalizedNotes}\n\n${normalizedTail}` : normalizedNotes;
|
||||
}
|
||||
|
||||
function verificationWithAdvisories(verification: string, manifest: unknown) {
|
||||
if (manifest === undefined) {
|
||||
return normalizeTail(verification);
|
||||
}
|
||||
const escape = (value: string) => value.replace(/[\\`*_{}[\]()<>!#|]/gu, "\\$&");
|
||||
const lines = validateReleaseManifestAdvisoryJobs(manifest).map((job) => {
|
||||
const detail =
|
||||
job.class === "recorded-flake" ? `; ${escape(job.reason)}; tracking: ${job.trackingUrl}` : "";
|
||||
return `${ADVISORY_LINE_PREFIX}${job.class}): ${escape(job.child)} / ${escape(job.job)} (${job.conclusion}): ${job.url}${detail}`;
|
||||
});
|
||||
const proof = normalizeTail(verification)
|
||||
.split("\n")
|
||||
.filter((line) => !line.startsWith(ADVISORY_LINE_PREFIX))
|
||||
.join("\n")
|
||||
.trimEnd();
|
||||
return lines.length > 0 ? [proof || RELEASE_VERIFICATION_HEADING, ...lines].join("\n") : proof;
|
||||
}
|
||||
|
||||
function extendedStableReleaseNotice({
|
||||
version,
|
||||
repository,
|
||||
|
|
@ -386,6 +407,7 @@ export function renderGithubReleaseNotes({
|
|||
repository,
|
||||
regularStableVersion,
|
||||
contributionRecordPath,
|
||||
validationManifest,
|
||||
verification = "",
|
||||
}: ReleaseNotesTarget & { verification?: string }) {
|
||||
assertString(repository, "repository");
|
||||
|
|
@ -421,10 +443,17 @@ export function renderGithubReleaseNotes({
|
|||
`compacted release notes are still too large for GitHub: ${size.characters} characters, ${size.bytes} bytes`,
|
||||
);
|
||||
}
|
||||
const normalizedVerification = normalizeTail(verification);
|
||||
const normalizedVerification = verificationWithAdvisories(verification, validationManifest);
|
||||
const bodyWithVerification = joinBody(baseBody, normalizedVerification);
|
||||
const verificationIncluded =
|
||||
normalizedVerification !== "" && fitsGithubReleaseBody(bodyWithVerification);
|
||||
if (
|
||||
!verificationIncluded &&
|
||||
validationManifest !== undefined &&
|
||||
validateReleaseManifestAdvisoryJobs(validationManifest).length > 0
|
||||
) {
|
||||
fail("release notes exceed GitHub's body limit with required advisory evidence");
|
||||
}
|
||||
const body = verificationIncluded ? bodyWithVerification : baseBody;
|
||||
return {
|
||||
body,
|
||||
|
|
@ -441,19 +470,12 @@ export function verifyGithubReleaseNotes({
|
|||
}: ReleaseNotesTarget & { body: unknown }) {
|
||||
assertString(body, "release body");
|
||||
const normalizedBody = body.trimEnd();
|
||||
const base = renderGithubReleaseNotes(target);
|
||||
if (normalizedBody === base.body) {
|
||||
return {
|
||||
...base,
|
||||
matches: true,
|
||||
actualSize: githubReleaseBodySize(normalizedBody),
|
||||
};
|
||||
}
|
||||
const base = renderGithubReleaseNotes({ ...target, validationManifest: undefined });
|
||||
const verificationPrefix = `${base.body}\n\n${RELEASE_VERIFICATION_HEADING}`;
|
||||
const verification = normalizedBody.startsWith(verificationPrefix)
|
||||
? normalizedBody.slice(base.body.length + 2)
|
||||
: "";
|
||||
const expected = verification ? renderGithubReleaseNotes({ ...target, verification }) : base;
|
||||
const expected = renderGithubReleaseNotes({ ...target, verification });
|
||||
return {
|
||||
...expected,
|
||||
matches: normalizedBody === expected.body,
|
||||
|
|
@ -467,7 +489,7 @@ function usage() {
|
|||
(--root <repository-path> [--ref <ref>] | --changelog <legacy-file>) \\
|
||||
--tag <tag> --repository <owner/repo> \\
|
||||
[--version <version>] [--regular-stable-version <version>] \\
|
||||
[--verification-file <path>] [--output <path>] \\
|
||||
[--verification-file <path>] [--validation-manifest <path>] [--output <path>] \\
|
||||
[--metadata-output <path>]
|
||||
Verification uses the same target arguments with --verify-body <path>.
|
||||
`;
|
||||
|
|
@ -483,6 +505,7 @@ function parseArgs(argv: string[]) {
|
|||
["--repository", "repository"],
|
||||
["--regular-stable-version", "regularStableVersion"],
|
||||
["--verification-file", "verificationFile"],
|
||||
["--validation-manifest", "validationManifest"],
|
||||
["--output", "output"],
|
||||
["--metadata-output", "metadataOutput"],
|
||||
["--verify-body", "verifyBody"],
|
||||
|
|
@ -558,6 +581,9 @@ function main() {
|
|||
repository,
|
||||
regularStableVersion: options.regularStableVersion,
|
||||
contributionRecordPath: source?.recordPath ?? undefined,
|
||||
validationManifest: options.validationManifest
|
||||
? JSON.parse(readFileSync(options.validationManifest, "utf8"))
|
||||
: undefined,
|
||||
};
|
||||
if (options.verifyBody) {
|
||||
const result = verifyGithubReleaseNotes({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue