From 2fa8a0681f26d79262955a0c62ab340657d1a5b6 Mon Sep 17 00:00:00 2001 From: Scott Dodson Date: Sat, 9 May 2026 21:32:33 -0400 Subject: [PATCH 1/3] Handle RHCOS 10 component names and versions in 5.Y payloads The Components list enrichment (URLs to RHCOS release browser) could miss RHCOS 10 entries when oc outputs names like "CoreOS 10.2" (period directly after "10") or when the generic "CoreOS" name is used with a 10.x version string. JSON: switch from exact component.Name match to strings.HasPrefix for the "Red Hat Enterprise Linux CoreOS 10" prefix so that "CoreOS 10.2", "CoreOS 10.20", etc. are all enriched with release browser URLs. Markdown regexes: broaden the RHCOS 10 patterns from `CoreOS 10(?: \d+\.\d+)?` to `CoreOS 10(?:[. ]\d[\d.]*)?` so they match "CoreOS 10.2" (period-separated) in addition to the existing "CoreOS 10" and "CoreOS 10 10.2" (space-separated) formats. Markdown fallback: when the generic RHCOS regex matches (no "10" in the component name) but the version string starts with 10.x, label the component as "Red Hat Enterprise Linux CoreOS 10" instead of the unversioned name. Co-Authored-By: Claude Opus 4.6 rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- pkg/rhcos/rhcos.go | 23 +++++-- pkg/rhcos/rhcos_test.go | 145 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 156 insertions(+), 12 deletions(-) diff --git a/pkg/rhcos/rhcos.go b/pkg/rhcos/rhcos.go index 2e82ca729..6203d8d71 100644 --- a/pkg/rhcos/rhcos.go +++ b/pkg/rhcos/rhcos.go @@ -42,8 +42,9 @@ var ( reMdRHCoSVersion = regexp.MustCompile(`\* Red Hat Enterprise Linux CoreOS(?: \d+\.\d+)? ((\d+)\.[\w\.\-]+)\n`) // RHEL 10 node image (rhel-coreos-10); match before generic RHCOS regex (longer prefix first). - reMdRHCoS10Diff = regexp.MustCompile(`\* Red Hat Enterprise Linux CoreOS 10(?: \d+\.\d+)? upgraded from ((\d+)\.[\w\.\-]+) to ((\d+)\.[\w\.\-]+)\n`) - reMdRHCoS10Version = regexp.MustCompile(`\* Red Hat Enterprise Linux CoreOS 10(?: \d+\.\d+)? ((\d+)\.[\w\.\-]+)\n`) + // [. ] allows either "CoreOS 10.2 ..." (period) or "CoreOS 10 10.2 ..." (space) after "10". + reMdRHCoS10Diff = regexp.MustCompile(`\* Red Hat Enterprise Linux CoreOS 10(?:[. ]\d[\d.]*)? upgraded from ((\d+)\.[\w\.\-]+) to ((\d+)\.[\w\.\-]+)\n`) + reMdRHCoS10Version = regexp.MustCompile(`\* Red Hat Enterprise Linux CoreOS 10(?:[. ]\d[\d.]*)? ((\d+)\.[\w\.\-]+)\n`) reMdCentOSCoSDiff = regexp.MustCompile(`\* CentOS Stream CoreOS upgraded from ((\d+)\.[\w\.\-]+) to ((\d+)\.[\w\.\-]+)\n`) reMdCentOSCoSVersion = regexp.MustCompile(`\* CentOS Stream CoreOS ((\d+)\.[\w\.\-]+)\n`) @@ -85,7 +86,11 @@ func TransformMarkDownOutput(markdown, fromTag, toTag, architecture, architectur name = rhelCoreOs10 case reMdRHCoSDiff.MatchString(markdown): m = reMdRHCoSDiff.FindStringSubmatch(markdown) - name = rhelCoreOs + if fromMajor, err := strconv.Atoi(m[2]); err == nil && fromMajor >= 10 && fromMajor < 100 { + name = rhelCoreOs10 + } else { + name = rhelCoreOs + } case reMdCentOSCoSDiff.MatchString(markdown): m = reMdCentOSCoSDiff.FindStringSubmatch(markdown) name = centosStreamCoreOs @@ -106,7 +111,11 @@ func TransformMarkDownOutput(markdown, fromTag, toTag, architecture, architectur name = rhelCoreOs10 case reMdRHCoSVersion.MatchString(markdown): m = reMdRHCoSVersion.FindStringSubmatch(markdown) - name = rhelCoreOs + if vMajor, err := strconv.Atoi(m[2]); err == nil && vMajor >= 10 && vMajor < 100 { + name = rhelCoreOs10 + } else { + name = rhelCoreOs + } case reMdCentOSCoSVersion.MatchString(markdown): m = reMdCentOSCoSVersion.FindStringSubmatch(markdown) name = centosStreamCoreOs @@ -129,8 +138,10 @@ func TransformJsonOutput(output, architecture, architectureExtension string) (st } for i, component := range changeLogJson.Components { - switch component.Name { - case rhelCoreOs, rhelCoreOs10, centosStreamCoreOs: + switch { + case strings.HasPrefix(component.Name, rhelCoreOs10): + changeLogJson.Components[i] = enrichCoreOSComponentJSON(component, architecture, architectureExtension) + case component.Name == rhelCoreOs || component.Name == centosStreamCoreOs: changeLogJson.Components[i] = enrichCoreOSComponentJSON(component, architecture, architectureExtension) } } diff --git a/pkg/rhcos/rhcos_test.go b/pkg/rhcos/rhcos_test.go index 9547c1ef6..8c7415d9c 100644 --- a/pkg/rhcos/rhcos_test.go +++ b/pkg/rhcos/rhcos_test.go @@ -221,13 +221,66 @@ func TestRHCoSVersionRegex(t *testing.T) { } func TestRHCoS10DiffRegex(t *testing.T) { - input := "* Red Hat Enterprise Linux CoreOS 10 10.0 upgraded from 10.0.20260101-0 to 10.0.20260201-0\n" - m := reMdRHCoS10Diff.FindStringSubmatch(input) - if m == nil { - t.Fatal("expected match for RHEL 10 upgrade line") + testCases := []struct { + name string + input string + shouldMatch bool + fromVersion string + toVersion string + }{ + { + name: "Space-separated RHEL version (original format)", + input: "* Red Hat Enterprise Linux CoreOS 10 10.0 upgraded from 10.0.20260101-0 to 10.0.20260201-0\n", + shouldMatch: true, + fromVersion: "10.0.20260101-0", + toVersion: "10.0.20260201-0", + }, + { + name: "Period-separated RHEL minor (CoreOS 10.2)", + input: "* Red Hat Enterprise Linux CoreOS 10.2 upgraded from 10.2.20260328-0 to 10.2.20260321-0\n", + shouldMatch: true, + fromVersion: "10.2.20260328-0", + toVersion: "10.2.20260321-0", + }, + { + name: "No RHEL minor version", + input: "* Red Hat Enterprise Linux CoreOS 10 upgraded from 10.2.20260328-0 to 10.2.20260321-0\n", + shouldMatch: true, + fromVersion: "10.2.20260328-0", + toVersion: "10.2.20260321-0", + }, + { + name: "Two-digit RHEL minor", + input: "* Red Hat Enterprise Linux CoreOS 10.20 upgraded from 10.20.20270101-0 to 10.20.20270201-0\n", + shouldMatch: true, + fromVersion: "10.20.20270101-0", + toVersion: "10.20.20270201-0", + }, + { + name: "RHCOS 9 line should NOT match RHCOS 10 regex", + input: "* Red Hat Enterprise Linux CoreOS 9.8 upgraded from 9.8.20260305-0 to 9.8.20260312-0\n", + shouldMatch: false, + }, } - if m[1] != "10.0.20260101-0" || m[3] != "10.0.20260201-0" { - t.Fatalf("unexpected submatches: %v", m) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := reMdRHCoS10Diff.FindStringSubmatch(tc.input) + if tc.shouldMatch { + if m == nil { + t.Fatalf("expected match but got none for input: %s", tc.input) + } + if m[1] != tc.fromVersion { + t.Errorf("Expected from version %q, got %q", tc.fromVersion, m[1]) + } + if m[3] != tc.toVersion { + t.Errorf("Expected to version %q, got %q", tc.toVersion, m[3]) + } + } else { + if m != nil { + t.Errorf("Expected no match but got: %v", m) + } + } + }) } } @@ -263,3 +316,83 @@ func TestTransformJsonOutputDualCoreOS(t *testing.T) { t.Fatalf("expected two versionUrl fields: %s", out) } } + +func TestTransformJsonOutputRHCOS10WithMinor(t *testing.T) { + j := `{ + "components": [ + {"name": "Red Hat Enterprise Linux CoreOS 10.2", "version": "10.2.20260328-0", "from": "10.2.20260321-0"} + ] +}` + out, err := TransformJsonOutput(j, "x86_64", "") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, `"versionUrl"`) { + t.Fatalf("expected versionUrl in output for RHCOS 10.2 component name: %s", out) + } + if !strings.Contains(out, "rhel-10.2") { + t.Fatalf("expected rhel-10.2 stream in URL: %s", out) + } +} + +func TestTransformMarkDownOutputRHCOS10Fallback(t *testing.T) { + input := `## Changes from 5.0.0-0.nightly-2026-03-01-000000 +* Red Hat Enterprise Linux CoreOS upgraded from 10.2.20260301-0 to 10.2.20260315-0 +` + out, err := TransformMarkDownOutput(input, "5.0.0-0.nightly-2026-03-01-000000", "5.0.0-0.nightly-2026-03-15-000000", "x86_64", "") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, "Red Hat Enterprise Linux CoreOS 10") { + t.Fatalf("expected RHCOS 10 label when version starts with 10.x, got:\n%s", out) + } +} + +func TestTransformMarkDownOutputRHCOS10PeriodFormat(t *testing.T) { + input := `## Changes from 5.0.0 +* Red Hat Enterprise Linux CoreOS 10.2 upgraded from 10.2.20260328-0 to 10.2.20260321-0 +` + out, err := TransformMarkDownOutput(input, "5.0.0", "5.0.1", "x86_64", "") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, "Red Hat Enterprise Linux CoreOS 10") { + t.Fatalf("expected RHCOS 10 label for CoreOS 10.2 format, got:\n%s", out) + } + if !strings.Contains(out, "rhel-10.2") { + t.Fatalf("expected rhel-10.2 stream in URL, got:\n%s", out) + } +} + +func TestGetRHCoSReleaseStreamRHCOS10(t *testing.T) { + testCases := []struct { + name string + version string + ok bool + expected string + }{ + { + name: "RHCOS 10.2", + version: "10.2.20260328-0", + ok: true, + expected: "prod/streams/rhel-10.2", + }, + { + name: "RHCOS 10.0", + version: "10.0.20260101-0", + ok: true, + expected: "prod/streams/rhel-10.0", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, ok := getRHCoSReleaseStream(tc.version, "") + if ok != tc.ok { + t.Errorf("expected ok=%v, got %v", tc.ok, ok) + } + if result != tc.expected { + t.Errorf("expected %q, got %q", tc.expected, result) + } + }) + } +} From 6ef73742f420d91a58c408568b2e5b9992f447fd Mon Sep 17 00:00:00 2001 From: Scott Dodson Date: Sat, 9 May 2026 21:32:44 -0400 Subject: [PATCH 2/3] Extend ReleaseTagIsDualRHCOS to include 5.Y releases OpenShift 5.Y ships RHCOS 10 (and potentially RHCOS 9 during transition). Extend the version check so that Major >= 5 is also recognized, matching the same dual-RHCOS rendering path already used for 4.21+. Co-Authored-By: Claude Opus 4.6 rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- pkg/release-controller/semver.go | 2 +- pkg/release-controller/semver_dual_rhcos_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/release-controller/semver.go b/pkg/release-controller/semver.go index 2a0a8588a..3334eac49 100644 --- a/pkg/release-controller/semver.go +++ b/pkg/release-controller/semver.go @@ -172,5 +172,5 @@ func ReleaseTagIsDualRHCOS(toTag string) bool { if err != nil { return false } - return v.Major == 4 && v.Minor >= 21 + return (v.Major == 4 && v.Minor >= 21) || v.Major >= 5 } diff --git a/pkg/release-controller/semver_dual_rhcos_test.go b/pkg/release-controller/semver_dual_rhcos_test.go index 7317885e2..2307fe6e6 100644 --- a/pkg/release-controller/semver_dual_rhcos_test.go +++ b/pkg/release-controller/semver_dual_rhcos_test.go @@ -13,6 +13,10 @@ func TestReleaseTagIsDualRHCOS(t *testing.T) { {"4.20.0", false}, {"4.20.0-ec.0", false}, {"not-a-version", false}, + {"5.0.0", true}, + {"5.0.0-ec.1", true}, + {"5.1.0", true}, + {"5.2.3", true}, } for _, tt := range tests { t.Run(tt.tag, func(t *testing.T) { From b7ca4c63b471b5744397898f5c67c1dc50b1585c Mon Sep 17 00:00:00 2001 From: Scott Dodson Date: Sat, 9 May 2026 22:40:41 -0400 Subject: [PATCH 3/3] Prefer RHCOS 9 in Components for 4.Y, RHCOS 10 for 5.Y When both rhel-coreos (RHCOS 9) and rhel-coreos-10 (RHCOS 10) exist in a payload, oc adm release info chooses which one to show in the Components section based on its own logic (often alphabetically or by tag priority). This meant 4.21 nightlies showed RHCOS 10 even though RHCOS 9 is the primary node OS for 4.Y releases. This change post-processes the changelog markdown to swap the RHCOS component when needed: - 4.Y releases prefer rhel-coreos (RHCOS 9) in the Components section - 5.Y+ releases prefer rhel-coreos-10 (RHCOS 10) in the Components section The Node Image Info section continues to show both RHCOS versions with full package details for dual-RHCOS releases (4.21+, 5.Y+). Implementation: - Add PreferredMachineOSTag() to determine the preferred machine-OS tag based on OpenShift major version - Extend TransformMarkDownOutput() to accept ReleaseInfo and toImage parameters for querying available machine-OS streams - Add swapRHCOSComponentIfNeeded() to detect and swap the RHCOS component in the Components section when both versions exist - Update all callers (changelog-preview, http_changelog, tests) Co-Authored-By: Claude Sonnet 4.5 rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- cmd/release-controller-api/http_changelog.go | 2 +- hack/changelog-preview/main.go | 2 +- pkg/release-controller/semver.go | 16 ++ .../semver_dual_rhcos_test.go | 30 ++++ pkg/rhcos/rhcos.go | 166 +++++++++++++++++- pkg/rhcos/rhcos_test.go | 6 +- 6 files changed, 216 insertions(+), 6 deletions(-) diff --git a/cmd/release-controller-api/http_changelog.go b/cmd/release-controller-api/http_changelog.go index b6f3d87ca..a9b88bf4b 100644 --- a/cmd/release-controller-api/http_changelog.go +++ b/cmd/release-controller-api/http_changelog.go @@ -76,7 +76,7 @@ func (c *Controller) getChangeLog(ctx context.Context, ch chan renderResult, chN return } - out, err = rhcos.TransformMarkDownOutput(out, fromTag, toTag, architecture, archExtension) + out, err = rhcos.TransformMarkDownOutput(out, fromTag, toTag, architecture, archExtension, c.releaseInfo, toImage.GenerateDigestPullSpec()) if err != nil { ch <- renderResult{err: err} return diff --git a/hack/changelog-preview/main.go b/hack/changelog-preview/main.go index be81342cb..9c57926ea 100644 --- a/hack/changelog-preview/main.go +++ b/hack/changelog-preview/main.go @@ -61,7 +61,7 @@ func main() { fmt.Fprintf(os.Stderr, "ChangeLog: %v\n", err) os.Exit(1) } - out, err = rhcos.TransformMarkDownOutput(out, *fromTag, *toTag, archName, archExt) + out, err = rhcos.TransformMarkDownOutput(out, *fromTag, *toTag, archName, archExt, info, *to) if err != nil { fmt.Fprintf(os.Stderr, "TransformMarkDownOutput: %v\n", err) os.Exit(1) diff --git a/pkg/release-controller/semver.go b/pkg/release-controller/semver.go index 3334eac49..73f6a0721 100644 --- a/pkg/release-controller/semver.go +++ b/pkg/release-controller/semver.go @@ -174,3 +174,19 @@ func ReleaseTagIsDualRHCOS(toTag string) bool { } return (v.Major == 4 && v.Minor >= 21) || v.Major >= 5 } + +// PreferredMachineOSTag returns the machine-OS tag that should be displayed first +// in the Components section based on the OpenShift major version. +// - 4.Y releases prefer rhel-coreos (RHCOS 9) +// - 5.Y+ releases prefer rhel-coreos-10 (RHCOS 10) +// Returns empty string if the version can't be parsed. +func PreferredMachineOSTag(releaseTag string) string { + v, err := SemverParseTolerant(releaseTag) + if err != nil { + return "" + } + if v.Major >= 5 { + return "rhel-coreos-10" + } + return "rhel-coreos" +} diff --git a/pkg/release-controller/semver_dual_rhcos_test.go b/pkg/release-controller/semver_dual_rhcos_test.go index 2307fe6e6..04af8a212 100644 --- a/pkg/release-controller/semver_dual_rhcos_test.go +++ b/pkg/release-controller/semver_dual_rhcos_test.go @@ -26,3 +26,33 @@ func TestReleaseTagIsDualRHCOS(t *testing.T) { }) } } + +func TestPreferredMachineOSTag(t *testing.T) { + tests := []struct { + tag string + want string + }{ + // 4.Y releases prefer rhel-coreos (RHCOS 9) + {"4.21.0-ec.1", "rhel-coreos"}, + {"4.21.0", "rhel-coreos"}, + {"4.22.1", "rhel-coreos"}, + {"4.20.0", "rhel-coreos"}, + {"4.20.0-ec.0", "rhel-coreos"}, + {"4.30.0", "rhel-coreos"}, + // 5.Y+ releases prefer rhel-coreos-10 (RHCOS 10) + {"5.0.0", "rhel-coreos-10"}, + {"5.0.0-ec.1", "rhel-coreos-10"}, + {"5.1.0", "rhel-coreos-10"}, + {"5.2.3", "rhel-coreos-10"}, + {"6.0.0", "rhel-coreos-10"}, + // Invalid versions return empty string + {"not-a-version", ""}, + } + for _, tt := range tests { + t.Run(tt.tag, func(t *testing.T) { + if got := PreferredMachineOSTag(tt.tag); got != tt.want { + t.Errorf("PreferredMachineOSTag(%q) = %q, want %q", tt.tag, got, tt.want) + } + }) + } +} diff --git a/pkg/rhcos/rhcos.go b/pkg/rhcos/rhcos.go index 6203d8d71..915151db6 100644 --- a/pkg/rhcos/rhcos.go +++ b/pkg/rhcos/rhcos.go @@ -5,6 +5,7 @@ import ( "fmt" "maps" "net/url" + "os" "regexp" "slices" "sort" @@ -59,7 +60,161 @@ var ( reRhelCoreOsVersion = regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)-(\d+)`) ) -func TransformMarkDownOutput(markdown, fromTag, toTag, architecture, architectureExtension string) (string, error) { +// swapRHCOSComponentIfNeeded replaces the RHCOS component shown in the ### Components section +// with the preferred version based on the OpenShift major version (4.Y prefers RHCOS 9, 5.Y+ prefers RHCOS 10). +func swapRHCOSComponentIfNeeded(markdown, toTag, architecture, architectureExtension string, releaseInfo releasecontroller.ReleaseInfo, toImage string) (string, error) { + // Determine preferred machine-OS tag based on release version + preferredTag := releasecontroller.PreferredMachineOSTag(toTag) + if preferredTag == "" { + // Can't parse version, skip swap + return markdown, nil + } + + // Query which machine-OS streams exist in the release + streams, err := releaseInfo.ListMachineOSStreams(toImage) + if err != nil || len(streams) == 0 { + // Can't determine streams, skip swap + return markdown, nil + } + + // Check if both rhel-coreos and rhel-coreos-10 exist + var hasRHCOS9, hasRHCOS10 bool + var rhcos9Info, rhcos10Info releasecontroller.MachineOSStreamInfo + for _, s := range streams { + if s.Tag == "rhel-coreos" { + hasRHCOS9 = true + rhcos9Info = s + } else if s.Tag == "rhel-coreos-10" { + hasRHCOS10 = true + rhcos10Info = s + } + } + + if !hasRHCOS9 || !hasRHCOS10 { + // Only one RHCOS version exists, no need to swap + return markdown, nil + } + + // Parse the Components section to find which RHCOS is currently shown + reComponentsSection := regexp.MustCompile(`(?s)(### Components.*?)\n\n###`) + componentsMatch := reComponentsSection.FindStringSubmatch(markdown) + if componentsMatch == nil { + // Can't find Components section + return markdown, nil + } + + componentsSection := componentsMatch[1] + + // Determine which RHCOS is currently shown and what we want to show + var currentlyShown, wantToShow string + var desiredInfo releasecontroller.MachineOSStreamInfo + + if strings.Contains(componentsSection, rhelCoreOs10) { + currentlyShown = "rhel-coreos-10" + } else if strings.Contains(componentsSection, rhelCoreOs) { + currentlyShown = "rhel-coreos" + } else { + // No RHCOS component found in Components section + return markdown, nil + } + + // Determine what we want to show + if preferredTag == "rhel-coreos-10" { + wantToShow = "rhel-coreos-10" + desiredInfo = rhcos10Info + } else { + wantToShow = "rhel-coreos" + desiredInfo = rhcos9Info + } + + if currentlyShown == wantToShow { + // Already showing the preferred version + return markdown, nil + } + + // Need to swap: fetch the version info for the desired RHCOS + releaseJSON, err := releaseInfo.ReleaseInfo(toImage) + if err != nil { + return markdown, fmt.Errorf("failed to get release info: %w", err) + } + + // Parse the release JSON to get the machine-os component version + var relInfo struct { + References struct { + Spec struct { + Tags []struct { + Name string `json:"name"` + Annotations map[string]string `json:"annotations"` + } `json:"tags"` + } `json:"spec"` + } `json:"references"` + } + + if err := json.Unmarshal([]byte(releaseJSON), &relInfo); err != nil { + return markdown, fmt.Errorf("failed to parse release JSON: %w", err) + } + + // Find the version annotation for the desired machine-OS tag + var desiredVersion string + for _, tag := range relInfo.References.Spec.Tags { + if tag.Name == wantToShow { + if versionAnnotation, ok := tag.Annotations["io.openshift.build.versions"]; ok { + // Parse the version from the annotation, format: "machine-os=X.Y.Z" + for _, part := range strings.Split(versionAnnotation, ",") { + part = strings.TrimSpace(part) + if strings.HasPrefix(part, "machine-os=") { + desiredVersion = strings.TrimPrefix(part, "machine-os=") + break + } + } + } + break + } + } + + if desiredVersion == "" { + // Couldn't find version, skip swap + return markdown, nil + } + + // Build the replacement RHCOS component line with proper formatting + displayName := desiredInfo.DisplayName + if displayName == "" { + displayName = releasecontroller.MachineOSTitle(desiredInfo) + } + + // Build the URL to the RHCOS release browser + stream, ok := getRHCoSReleaseStream(desiredVersion, architectureExtension) + if !ok { + // Can't determine stream, skip enrichment + return markdown, nil + } + + rhcosURL := url.URL{ + Scheme: serviceScheme, + Host: serviceUrl, + Path: "/", + Fragment: desiredVersion, + RawQuery: (url.Values{ + "stream": []string{stream}, + "arch": []string{architecture}, + "release": []string{desiredVersion}, + }).Encode(), + } + + // Create the new component line with enriched link and alert box + enrichedComponent := fmt.Sprintf("* %s [%s](%s) %s", displayName, desiredVersion, rhcosURL.String(), baseLayerAlertBox) + + // Find and replace the old RHCOS component line in the Components section + reComponentLine := regexp.MustCompile(`\* Red Hat Enterprise Linux CoreOS[^\n]+`) + + newComponentsSection := reComponentLine.ReplaceAllString(componentsSection, enrichedComponent) + markdown = strings.Replace(markdown, componentsSection, newComponentsSection, 1) + + return markdown, nil +} + +func TransformMarkDownOutput(markdown, fromTag, toTag, architecture, architectureExtension string, releaseInfo releasecontroller.ReleaseInfo, toImage string) (string, error) { // replace references to the previous version with links rePrevious, err := regexp.Compile(fmt.Sprintf(`([^\w:])%s(\W)`, regexp.QuoteMeta(fromTag))) if err != nil { @@ -76,6 +231,15 @@ func TransformMarkDownOutput(markdown, fromTag, toTag, architecture, architectur // add link to tag from which current version promoted from markdown = reMdPromotedFrom.ReplaceAllString(markdown, fmt.Sprintf("Release %s was created from [$1:$2](/releasetag/$2)", toTag)) + // Swap RHCOS component in Components section if needed (4.Y prefers RHCOS 9, 5.Y+ prefers RHCOS 10) + if releaseInfo != nil && toImage != "" { + markdown, err = swapRHCOSComponentIfNeeded(markdown, toTag, architecture, architectureExtension, releaseInfo, toImage) + if err != nil { + // Log but don't fail - this is a best-effort improvement + fmt.Fprintf(os.Stderr, "Warning: failed to swap RHCOS component: %v\n", err) + } + } + // Apply CoreOS link transforms for every matching line (OpenShift 4.21+ may list RHCOS 9 and 10 separately). for { var m []string diff --git a/pkg/rhcos/rhcos_test.go b/pkg/rhcos/rhcos_test.go index 8c7415d9c..b6e43b8c6 100644 --- a/pkg/rhcos/rhcos_test.go +++ b/pkg/rhcos/rhcos_test.go @@ -289,7 +289,7 @@ func TestTransformMarkDownOutputDualRHCOSLines(t *testing.T) { * Red Hat Enterprise Linux CoreOS 9.8 upgraded from 9.8.20260101-0 to 9.8.20260201-0 * Red Hat Enterprise Linux CoreOS 10 10.0 upgraded from 10.0.20260101-0 to 10.0.20260201-0 ` - out, err := TransformMarkDownOutput(input, "4.20.0", "4.21.0", "x86_64", "") + out, err := TransformMarkDownOutput(input, "4.20.0", "4.21.0", "x86_64", "", nil, "") if err != nil { t.Fatal(err) } @@ -339,7 +339,7 @@ func TestTransformMarkDownOutputRHCOS10Fallback(t *testing.T) { input := `## Changes from 5.0.0-0.nightly-2026-03-01-000000 * Red Hat Enterprise Linux CoreOS upgraded from 10.2.20260301-0 to 10.2.20260315-0 ` - out, err := TransformMarkDownOutput(input, "5.0.0-0.nightly-2026-03-01-000000", "5.0.0-0.nightly-2026-03-15-000000", "x86_64", "") + out, err := TransformMarkDownOutput(input, "5.0.0-0.nightly-2026-03-01-000000", "5.0.0-0.nightly-2026-03-15-000000", "x86_64", "", nil, "") if err != nil { t.Fatal(err) } @@ -352,7 +352,7 @@ func TestTransformMarkDownOutputRHCOS10PeriodFormat(t *testing.T) { input := `## Changes from 5.0.0 * Red Hat Enterprise Linux CoreOS 10.2 upgraded from 10.2.20260328-0 to 10.2.20260321-0 ` - out, err := TransformMarkDownOutput(input, "5.0.0", "5.0.1", "x86_64", "") + out, err := TransformMarkDownOutput(input, "5.0.0", "5.0.1", "x86_64", "", nil, "") if err != nil { t.Fatal(err) }