From c0a9c28274b36996786a1480619870ae06727873 Mon Sep 17 00:00:00 2001 From: Vivus Ignis Date: Wed, 24 Sep 2025 10:41:23 +0200 Subject: [PATCH 1/4] main release_notes routines --- src/gardenlinux/constants.py | 2 + src/gardenlinux/github/release/__init__.py | 6 +- .../github/release_notes/__init__.py | 31 +++++ .../github/release_notes/helpers.py | 35 ++++++ .../github/release_notes/sections.py | 109 ++++++++++++++++++ 5 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 src/gardenlinux/github/release_notes/__init__.py create mode 100644 src/gardenlinux/github/release_notes/helpers.py create mode 100644 src/gardenlinux/github/release_notes/sections.py diff --git a/src/gardenlinux/constants.py b/src/gardenlinux/constants.py index 814e42ee..2e25f255 100644 --- a/src/gardenlinux/constants.py +++ b/src/gardenlinux/constants.py @@ -147,3 +147,5 @@ OCI_IMAGE_INDEX_MEDIA_TYPE = "application/vnd.oci.image.index.v1+json" RELEASE_ID_FILE = ".github_release_id" + +REQUESTS_TIMEOUTS = (5, 30) # connect, read diff --git a/src/gardenlinux/github/release/__init__.py b/src/gardenlinux/github/release/__init__.py index 441c45f2..b908c0d3 100644 --- a/src/gardenlinux/github/release/__init__.py +++ b/src/gardenlinux/github/release/__init__.py @@ -4,12 +4,10 @@ import requests -from gardenlinux.constants import RELEASE_ID_FILE +from gardenlinux.constants import RELEASE_ID_FILE, REQUESTS_TIMEOUTS from gardenlinux.logger import LoggerSetup -LOGGER = LoggerSetup.get_logger("gardenlinux.github", "INFO") - -REQUESTS_TIMEOUTS = (5, 30) # connect, read +LOGGER = LoggerSetup.get_logger("gardenlinux.github.release", "INFO") def create_github_release(owner, repo, tag, commitish, latest, body): diff --git a/src/gardenlinux/github/release_notes/__init__.py b/src/gardenlinux/github/release_notes/__init__.py new file mode 100644 index 00000000..f9993dbf --- /dev/null +++ b/src/gardenlinux/github/release_notes/__init__.py @@ -0,0 +1,31 @@ +from .helpers import get_package_list +from .sections import ( + release_notes_changes_section, + release_notes_compare_package_versions_section, + release_notes_software_components_section, +) + + +def create_github_release_notes(gardenlinux_version, commitish): + package_list = get_package_list(gardenlinux_version) + + output = "" + + output += release_notes_changes_section(gardenlinux_version) + + output += release_notes_software_components_section(package_list) + + output += release_notes_compare_package_versions_section( + gardenlinux_version, package_list + ) + + output += "\n" + output += "## Kernel Module Build Container (kmodbuild)" + output += "\n" + output += "```" + output += "\n" + output += f"ghcr.io/gardenlinux/gardenlinux/kmodbuild:{gardenlinux_version}" + output += "\n" + output += "```" + output += "\n" + return output diff --git a/src/gardenlinux/github/release_notes/helpers.py b/src/gardenlinux/github/release_notes/helpers.py new file mode 100644 index 00000000..42874a95 --- /dev/null +++ b/src/gardenlinux/github/release_notes/helpers.py @@ -0,0 +1,35 @@ +import gzip +import io + +import requests + +from gardenlinux.apt import DebsrcFile, GardenLinuxRepo +from gardenlinux.apt.package_repo_info import compare_repo +from gardenlinux.constants import REQUESTS_TIMEOUTS + + +def get_package_list(gardenlinux_version): + url = f"https://packages.gardenlinux.io/gardenlinux/dists/{gardenlinux_version}/main/binary-amd64/Packages.gz" + response = requests.get(url, timeout=REQUESTS_TIMEOUTS) + response.raise_for_status() + + d = DebsrcFile() + + with io.BytesIO(response.content) as buf: + with gzip.open(buf, "rt") as f: + d.read(f) + + return d + + +def compare_apt_repo_versions(previous_version, current_version): + previous_repo = GardenLinuxRepo(previous_version) + current_repo = GardenLinuxRepo(current_version) + pkg_diffs = sorted(compare_repo(previous_repo, current_repo), key=lambda t: t[0]) + + output = f"| Package | {previous_version} | {current_version} |\n" + output += "|---------|--------------------|-------------------|\n" + + for pkg in pkg_diffs: + output += f"|{pkg[0]} | {pkg[1] if pkg[1] is not None else '-'} | {pkg[2] if pkg[2] is not None else '-'} |\n" + return output diff --git a/src/gardenlinux/github/release_notes/sections.py b/src/gardenlinux/github/release_notes/sections.py new file mode 100644 index 00000000..5b5c7fbc --- /dev/null +++ b/src/gardenlinux/github/release_notes/sections.py @@ -0,0 +1,109 @@ +import re +import textwrap + +import requests + +from gardenlinux.constants import REQUESTS_TIMEOUTS +from gardenlinux.logger import LoggerSetup + +from .helpers import compare_apt_repo_versions + +LOGGER = LoggerSetup.get_logger("gardenlinux.github.release_notes", "INFO") + + +def release_notes_changes_section(gardenlinux_version): + """ + Get list of fixed CVEs, grouped by upgraded package. + Note: This result is not perfect, feel free to edit the generated release notes and + file issues in glvd for improvement suggestions https://github.com/gardenlinux/glvd/issues + """ + try: + url = f"https://glvd.ingress.glvd.gardnlinux.shoot.canary.k8s-hana.ondemand.com/v1/patchReleaseNotes/{gardenlinux_version}" + response = requests.get(url, timeout=REQUESTS_TIMEOUTS) + response.raise_for_status() + data = response.json() + + if len(data["packageList"]) == 0: + return "" + + output = [ + "## Changes", + "The following packages have been upgraded, to address the mentioned CVEs:", + ] + for package in data["packageList"]: + upgrade_line = ( + f"- upgrade '{package['sourcePackageName']}' from `{package['oldVersion']}` " + f"to `{package['newVersion']}`" + ) + output.append(upgrade_line) + + if package["fixedCves"]: + for fixedCve in package["fixedCves"]: + output.append(f" - {fixedCve}") + + return "\n".join(output) + "\n\n" + except Exception as exn: + # There are expected error cases, for example with versions not supported by glvd (1443.x) or when the api is not available + # Fail gracefully by adding the placeholder we previously used, so that the release note generation does not fail. + LOGGER.error(f"Failed to process GLVD API output: {exn}") + return textwrap.dedent( + """ + ## Changes + The following packages have been upgraded, to address the mentioned CVEs: + **todo release facilitator: fill this in** + """ + ) + + +def release_notes_software_components_section(package_list): + output = "## Software Component Versions\n" + output += "```" + output += "\n" + packages_regex = re.compile( + r"^linux-image-amd64$|^systemd$|^containerd$|^runc$|^curl$|^openssl$|^openssh-server$|^libc-bin$" + ) + for entry in package_list.values(): + if packages_regex.match(entry.deb_source): + output += f"{entry!r}\n" + output += "```" + output += "\n\n" + return output + + +def release_notes_compare_package_versions_section(gardenlinux_version, package_list): + output = "" + version_components = gardenlinux_version.split(".") + # Assumes we always have version numbers like 1443.2 + if len(version_components) == 2: + try: + major = int(version_components[0]) + patch = int(version_components[1]) + + if patch > 0: + previous_version = f"{major}.{patch - 1}" + + output += ( + f"## Changes in Package Versions Compared to {previous_version}\n" + ) + output += compare_apt_repo_versions( + previous_version, gardenlinux_version + ) + elif patch == 0: + output += f"## Full List of Packages in Garden Linux version {major}\n" + output += "
Expand to see full list\n" + output += "
"
+                output += "\n"
+                for entry in package_list.values():
+                    output += f"{entry!r}\n"
+                output += "
" + output += "\n
\n\n" + + except ValueError: + LOGGER.error( + f"Could not parse {gardenlinux_version} as the Garden Linux version, skipping version compare section" + ) + else: + LOGGER.error( + f"Unexpected version number format {gardenlinux_version}, expected format (major is int).(patch is int)" + ) + return output From 92dc6ee822f6622d725551107596d7a8c669eb19 Mon Sep 17 00:00:00 2001 From: Vivus Ignis Date: Wed, 24 Sep 2025 11:15:12 +0200 Subject: [PATCH 2/4] release notes sections creation --- src/gardenlinux/apt/__init__.py | 3 +- src/gardenlinux/constants.py | 7 +++- src/gardenlinux/github/__main__.py | 29 +++++++++++++- .../github/release_notes/__init__.py | 2 + .../github/release_notes/sections.py | 11 ++++-- tests/constants.py | 2 - tests/github/conftest.py | 2 +- .../test_create_github_release_notes.py | 39 +++++++++++++++++++ 8 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 tests/github/test_create_github_release_notes.py diff --git a/src/gardenlinux/apt/__init__.py b/src/gardenlinux/apt/__init__.py index 3e0ca58c..3d2dfb7c 100644 --- a/src/gardenlinux/apt/__init__.py +++ b/src/gardenlinux/apt/__init__.py @@ -5,5 +5,6 @@ """ from .debsource import Debsrc, DebsrcFile +from .package_repo_info import GardenLinuxRepo -__all__ = ["Debsrc", "DebsrcFile"] +__all__ = ["Debsrc", "DebsrcFile", "GardenLinuxRepo"] diff --git a/src/gardenlinux/constants.py b/src/gardenlinux/constants.py index 2e25f255..76f2bdf0 100644 --- a/src/gardenlinux/constants.py +++ b/src/gardenlinux/constants.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +import os +from pathlib import Path ARCHS = ["amd64", "arm64"] @@ -149,3 +150,7 @@ RELEASE_ID_FILE = ".github_release_id" REQUESTS_TIMEOUTS = (5, 30) # connect, read + +S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads" + +GLVD_BASE_URL = "https://glvd.ingress.glvd.gardnlinux.shoot.canary.k8s-hana.ondemand.com/v1" diff --git a/src/gardenlinux/github/__main__.py b/src/gardenlinux/github/__main__.py index 828f5ba6..9ded0bce 100644 --- a/src/gardenlinux/github/__main__.py +++ b/src/gardenlinux/github/__main__.py @@ -1,12 +1,25 @@ import argparse -from .release import upload_to_github_release_page +from gardenlinux.logger import LoggerSetup + +from .release import create_github_release, upload_to_github_release_page, write_to_release_id_file +from .release_notes import create_github_release_notes + +LOGGER = LoggerSetup.get_logger("gardenlinux.github", "INFO") def main(): parser = argparse.ArgumentParser(description="GitHub Release Script") subparsers = parser.add_subparsers(dest="command") + create_parser = subparsers.add_parser("create") + create_parser.add_argument("--owner", default="gardenlinux") + create_parser.add_argument("--repo", default="gardenlinux") + create_parser.add_argument("--tag", required=True) + create_parser.add_argument("--commit", required=True) + create_parser.add_argument('--latest', action='store_true', default=False) + create_parser.add_argument("--dry-run", action="store_true", default=False) + upload_parser = subparsers.add_parser("upload") upload_parser.add_argument("--owner", default="gardenlinux") upload_parser.add_argument("--repo", default="gardenlinux") @@ -16,7 +29,19 @@ def main(): args = parser.parse_args() - if args.command == "upload": + if args.command == "create": + body = create_github_release_notes(args.tag, args.commit) + if args.dry_run: + print("Dry Run ...") + print("This release would be created:") + print(body) + else: + release_id = create_github_release( + args.owner, args.repo, args.tag, args.commit, args.latest, body + ) + write_to_release_id_file(f"{release_id}") + LOGGER.info(f"Release created with ID: {release_id}") + elif args.command == "upload": upload_to_github_release_page( args.owner, args.repo, args.release_id, args.file_path, args.dry_run ) diff --git a/src/gardenlinux/github/release_notes/__init__.py b/src/gardenlinux/github/release_notes/__init__.py index f9993dbf..e977bc96 100644 --- a/src/gardenlinux/github/release_notes/__init__.py +++ b/src/gardenlinux/github/release_notes/__init__.py @@ -19,6 +19,8 @@ def create_github_release_notes(gardenlinux_version, commitish): gardenlinux_version, package_list ) + # TODO: image ids + output += "\n" output += "## Kernel Module Build Container (kmodbuild)" output += "\n" diff --git a/src/gardenlinux/github/release_notes/sections.py b/src/gardenlinux/github/release_notes/sections.py index 5b5c7fbc..fbfa21e2 100644 --- a/src/gardenlinux/github/release_notes/sections.py +++ b/src/gardenlinux/github/release_notes/sections.py @@ -3,7 +3,7 @@ import requests -from gardenlinux.constants import REQUESTS_TIMEOUTS +from gardenlinux.constants import GLVD_BASE_URL, REQUESTS_TIMEOUTS from gardenlinux.logger import LoggerSetup from .helpers import compare_apt_repo_versions @@ -18,7 +18,7 @@ def release_notes_changes_section(gardenlinux_version): file issues in glvd for improvement suggestions https://github.com/gardenlinux/glvd/issues """ try: - url = f"https://glvd.ingress.glvd.gardnlinux.shoot.canary.k8s-hana.ondemand.com/v1/patchReleaseNotes/{gardenlinux_version}" + url = f"{GLVD_BASE_URL}/patchReleaseNotes/{gardenlinux_version}" response = requests.get(url, timeout=REQUESTS_TIMEOUTS) response.raise_for_status() data = response.json() @@ -43,8 +43,11 @@ def release_notes_changes_section(gardenlinux_version): return "\n".join(output) + "\n\n" except Exception as exn: - # There are expected error cases, for example with versions not supported by glvd (1443.x) or when the api is not available - # Fail gracefully by adding the placeholder we previously used, so that the release note generation does not fail. + # There are expected error cases, + # for example with versions not supported by glvd (1443.x) + # or when the api is not available + # Fail gracefully by adding the placeholder we previously used, + # so that the release note generation does not fail. LOGGER.error(f"Failed to process GLVD API output: {exn}") return textwrap.dedent( """ diff --git a/tests/constants.py b/tests/constants.py index f1bcecae..77a525b4 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -27,6 +27,4 @@ TEST_GARDENLINUX_RELEASE = "1877.3" TEST_GARDENLINUX_COMMIT = "75df9f401a842914563f312899ec3ce34b24515c" -RELEASE_ID_FILE = ".github_release_id" - S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads" diff --git a/tests/github/conftest.py b/tests/github/conftest.py index 68576a82..00ad77f6 100644 --- a/tests/github/conftest.py +++ b/tests/github/conftest.py @@ -4,7 +4,7 @@ import pytest -from ..constants import RELEASE_ID_FILE, S3_DOWNLOADS_DIR +from gardenlinux.constants import RELEASE_ID_FILE, S3_DOWNLOADS_DIR @pytest.fixture diff --git a/tests/github/test_create_github_release_notes.py b/tests/github/test_create_github_release_notes.py new file mode 100644 index 00000000..1a36fb26 --- /dev/null +++ b/tests/github/test_create_github_release_notes.py @@ -0,0 +1,39 @@ +import requests_mock + +from gardenlinux.constants import GLVD_BASE_URL +from gardenlinux.github.release_notes import ( + release_notes_changes_section, + release_notes_compare_package_versions_section, +) + +from ..constants import TEST_GARDENLINUX_RELEASE + + +def test_release_notes_changes_section_empty_packagelist(): + with requests_mock.Mocker() as m: + m.get( + f"{GLVD_BASE_URL}/patchReleaseNotes/{TEST_GARDENLINUX_RELEASE}", + text='{"packageList": []}', + status_code=200 + ) + assert release_notes_changes_section(TEST_GARDENLINUX_RELEASE) == "", \ + "Expected an empty result if GLVD returns an empty package list" + + +def test_release_notes_changes_section_broken_glvd_response(): + with requests_mock.Mocker() as m: + m.get( + f"{GLVD_BASE_URL}/patchReleaseNotes/{TEST_GARDENLINUX_RELEASE}", + text="

Personal Home Page

", + status_code=200 + ) + assert "fill this in" in release_notes_changes_section(TEST_GARDENLINUX_RELEASE), \ + "Expected a placeholder message to be generated if GVLD response is not valid" + + +def test_release_notes_compare_package_versions_section_semver_is_not_recognized(): + assert release_notes_compare_package_versions_section("1.2.0", []) == "", "Semver is not supported" + + +def test_release_notes_compare_package_versions_section_unrecognizable_version(): + assert release_notes_compare_package_versions_section("garden.linux", []) == "" From 222809f1ec4d5da674be7065b4cfbf9c9d9fd399 Mon Sep 17 00:00:00 2001 From: Vivus Ignis Date: Wed, 24 Sep 2025 11:17:26 +0200 Subject: [PATCH 3/4] cleanup --- tests/constants.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/constants.py b/tests/constants.py index 77a525b4..877ad95f 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -1,8 +1,3 @@ -# -*- coding: utf-8 -*- - -import os -from pathlib import Path - from gardenlinux.git import Repository TEST_DATA_DIR = "test-data" @@ -26,5 +21,3 @@ TEST_GARDENLINUX_RELEASE = "1877.3" TEST_GARDENLINUX_COMMIT = "75df9f401a842914563f312899ec3ce34b24515c" - -S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads" From 38b3d1c782a19bea96e086a76f2bdf5e9c61c50c Mon Sep 17 00:00:00 2001 From: Vivus Ignis Date: Thu, 25 Sep 2025 09:19:52 +0200 Subject: [PATCH 4/4] constant for deb repo url --- src/gardenlinux/constants.py | 1 + src/gardenlinux/github/release_notes/helpers.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gardenlinux/constants.py b/src/gardenlinux/constants.py index 76f2bdf0..895cac3b 100644 --- a/src/gardenlinux/constants.py +++ b/src/gardenlinux/constants.py @@ -154,3 +154,4 @@ S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads" GLVD_BASE_URL = "https://glvd.ingress.glvd.gardnlinux.shoot.canary.k8s-hana.ondemand.com/v1" +GL_DEB_REPO_BASE_URL = "https://packages.gardenlinux.io/gardenlinux" diff --git a/src/gardenlinux/github/release_notes/helpers.py b/src/gardenlinux/github/release_notes/helpers.py index 42874a95..5ee2b7a8 100644 --- a/src/gardenlinux/github/release_notes/helpers.py +++ b/src/gardenlinux/github/release_notes/helpers.py @@ -5,11 +5,11 @@ from gardenlinux.apt import DebsrcFile, GardenLinuxRepo from gardenlinux.apt.package_repo_info import compare_repo -from gardenlinux.constants import REQUESTS_TIMEOUTS +from gardenlinux.constants import GL_DEB_REPO_BASE_URL, REQUESTS_TIMEOUTS def get_package_list(gardenlinux_version): - url = f"https://packages.gardenlinux.io/gardenlinux/dists/{gardenlinux_version}/main/binary-amd64/Packages.gz" + url = f"{GL_DEB_REPO_BASE_URL}/dists/{gardenlinux_version}/main/binary-amd64/Packages.gz" response = requests.get(url, timeout=REQUESTS_TIMEOUTS) response.raise_for_status()