From 14cbaaad9c52c3f10a1ee18ff3e82b09608cd872 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:32:10 +0100 Subject: [PATCH] fix(release): accept cumulative prerelease changelog bases The release train explicitly compares the full beta range with the previous stable. Accept that supported base as well as the default previous numbered prerelease, while rejecting missing, ambiguous and unrelated comparison bases. Contract-Neutral: Align release packet checks with the existing metadata contract. No public install or runtime contract delta. --- .../installtests/build_release_assets_test.go | 56 ++++++++++++++++--- .../installtests/install_docker_sh_test.go | 4 +- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 78f73b83c4..5771f29b5b 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -1301,11 +1301,6 @@ func TestCurrentPrereleasePacketTracksInstallMetadata(t *testing.T) { if _, _, ok := strings.Cut(version, "-"); !ok { t.Fatalf("current prerelease %q has no stable target", version) } - comparisonVersion, ok := previousPrereleaseVersion(version) - if !ok { - comparisonVersion = previous - } - releaseNotesPath := repoFile("docs", "releases", "RELEASE_NOTES_v"+version+".md") changelogPath := repoFile("docs", "releases", "V6_CHANGELOG_v"+version+".md") @@ -1319,16 +1314,23 @@ func TestCurrentPrereleasePacketTracksInstallMetadata(t *testing.T) { "The rollback target is stable `v"+previous+"`", ) assertFileDoesNotContain(t, releaseNotesPath, "## Fixes") - comparisonSummary := "This changelog describes the changes since `v" + comparisonVersion + "`" if version == "6.4.0-rc.10" { - comparisonSummary = "The `v6.4.0-rc.9` release staged an immutable draft, tag, and exact-version artifacts but did not activate publicly." + assertFileContainsAllNormalized(t, changelogPath, + "The `v6.4.0-rc.9` release staged an immutable draft, tag, and exact-version artifacts but did not activate publicly.") + } else { + content, err := os.ReadFile(changelogPath) + if err != nil { + t.Fatalf("read %s: %v", changelogPath, err) + } + if !prereleaseChangelogHasSupportedBase(string(content), version, previous) { + t.Fatalf("%s must describe changes since the previous numbered prerelease or stable v%s", changelogPath, previous) + } } assertFileContainsAllNormalized(t, changelogPath, "Version: `v"+version+"`", "Previous stable: `v"+previous+"`", "Rollback target: `v"+previous+"`", "Promotion path: ", - comparisonSummary, "Windows signing decision: prereleases publish checksum- and detached-signature-verified Windows agents without Authenticode", "Mobile decision: ", "no companion mobile build or store rollout is required", @@ -4952,6 +4954,44 @@ func TestCommittedReleaseVisualSidecarsCarrySelectionEvidence(t *testing.T) { } } +// Metadata defaults to the previous numbered prerelease. The release train can +// explicitly select the previous stable for a cumulative changelog instead. +func prereleaseChangelogHasSupportedBase(content, version, previousStable string) bool { + matches := regexp.MustCompile("This changelog describes the changes since `v([^`]+)`").FindAllStringSubmatch(normalizedInstallTestWhitespace(content), -1) + if len(matches) != 1 { + return false + } + base := matches[0][1] + if base == previousStable { + return true + } + previous, ok := previousPrereleaseVersion(version) + return ok && base == previous +} + +func TestPrereleaseChangelogSupportsIncrementalAndCumulativeBases(t *testing.T) { + for _, tc := range []struct { + name, version, content string + want bool + }{ + {"incremental beta", "6.6.0-beta.3", "This changelog describes the changes since `v6.6.0-beta.2`", true}, + {"cumulative beta", "6.6.0-beta.3", "This changelog describes the changes since\n`v6.5.0`", true}, + {"incremental candidate", "6.6.0-rc.2", "This changelog describes the changes since `v6.6.0-rc.1`", true}, + {"first beta", "6.6.0-beta.1", "This changelog describes the changes since `v6.5.0`", true}, + {"unrelated stable", "6.6.0-beta.3", "This changelog describes the changes since `v6.4.0`", false}, + {"older beta", "6.6.0-beta.3", "This changelog describes the changes since `v6.6.0-beta.1`", false}, + {"other stage", "6.6.0-rc.2", "This changelog describes the changes since `v6.6.0-beta.1`", false}, + {"missing comparison", "6.6.0-beta.3", "## Changes", false}, + {"conflicting comparisons", "6.6.0-beta.3", "This changelog describes the changes since `v6.5.0`\nThis changelog describes the changes since `v6.6.0-beta.2`", false}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := prereleaseChangelogHasSupportedBase(tc.content, tc.version, "6.5.0"); got != tc.want { + t.Fatalf("supported base = %v, want %v", got, tc.want) + } + }) + } +} + func assertFileContainsAllNormalized(t *testing.T, path string, required ...string) { t.Helper() content, err := os.ReadFile(path) diff --git a/scripts/installtests/install_docker_sh_test.go b/scripts/installtests/install_docker_sh_test.go index d1fe60a96c..000bbcc8b8 100644 --- a/scripts/installtests/install_docker_sh_test.go +++ b/scripts/installtests/install_docker_sh_test.go @@ -301,8 +301,8 @@ func previousPrereleaseVersion(version string) (string, bool) { return "", false } -// The release train cuts several betas before its candidate. The second beta's changelog -// is written as the changes since the first, so the checks that read it must expect that. +// This predecessor is the metadata tool's default comparison base. A cumulative +// changelog can explicitly compare with the previous stable instead. func TestPreviousPrereleaseVersionFollowsItsOwnStage(t *testing.T) { for _, tc := range []struct { version, want string