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 814e42ee..895cac3b 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"]
@@ -147,3 +148,10 @@
OCI_IMAGE_INDEX_MEDIA_TYPE = "application/vnd.oci.image.index.v1+json"
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"
+GL_DEB_REPO_BASE_URL = "https://packages.gardenlinux.io/gardenlinux"
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/__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..e977bc96
--- /dev/null
+++ b/src/gardenlinux/github/release_notes/__init__.py
@@ -0,0 +1,33 @@
+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
+ )
+
+ # TODO: image ids
+
+ 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..5ee2b7a8
--- /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 GL_DEB_REPO_BASE_URL, REQUESTS_TIMEOUTS
+
+
+def get_package_list(gardenlinux_version):
+ 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()
+
+ 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..fbfa21e2
--- /dev/null
+++ b/src/gardenlinux/github/release_notes/sections.py
@@ -0,0 +1,112 @@
+import re
+import textwrap
+
+import requests
+
+from gardenlinux.constants import GLVD_BASE_URL, 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"{GLVD_BASE_URL}/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