diff --git a/.gitignore b/.gitignore index 67f4fe3a2..459bba069 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,3 @@ __pycache__/ coverage.out golangci-lint.out report.json - -# Local output from hack/changelog-preview (go run … > sample-changelog.md) -hack/changelog-preview/sample-changelog.md diff --git a/cmd/release-controller-api/http_changelog.go b/cmd/release-controller-api/http_changelog.go index aa47f70ec..d04501b98 100644 --- a/cmd/release-controller-api/http_changelog.go +++ b/cmd/release-controller-api/http_changelog.go @@ -72,7 +72,6 @@ func (c *Controller) getChangeLog(ch chan renderResult, chNodeInfo chan renderRe return } ch <- renderResult{out: out} - return } out, err = rhcos.TransformMarkDownOutput(out, fromTag, toTag, architecture, archExtension) @@ -87,31 +86,25 @@ func (c *Controller) getChangeLog(ch chan renderResult, chNodeInfo chan renderRe return } - toImagePullspec := toImage.GenerateDigestPullSpec() - fromImagePullspec := fromImage.GenerateDigestPullSpec() - - // Request node image info when the changelog links to #node-image-info (CoreOS infobox) or when - // the target payload has discoverable machine-os streams (newer oc may omit RHCOS summary lines). - fetchNode := strings.Contains(out, "#node-image-info") - if !fetchNode { - streams, err := c.releaseInfo.ListMachineOSStreams(toImagePullspec) - if err != nil { - chNodeInfo <- renderResult{err: err} - return - } - fetchNode = len(streams) > 0 - } - if !fetchNode { + // Only request node image info if it'll be rendered. Use the exact + // check that renderChangeLog does to know if to consume from us. + if !strings.Contains(out, "#node-image-info") { chNodeInfo <- renderResult{} return } - nodeMD, err := rhcos.NodeImageSectionMarkdown(c.releaseInfo, fromImagePullspec, toImagePullspec, out) + toImagePullspec := toImage.GenerateDigestPullSpec() + rpmlist, err := c.releaseInfo.RpmList(toImagePullspec) if err != nil { chNodeInfo <- renderResult{err: err} - return } - chNodeInfo <- renderResult{out: nodeMD} + + rpmdiff, err := c.releaseInfo.RpmDiff(fromImage.GenerateDigestPullSpec(), toImagePullspec) + if err != nil { + chNodeInfo <- renderResult{err: err} + } + + chNodeInfo <- renderResult{out: rhcos.RenderNodeImageInfo(out, rpmlist, rpmdiff)} } func (c *Controller) renderChangeLog(w http.ResponseWriter, fromPull string, fromTag string, toPull string, toTag string, format string) { @@ -180,17 +173,9 @@ func (c *Controller) renderChangeLog(w http.ResponseWriter, fromPull string, fro fmt.Fprintf(w, `
%s
`, fmt.Sprintf("Unable to show full changelog: %s", render.err)) } - needsNode := strings.Contains(render.out, "#node-image-info") - if !needsNode && render.err == nil && format != "json" { - toImage, err := releasecontroller.GetImageInfo(c.releaseInfo, c.architecture, toPull) - if err == nil { - streams, err2 := c.releaseInfo.ListMachineOSStreams(toImage.GenerateDigestPullSpec()) - if err2 == nil && len(streams) > 0 { - needsNode = true - } - } - } - if !needsNode { + // only render a CoreOS diff if we need to; we can know this by + // checking if it links to the diff section we create here + if !strings.Contains(render.out, "#node-image-info") { return } diff --git a/hack/changelog-preview/main.go b/hack/changelog-preview/main.go deleted file mode 100644 index bedd4f864..000000000 --- a/hack/changelog-preview/main.go +++ /dev/null @@ -1,81 +0,0 @@ -// changelog-preview runs the same ChangeLog + RHCOS markdown transforms as the release-controller API -// without needing a Kubernetes cluster. Requires `oc` on PATH and registry pull access to the -// release images you pass. -// -// By default it also appends the Node Image Info section (RPM lists and diffs per CoreOS stream -// when applicable), matching the web UI. Use --skip-node-info for changelog-only output. -// -// Example: -// -// go run ./hack/changelog-preview/ \ -// --from quay.io/openshift-release-dev/ocp-release@sha256:... \ -// --to quay.io/openshift-release-dev/ocp-release@sha256:... \ -// --from-tag 4.20.0-0.nightly-2025-01-01-000000 \ -// --to-tag 4.21.0-ec.1 -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/openshift/release-controller/pkg/rhcos" - releasecontroller "github.com/openshift/release-controller/pkg/release-controller" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "sigs.k8s.io/prow/pkg/jira" -) - -func main() { - from := flag.String("from", "", "from release image pull spec (digest or tag@repo)") - to := flag.String("to", "", "to release image pull spec") - fromTag := flag.String("from-tag", "previous", "from tag name (for markdown link substitution)") - toTag := flag.String("to-tag", "current", "to tag name (for markdown link substitution)") - arch := flag.String("arch", "amd64", "release architecture (amd64, arm64, ...)") - skipNode := flag.Bool("skip-node-info", false, "omit Node Image Info (faster; no extra oc rpmdb/image-for calls)") - flag.Parse() - if *from == "" || *to == "" { - fmt.Fprintf(os.Stderr, "usage: changelog-preview --fromThe CoreOS links above are for the base CoreOS layer used to build
the OpenShift node image and do not contain OpenShift components. This is
@@ -41,10 +40,6 @@ var (
reMdRHCoSDiff = regexp.MustCompile(`\* Red Hat Enterprise Linux CoreOS(?: \d+\.\d+)? upgraded from ((\d+)\.[\w\.\-]+) to ((\d+)\.[\w\.\-]+)\n`)
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`)
-
reMdCentOSCoSDiff = regexp.MustCompile(`\* CentOS Stream CoreOS upgraded from ((\d+)\.[\w\.\-]+) to ((\d+)\.[\w\.\-]+)\n`)
reMdCentOSCoSVersion = regexp.MustCompile(`\* CentOS Stream CoreOS ((\d+)\.[\w\.\-]+)\n`)
@@ -75,48 +70,17 @@ 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))
- // Apply CoreOS link transforms for every matching line (OpenShift 4.21+ may list RHCOS 9 and 10 separately).
- for {
- var m []string
- var name string
- switch {
- case reMdRHCoS10Diff.MatchString(markdown):
- m = reMdRHCoS10Diff.FindStringSubmatch(markdown)
- name = rhelCoreOs10
- case reMdRHCoSDiff.MatchString(markdown):
- m = reMdRHCoSDiff.FindStringSubmatch(markdown)
- name = rhelCoreOs
- case reMdCentOSCoSDiff.MatchString(markdown):
- m = reMdCentOSCoSDiff.FindStringSubmatch(markdown)
- name = centosStreamCoreOs
- default:
- m = nil
- }
- if m == nil {
- break
- }
- markdown = transformCoreOSUpgradeLinks(name, architecture, architectureExtension, markdown, m)
+ // TODO: As we get more comfortable with these sorts of transformations, we could make them more generic.
+ // For now, this will have to do.
+ if m := reMdRHCoSDiff.FindStringSubmatch(markdown); m != nil {
+ markdown = transformCoreOSUpgradeLinks(rhelCoreOs, architecture, architectureExtension, markdown, m)
+ } else if m = reMdCentOSCoSDiff.FindStringSubmatch(markdown); m != nil {
+ markdown = transformCoreOSUpgradeLinks(centosStreamCoreOs, architecture, architectureExtension, markdown, m)
}
- for {
- var m []string
- var name string
- switch {
- case reMdRHCoS10Version.MatchString(markdown):
- m = reMdRHCoS10Version.FindStringSubmatch(markdown)
- name = rhelCoreOs10
- case reMdRHCoSVersion.MatchString(markdown):
- m = reMdRHCoSVersion.FindStringSubmatch(markdown)
- name = rhelCoreOs
- case reMdCentOSCoSVersion.MatchString(markdown):
- m = reMdCentOSCoSVersion.FindStringSubmatch(markdown)
- name = centosStreamCoreOs
- default:
- m = nil
- }
- if m == nil {
- break
- }
- markdown = transformCoreOSLinks(name, architecture, architectureExtension, markdown, m)
+ if m := reMdRHCoSVersion.FindStringSubmatch(markdown); m != nil {
+ markdown = transformCoreOSLinks(rhelCoreOs, architecture, architectureExtension, markdown, m)
+ } else if m = reMdCentOSCoSVersion.FindStringSubmatch(markdown); m != nil {
+ markdown = transformCoreOSLinks(centosStreamCoreOs, architecture, architectureExtension, markdown, m)
}
return markdown, nil
}
@@ -130,8 +94,58 @@ func TransformJsonOutput(output, architecture, architectureExtension string) (st
for i, component := range changeLogJson.Components {
switch component.Name {
- case rhelCoreOs, rhelCoreOs10, centosStreamCoreOs:
- changeLogJson.Components[i] = enrichCoreOSComponentJSON(component, architecture, architectureExtension)
+ case rhelCoreOs, centosStreamCoreOs:
+ var ok bool
+ var fromStream, toStream string
+ if len(component.Version) == 0 {
+ continue
+ }
+ if toStream, ok = getRHCoSReleaseStream(component.Version, architectureExtension); ok {
+ toURL := url.URL{
+ Scheme: serviceScheme,
+ Host: serviceUrl,
+ Path: "/",
+ Fragment: component.Version,
+ RawQuery: (url.Values{
+ "stream": []string{toStream},
+ "arch": []string{architecture},
+ "release": []string{component.Version},
+ }).Encode(),
+ }
+ component.VersionUrl = toURL.String()
+ }
+
+ if len(component.From) > 0 {
+ if fromStream, ok = getRHCoSReleaseStream(component.From, architectureExtension); ok {
+ fromUrl := url.URL{
+ Scheme: serviceScheme,
+ Host: serviceUrl,
+ Path: "/",
+ Fragment: component.From,
+ RawQuery: (url.Values{
+ "stream": []string{fromStream},
+ "arch": []string{architecture},
+ "release": []string{component.From},
+ }).Encode(),
+ }
+ component.FromUrl = fromUrl.String()
+
+ diffURL := url.URL{
+ Scheme: serviceScheme,
+ Host: serviceUrl,
+ Path: "/diff.html",
+ RawQuery: (url.Values{
+ "first_stream": []string{fromStream},
+ "first_release": []string{component.From},
+ "second_stream": []string{toStream},
+ "second_release": []string{component.Version},
+ "arch": []string{architecture},
+ }).Encode(),
+ }
+ component.DiffUrl = diffURL.String()
+ }
+ }
+ changeLogJson.Components[i] = component
}
}
@@ -143,60 +157,6 @@ func TransformJsonOutput(output, architecture, architectureExtension string) (st
return string(updated), nil
}
-func enrichCoreOSComponentJSON(component releasecontroller.ChangeLogComponentInfo, architecture, architectureExtension string) releasecontroller.ChangeLogComponentInfo {
- var ok bool
- var fromStream, toStream string
- if len(component.Version) == 0 {
- return component
- }
- if toStream, ok = getRHCoSReleaseStream(component.Version, architectureExtension); ok {
- toURL := url.URL{
- Scheme: serviceScheme,
- Host: serviceUrl,
- Path: "/",
- Fragment: component.Version,
- RawQuery: (url.Values{
- "stream": []string{toStream},
- "arch": []string{architecture},
- "release": []string{component.Version},
- }).Encode(),
- }
- component.VersionUrl = toURL.String()
- }
-
- if len(component.From) > 0 {
- if fromStream, ok = getRHCoSReleaseStream(component.From, architectureExtension); ok {
- fromUrl := url.URL{
- Scheme: serviceScheme,
- Host: serviceUrl,
- Path: "/",
- Fragment: component.From,
- RawQuery: (url.Values{
- "stream": []string{fromStream},
- "arch": []string{architecture},
- "release": []string{component.From},
- }).Encode(),
- }
- component.FromUrl = fromUrl.String()
-
- diffURL := url.URL{
- Scheme: serviceScheme,
- Host: serviceUrl,
- Path: "/diff.html",
- RawQuery: (url.Values{
- "first_stream": []string{fromStream},
- "first_release": []string{component.From},
- "second_stream": []string{toStream},
- "second_release": []string{component.Version},
- "arch": []string{architecture},
- }).Encode(),
- }
- component.DiffUrl = diffURL.String()
- }
- }
- return component
-}
-
func getRHCoSReleaseStream(version, architectureExtension string) (string, bool) {
if strings.HasPrefix(version, "4") {
if m := reOcpCoreOsVersion.FindStringSubmatch(version); m != nil {
@@ -348,107 +308,68 @@ func transformCoreOSLinks(name, architecture, architectureExtension, input strin
return strings.ReplaceAll(input, matches[0], replace)
}
-// CoreOSNodeStream holds RPM package lists and diffs for one rhel-coreos* or stream-coreos image.
-type CoreOSNodeStream struct {
- Title string
- RpmList releasecontroller.RpmList
- RpmDiff releasecontroller.RpmDiff
-}
-
func RenderNodeImageInfo(markdown string, rpmList releasecontroller.RpmList, rpmDiff releasecontroller.RpmDiff) string {
- return RenderDualNodeImageInfo(markdown, []CoreOSNodeStream{{RpmList: rpmList, RpmDiff: rpmDiff}})
-}
+ output := new(strings.Builder)
-// RenderDualNodeImageInfo renders one or more Node Image Info sections (e.g. multiple machine-OS
-// streams in OpenShift 4.21+).
-func RenderDualNodeImageInfo(markdown string, streams []CoreOSNodeStream) string {
- if len(streams) == 0 {
- return ""
- }
- var out strings.Builder
- dual := len(streams) > 1
- for i, s := range streams {
- if i > 0 {
- out.WriteString("\n\n")
- }
- renderOneNodeStream(&out, s, dual)
- }
- out.WriteString(baseLayerFooter(markdown))
- return out.String()
-}
-
-func renderOneNodeStream(out *strings.Builder, stream CoreOSNodeStream, dual bool) {
- h := "###"
- if stream.Title != "" {
- fmt.Fprintf(out, "### %s\n\n", stream.Title)
- h = "####"
- } else if dual {
- h = "####"
- }
-
- fmt.Fprintf(out, "%s Package List\n\n", h)
+ fmt.Fprintf(output, "### Package List\n\n")
importantPkgs := []string{"cri-o", "kernel", "openshift-kubelet", "systemd"}
for _, pkg := range importantPkgs {
- fmt.Fprintf(out, "* %s-%s\n", pkg, stream.RpmList.Packages[pkg])
+ fmt.Fprintf(output, "* %s-%s\n", pkg, rpmList.Packages[pkg])
}
- fmt.Fprintf(out, "\nFull list (%d packages)
\n\n", len(stream.RpmList.Packages))
- sortedPkgs := slices.Sorted(maps.Keys(stream.RpmList.Packages))
+ fmt.Fprintf(output, "\nFull list (%d packages)
\n\n", len(rpmList.Packages))
+ sortedPkgs := slices.Sorted(maps.Keys(rpmList.Packages))
for _, pkg := range sortedPkgs {
- fmt.Fprintf(out, "* %s-%s\n", pkg, stream.RpmList.Packages[pkg])
+ fmt.Fprintf(output, "* %s-%s\n", pkg, rpmList.Packages[pkg])
}
- fmt.Fprintf(out, "Full list (%d packages)
\n\n", len(stream.RpmList.Extensions))
- sortedPkgs = slices.Sorted(maps.Keys(stream.RpmList.Extensions))
+ fmt.Fprintf(output, "\nFull list (%d packages)
\n\n", len(rpmList.Extensions))
+ sortedPkgs = slices.Sorted(maps.Keys(rpmList.Extensions))
for _, pkg := range sortedPkgs {
- fmt.Fprintf(out, "* %s-%s\n", pkg, stream.RpmList.Extensions[pkg])
+ fmt.Fprintf(output, "* %s-%s\n", pkg, rpmList.Extensions[pkg])
}
- fmt.Fprintf(out, "
%s **base layer**: %s\n\n", m[1], m[3])
}
- var b strings.Builder
- for _, m := range matches {
- fmt.Fprintf(&b, "
%s **base layer**: %s\n\n", m[1], m[3])
- }
- return b.String()
+
+ return output.String()
}
diff --git a/pkg/rhcos/rhcos_test.go b/pkg/rhcos/rhcos_test.go
index 9547c1ef6..e3296ff4e 100644
--- a/pkg/rhcos/rhcos_test.go
+++ b/pkg/rhcos/rhcos_test.go
@@ -1,7 +1,6 @@
package rhcos
import (
- "strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -219,47 +218,3 @@ 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")
- }
- if m[1] != "10.0.20260101-0" || m[3] != "10.0.20260201-0" {
- t.Fatalf("unexpected submatches: %v", m)
- }
-}
-
-func TestTransformMarkDownOutputDualRHCOSLines(t *testing.T) {
- input := `## Changes from 4.20.0
-* 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", "")
- if err != nil {
- t.Fatal(err)
- }
- if strings.Count(out, "coreos-base-alert") < 2 {
- t.Fatalf("expected two CoreOS base layer infoboxes, got:\n%s", out)
- }
-}
-
-func TestTransformJsonOutputDualCoreOS(t *testing.T) {
- j := `{
- "components": [
- {"name": "Red Hat Enterprise Linux CoreOS", "version": "9.8.20260201-0", "from": "9.8.20260101-0"},
- {"name": "Red Hat Enterprise Linux CoreOS 10", "version": "10.0.20260201-0", "from": "10.0.20260101-0"}
- ]
-}`
- out, err := TransformJsonOutput(j, "x86_64", "")
- if err != nil {
- t.Fatal(err)
- }
- if !strings.Contains(out, `"versionUrl"`) {
- t.Fatalf("expected versionUrl in output: %s", out)
- }
- if strings.Count(out, `"versionUrl"`) < 2 {
- t.Fatalf("expected two versionUrl fields: %s", out)
- }
-}