diff --git a/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs b/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs
index fa1796cf8489..b980b5ba796e 100644
--- a/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs
+++ b/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs
@@ -1357,6 +1357,7 @@ export function ledgerFor(
priorRecord,
sourcePullRequests,
sourceReferences,
+ noteReferences,
legacyIssuePullRequests,
revertedReferences,
shippedBaselines,
@@ -1381,6 +1382,7 @@ export function ledgerFor(
const recordedPullRequests = new Set([
...sourcePullRequests,
...sourceReferences,
+ ...noteReferences,
...legacyIssuePullRequests,
...priorRecord.pullRequests.keys(),
]);
@@ -1853,6 +1855,7 @@ function main() {
priorRecord,
source.pullRequests,
source.references,
+ noteReferences,
legacyIssuePullRequests,
source.revertedReferences,
source.shippedBaselines,
diff --git a/.github/workflows/openclaw-release-publish.yml b/.github/workflows/openclaw-release-publish.yml
index 9efa3ef2a740..a8df3bf31584 100644
--- a/.github/workflows/openclaw-release-publish.yml
+++ b/.github/workflows/openclaw-release-publish.yml
@@ -898,6 +898,16 @@ jobs:
return 0
fi
+ # The renderer omits the verification tail when the canonical body
+ # already reaches GitHub's limit. A canonical proofless body with
+ # intact dependency evidence is retry-safe: postpublish re-attempts
+ # the proof append on this run.
+ if [[ "${has_asset}" == "true" &&
+ "${has_canonical_body}" == "true" &&
+ "${has_proof}" != "true" ]]; then
+ return 0
+ fi
+
{
echo "Release ${RELEASE_TAG} already has a public GitHub release page without complete postpublish evidence for ${TARGET_SHA}."
echo "Refusing to reuse a public prerelease tag after publication started: ${release_url}"
diff --git a/scripts/release-candidate-checklist.mjs b/scripts/release-candidate-checklist.mjs
index 74a5a2352641..6953f092815a 100644
--- a/scripts/release-candidate-checklist.mjs
+++ b/scripts/release-candidate-checklist.mjs
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
import { stripLeadingPackageManagerSeparator } from "./lib/arg-utils.mjs";
import { readBoundedResponseText } from "./lib/bounded-response.mjs";
import {
+ correctionVersionForTag,
extractChangelogReleaseSections,
extractChangelogSection,
formatShippedBaselineExclusions,
@@ -488,16 +489,30 @@ export function validateCandidateChangelogProvenance({
isAncestor = gitIsAncestor,
loadShippedBaseline = loadCandidateShippedBaseline,
}) {
+ // Validate the same section the renderer publishes: correction tags may
+ // carry their own heading, and alpha tags may fall back to Unreleased.
let section;
+ let sectionVersion = version;
let usesAlphaUnreleasedFallback = false;
- try {
- section = extractChangelogSection(changelog, version);
- } catch (error) {
- if (!/-alpha\.[1-9][0-9]*$/u.test(tag)) {
- throw error;
+ const correctionVersion = correctionVersionForTag(tag);
+ if (correctionVersion && correctionVersion !== version) {
+ try {
+ section = extractChangelogSection(changelog, correctionVersion);
+ sectionVersion = correctionVersion;
+ } catch {
+ // The correction has no dedicated section; validate the base section.
+ }
+ }
+ if (section === undefined) {
+ try {
+ section = extractChangelogSection(changelog, version);
+ } catch (error) {
+ if (!/-alpha\.[1-9][0-9]*$/u.test(tag)) {
+ throw error;
+ }
+ section = releaseNotesSectionForTag(changelog, version, tag);
+ usesAlphaUnreleasedFallback = true;
}
- section = releaseNotesSectionForTag(changelog, version, tag);
- usesAlphaUnreleasedFallback = true;
}
const recordStart = section.search(/\n### Complete contribution record\r?$/m);
if (recordStart < 0) {
@@ -508,12 +523,14 @@ export function validateCandidateChangelogProvenance({
shippedBaselines: [],
};
}
- throw new Error(`CHANGELOG.md ## ${version} is missing ### Complete contribution record`);
+ throw new Error(
+ `CHANGELOG.md ## ${sectionVersion} is missing ### Complete contribution record`,
+ );
}
const record = section.slice(recordStart);
const recordedPullRequests = candidateContributionRecordPullRequests(
section,
- `CHANGELOG.md ## ${version}`,
+ `CHANGELOG.md ## ${sectionVersion}`,
);
const provenance = record.match(
/^This audited record covers the complete (?\S+)\.\.(?[0-9a-f]{40}) history:/mu,
@@ -522,7 +539,7 @@ export function validateCandidateChangelogProvenance({
const recordedTarget = provenance?.groups?.target;
if (!base || !recordedTarget) {
throw new Error(
- `CHANGELOG.md ## ${version} is missing exact complete contribution record provenance`,
+ `CHANGELOG.md ## ${sectionVersion} is missing exact complete contribution record provenance`,
);
}
const shippedBaselines = parseShippedBaselineExclusions(record);
diff --git a/scripts/render-github-release-notes.mjs b/scripts/render-github-release-notes.mjs
index 5d502c8f862a..bc8911e45fec 100644
--- a/scripts/render-github-release-notes.mjs
+++ b/scripts/render-github-release-notes.mjs
@@ -230,15 +230,22 @@ function compactReleaseNotes(section, repository, tag) {
].join("\n");
}
-export function releaseNotesSectionForTag(changelog, version, tag) {
- // Numeric-correction tags (vX-N) prefer their own exact heading when the
- // changelog carries one; otherwise they fall back to the base version.
+export function correctionVersionForTag(tag) {
+ // Numeric-correction tags (vX-N) may carry their own changelog heading;
+ // alpha/beta prerelease tags never do.
const taggedVersion = tag.replace(/^v/u, "");
const isCorrection =
/-[1-9][0-9]*$/u.test(taggedVersion) && !/-(?:alpha|beta)\.[1-9][0-9]*$/u.test(taggedVersion);
- if (isCorrection && taggedVersion !== version) {
+ return isCorrection ? taggedVersion : undefined;
+}
+
+export function releaseNotesSectionForTag(changelog, version, tag) {
+ // Correction tags prefer their own exact heading when the changelog carries
+ // one; otherwise they fall back to the base version.
+ const correctionVersion = correctionVersionForTag(tag);
+ if (correctionVersion && correctionVersion !== version) {
try {
- return extractChangelogSection(changelog, taggedVersion);
+ return extractChangelogSection(changelog, correctionVersion);
} catch {
// The correction has no dedicated section; use the base version below.
}
diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts
index 20ea85648b33..6aba3b9dda32 100644
--- a/test/scripts/package-acceptance-workflow.test.ts
+++ b/test/scripts/package-acceptance-workflow.test.ts
@@ -1,5 +1,5 @@
// Package Acceptance Workflow tests cover package acceptance workflow script behavior.
-import { spawnSync } from "node:child_process";
+import { execFileSync, spawnSync } from "node:child_process";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
@@ -2639,9 +2639,12 @@ describe("package artifact reuse", () => {
expect(clawHubMetadataIndex).toBeGreaterThan(clawHubSetupIndex);
expect(releaseWorkflow).toContain("Plugin npm run ID");
expect(releaseWorkflow).toContain("Plugin ClawHub run ID");
- expect(releaseWorkflow).toContain(
+ expect(releaseWorkflow).not.toContain(
"did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs",
);
+ expect(releaseWorkflow).not.toContain("return_run_details: true");
+ expect(releaseWorkflow).toContain("'.workflow_run_id'");
+ expect(releaseWorkflow).toContain("'.html_url'");
expect(releaseWorkflow).not.toContain("BEFORE_IDS=");
expect(releaseWorkflow).not.toContain("before_json");
expect(releaseWorkflow).toContain("plugin-clawhub-new.yml");
diff --git a/test/scripts/release-notes-ledger.test.ts b/test/scripts/release-notes-ledger.test.ts
index 8f3194fbbef4..c855a7c2a67d 100644
--- a/test/scripts/release-notes-ledger.test.ts
+++ b/test/scripts/release-notes-ledger.test.ts
@@ -113,6 +113,7 @@ describe("renderContributionRecordEntry", () => {
new Set(),
new Set(),
new Set(),
+ [],
Date.parse("2026-07-09T00:00:00Z"),
);
@@ -146,6 +147,12 @@ describe("renderContributionRecordEntry", () => {
"",
"### Highlights",
"",
+ "- Highlight one.",
+ "- Highlight two.",
+ "- Highlight three.",
+ "- Highlight four.",
+ "- Highlight five.",
+ "",
"### Changes",
"",
"### Fixes",
@@ -181,6 +188,12 @@ describe("renderContributionRecordEntry", () => {
"",
"### Highlights",
"",
+ "- Highlight one.",
+ "- Highlight two.",
+ "- Highlight three.",
+ "- Highlight four.",
+ "- Highlight five.",
+ "",
"### Changes",
"",
"### Fixes",
diff --git a/test/scripts/verify-release-notes.test.ts b/test/scripts/verify-release-notes.test.ts
index e29bf23c5efd..65aa5f54b9b0 100644
--- a/test/scripts/verify-release-notes.test.ts
+++ b/test/scripts/verify-release-notes.test.ts
@@ -143,8 +143,12 @@ describe("release-note verification", () => {
const filtered = withoutExcludedContributionRecords(record, new Set([2, 10]));
- expect([...filtered.pullRequests]).toEqual([[1, { references: [], thanks: [] }]]);
- expect([...filtered.legacyIssues]).toEqual([[11, { references: [], thanks: [] }]]);
+ expect([...filtered.pullRequests]).toEqual([
+ [1, { externalReferences: [], references: [], thanks: [] }],
+ ]);
+ expect([...filtered.legacyIssues]).toEqual([
+ [11, { externalReferences: [], references: [], thanks: [] }],
+ ]);
});
it("does not treat the shipped baseline inventory as current release-note references", () => {