mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(release): accept explicit provenance mappings (#120842)
This commit is contained in:
@@ -59,6 +59,10 @@ every human `Thanks @...` attribution.
|
||||
--write-ledger
|
||||
```
|
||||
|
||||
Add repeatable `--release-provenance '<40sha> -> #PR[, #PR]'` inputs when
|
||||
release commits cannot carry provenance metadata. These use the same exact
|
||||
marker grammar and current-main validation as commit-body markers.
|
||||
|
||||
The verifier automatically reuses public GitHub GraphQL responses from an
|
||||
exact base/target SHA snapshot under the worktree's git metadata. Iterative
|
||||
rewrites at the same target avoid repeated network discovery. Use
|
||||
|
||||
@@ -96,6 +96,8 @@ Options:
|
||||
--main-ref <ref> Canonical mainline used to replace backport PRs.
|
||||
--seed-ref <ref> Use an existing release section as editorial input.
|
||||
--shipped-ref <tag> Exclude PRs already recorded by this shipped tag; repeatable.
|
||||
--release-provenance <sha -> #PR[, #PR]>
|
||||
Supply an exact provenance marker; repeatable.
|
||||
--write-ledger Write the verified ledger back into CHANGELOG.md.
|
||||
--release-tag <tag> GitHub release tag to compare; repeatable with --check-github.
|
||||
--check-github Require each supplied GitHub release body to match.
|
||||
@@ -103,9 +105,10 @@ Options:
|
||||
--help Show this help text.`);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
export function parseArgs(argv) {
|
||||
const options = {
|
||||
releaseTags: [],
|
||||
releaseProvenance: [],
|
||||
checkGithub: false,
|
||||
help: false,
|
||||
json: false,
|
||||
@@ -150,6 +153,7 @@ function parseArgs(argv) {
|
||||
arg === "--target" ||
|
||||
arg === "--version" ||
|
||||
arg === "--release-tag" ||
|
||||
arg === "--release-provenance" ||
|
||||
arg === "--shipped-ref" ||
|
||||
arg === "--github-snapshot" ||
|
||||
arg === "--main-ref" ||
|
||||
@@ -162,6 +166,8 @@ function parseArgs(argv) {
|
||||
}
|
||||
if (arg === "--release-tag") {
|
||||
options.releaseTags.push(value);
|
||||
} else if (arg === "--release-provenance") {
|
||||
options.releaseProvenance.push(value);
|
||||
} else if (arg === "--shipped-ref") {
|
||||
options.shippedRefs.push(value);
|
||||
} else if (arg === "--manifest") {
|
||||
@@ -851,39 +857,48 @@ function backportPullRequestOrigins(message) {
|
||||
].map((match) => Number(match[1]));
|
||||
}
|
||||
|
||||
export function releaseProvenanceMarkers(message) {
|
||||
const markers = [];
|
||||
for (const line of message.split("\n")) {
|
||||
if (!/^Release provenance:/i.test(line)) {
|
||||
continue;
|
||||
}
|
||||
const match = line.match(/^Release provenance: ([0-9a-f]{40}) -> (#\d+(?:,\s*#\d+)*)\.?\s*$/i);
|
||||
if (!match) {
|
||||
fail(`invalid release provenance marker: ${line}`);
|
||||
}
|
||||
markers.push({
|
||||
commit: match[1].toLowerCase(),
|
||||
pullRequests: [...match[2].matchAll(/#(\d+)/g)].map((reference) => Number(reference[1])),
|
||||
});
|
||||
function releaseProvenanceMarker(line) {
|
||||
const match = line.match(/^Release provenance: ([0-9a-f]{40}) -> (#\d+(?:,\s*#\d+)*)\.?\s*$/i);
|
||||
if (!match) {
|
||||
fail(`invalid release provenance marker: ${line}`);
|
||||
}
|
||||
return markers;
|
||||
return {
|
||||
commit: match[1].toLowerCase(),
|
||||
pullRequests: [...match[2].matchAll(/#(\d+)/g)].map((reference) => Number(reference[1])),
|
||||
};
|
||||
}
|
||||
|
||||
export function collectReleaseProvenanceOverrides(activeCommits) {
|
||||
export function releaseProvenanceMarkers(message) {
|
||||
return message
|
||||
.split("\n")
|
||||
.filter((line) => /^Release provenance:/i.test(line))
|
||||
.map(releaseProvenanceMarker);
|
||||
}
|
||||
|
||||
export function collectReleaseProvenanceOverrides(activeCommits, releaseProvenance = []) {
|
||||
const activeCommitHashes = new Set(activeCommits.map((commit) => commit.hash));
|
||||
const overrides = new Map();
|
||||
const addMarker = (marker) => {
|
||||
if (!activeCommitHashes.has(marker.commit)) {
|
||||
fail(`release provenance marker targets commit outside the active range: ${marker.commit}`);
|
||||
}
|
||||
const existing = overrides.get(marker.commit);
|
||||
if (existing && existing.join(",") !== marker.pullRequests.join(",")) {
|
||||
fail(`conflicting release provenance markers for ${marker.commit}`);
|
||||
}
|
||||
overrides.set(marker.commit, marker.pullRequests);
|
||||
};
|
||||
for (const commit of activeCommits) {
|
||||
for (const marker of releaseProvenanceMarkers(commit.body)) {
|
||||
if (!activeCommitHashes.has(marker.commit)) {
|
||||
fail(`release provenance marker targets commit outside the active range: ${marker.commit}`);
|
||||
}
|
||||
const existing = overrides.get(marker.commit);
|
||||
if (existing && existing.join(",") !== marker.pullRequests.join(",")) {
|
||||
fail(`conflicting release provenance markers for ${marker.commit}`);
|
||||
}
|
||||
overrides.set(marker.commit, marker.pullRequests);
|
||||
addMarker(marker);
|
||||
}
|
||||
}
|
||||
for (const value of releaseProvenance) {
|
||||
if (/[\r\n]/u.test(value)) {
|
||||
fail(`invalid release provenance marker: Release provenance: ${value}`);
|
||||
}
|
||||
addMarker(releaseProvenanceMarker(`Release provenance: ${value}`));
|
||||
}
|
||||
return overrides;
|
||||
}
|
||||
|
||||
@@ -1017,7 +1032,7 @@ function canonicalMainCommits(base, mainRef) {
|
||||
return commits;
|
||||
}
|
||||
|
||||
function sourceCommits(base, target, mainRef) {
|
||||
function sourceCommits(base, target, mainRef, releaseProvenance = []) {
|
||||
const targetCommit = git(["rev-parse", `${target}^{commit}`]);
|
||||
if (!gitIsAncestor(base, targetCommit)) {
|
||||
fail(`release range base ${base} must be an ancestor of target ${target}`);
|
||||
@@ -1193,7 +1208,7 @@ function sourceCommits(base, target, mainRef) {
|
||||
activeCommits.map((commit) => commit.hash),
|
||||
targetTimestamp,
|
||||
);
|
||||
const provenanceOverrides = collectReleaseProvenanceOverrides(activeCommits);
|
||||
const provenanceOverrides = collectReleaseProvenanceOverrides(activeCommits, releaseProvenance);
|
||||
const mainCommits = canonicalMainCommits(base, mainRef);
|
||||
const mainCommit = provenanceOverrides.size > 0 ? gitCommit(mainRef, true) : undefined;
|
||||
const mainCommitsByHash = new Map(mainCommits.map((commit) => [commit.hash, commit]));
|
||||
@@ -2412,7 +2427,12 @@ function main() {
|
||||
githubSnapshotState = initializeGithubSnapshot(options);
|
||||
const changelog = readFileSync("CHANGELOG.md", "utf8");
|
||||
const section = sectionFor(changelog, options.version);
|
||||
const source = sourceCommits(options.base, options.target, options.mainRef ?? "origin/main");
|
||||
const source = sourceCommits(
|
||||
options.base,
|
||||
options.target,
|
||||
options.mainRef ?? "origin/main",
|
||||
options.releaseProvenance,
|
||||
);
|
||||
const committedSection = optionalSectionFor(
|
||||
git(["show", `${source.target}:CHANGELOG.md`]),
|
||||
options.version,
|
||||
|
||||
Vendored
+5
@@ -143,7 +143,12 @@ declare module "*openclaw-changelog-update/scripts/verify-release-notes.mjs" {
|
||||
): Array<{ commit: string; pullRequests: number[] }>;
|
||||
export function collectReleaseProvenanceOverrides(
|
||||
activeCommits: Array<{ body: string; hash: string }>,
|
||||
releaseProvenance?: string[],
|
||||
): Map<string, number[]>;
|
||||
export function parseArgs(argv: string[]): {
|
||||
releaseProvenance: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
export function resolvedReleasePullRequests(
|
||||
currentPullRequests: number[],
|
||||
mainPullRequests: number[],
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
githubApiWithSnapshot,
|
||||
highlightCountError,
|
||||
isEligibleHandle,
|
||||
parseArgs,
|
||||
persistGithubSnapshot,
|
||||
pullRequestTitleFromCommitSubject,
|
||||
releaseNoteReferences,
|
||||
@@ -275,6 +276,74 @@ describe("release-note verification", () => {
|
||||
).toThrow(`conflicting release provenance markers for ${releaseCommit}`);
|
||||
});
|
||||
|
||||
it("accepts repeatable CLI provenance without metadata commits", () => {
|
||||
const mappings: Array<[string, number]> = [
|
||||
["bdde3d1c6dd7cc415588a72cf27ebe27f83bfe47", 120085],
|
||||
["5090cae6d0cc3f2ec272b2448970c6238f525610", 120479],
|
||||
["8ff1724067c1dcfb9a63574e6f3771261033ffae", 120479],
|
||||
["0cd3075adf7cd201e17d25c95cbe190991f8aab1", 120538],
|
||||
];
|
||||
const payloads = mappings.map(([commit, pullRequest]) => `${commit} -> #${pullRequest}`);
|
||||
const options = parseArgs([
|
||||
"--base",
|
||||
"base",
|
||||
"--target",
|
||||
"target",
|
||||
"--version",
|
||||
"2026.8.1",
|
||||
...payloads.flatMap((payload) => ["--release-provenance", payload]),
|
||||
]);
|
||||
|
||||
expect(options.releaseProvenance).toEqual(payloads);
|
||||
expect(
|
||||
collectReleaseProvenanceOverrides(
|
||||
mappings.map(([hash]) => ({ body: "", hash })),
|
||||
options.releaseProvenance,
|
||||
),
|
||||
).toEqual(new Map(mappings.map(([commit, pullRequest]) => [commit, [pullRequest]])));
|
||||
});
|
||||
|
||||
it("validates CLI provenance through the exact marker merge path", () => {
|
||||
const activeCommit = "a".repeat(40);
|
||||
|
||||
expect(() =>
|
||||
collectReleaseProvenanceOverrides([{ body: "", hash: activeCommit }], ["short -> #1"]),
|
||||
).toThrow("invalid release provenance marker");
|
||||
expect(() =>
|
||||
collectReleaseProvenanceOverrides(
|
||||
[{ body: "", hash: activeCommit }],
|
||||
[`${activeCommit} -> #1 trailing`],
|
||||
),
|
||||
).toThrow("invalid release provenance marker");
|
||||
expect(() =>
|
||||
collectReleaseProvenanceOverrides(
|
||||
[{ body: "", hash: activeCommit }],
|
||||
[`${activeCommit} -> #1\nRelease provenance: ${activeCommit} -> #2`],
|
||||
),
|
||||
).toThrow("invalid release provenance marker");
|
||||
for (const payload of [
|
||||
`${activeCommit} -> #1\n`,
|
||||
`${activeCommit} -> #1,\n#2`,
|
||||
`${activeCommit} -> #1\r\n`,
|
||||
]) {
|
||||
expect(() =>
|
||||
collectReleaseProvenanceOverrides([{ body: "", hash: activeCommit }], [payload]),
|
||||
).toThrow("invalid release provenance marker");
|
||||
}
|
||||
expect(() =>
|
||||
collectReleaseProvenanceOverrides(
|
||||
[{ body: "", hash: activeCommit }],
|
||||
[`${"b".repeat(40)} -> #1`],
|
||||
),
|
||||
).toThrow("release provenance marker targets commit outside the active range");
|
||||
expect(() =>
|
||||
collectReleaseProvenanceOverrides(
|
||||
[{ body: `Release provenance: ${activeCommit} -> #1`, hash: activeCommit }],
|
||||
[`${activeCommit} -> #2`],
|
||||
),
|
||||
).toThrow(`conflicting release provenance markers for ${activeCommit}`);
|
||||
});
|
||||
|
||||
it("requires release provenance PRs to be merged into current main", () => {
|
||||
const releaseCommit = "a".repeat(40);
|
||||
const mainCommit = "b".repeat(40);
|
||||
|
||||
Reference in New Issue
Block a user