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", () => {