Skip to content

Commit 8d6fb94

Browse files
authored
Merge pull request #202 from gardenlinux/create-github-release-notes
Create GitHub release notes
2 parents ef7d368 + 38b3d1c commit 8d6fb94

File tree

10 files changed

+260
-18
lines changed

10 files changed

+260
-18
lines changed

src/gardenlinux/apt/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"""
66

77
from .debsource import Debsrc, DebsrcFile
8+
from .package_repo_info import GardenLinuxRepo
89

9-
__all__ = ["Debsrc", "DebsrcFile"]
10+
__all__ = ["Debsrc", "DebsrcFile", "GardenLinuxRepo"]

src/gardenlinux/constants.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# -*- coding: utf-8 -*-
1+
import os
2+
from pathlib import Path
23

34
ARCHS = ["amd64", "arm64"]
45

@@ -147,3 +148,10 @@
147148
OCI_IMAGE_INDEX_MEDIA_TYPE = "application/vnd.oci.image.index.v1+json"
148149

149150
RELEASE_ID_FILE = ".github_release_id"
151+
152+
REQUESTS_TIMEOUTS = (5, 30) # connect, read
153+
154+
S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads"
155+
156+
GLVD_BASE_URL = "https://glvd.ingress.glvd.gardnlinux.shoot.canary.k8s-hana.ondemand.com/v1"
157+
GL_DEB_REPO_BASE_URL = "https://packages.gardenlinux.io/gardenlinux"

src/gardenlinux/github/__main__.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import argparse
22

3-
from .release import upload_to_github_release_page
3+
from gardenlinux.logger import LoggerSetup
4+
5+
from .release import create_github_release, upload_to_github_release_page, write_to_release_id_file
6+
from .release_notes import create_github_release_notes
7+
8+
LOGGER = LoggerSetup.get_logger("gardenlinux.github", "INFO")
49

510

611
def main():
712
parser = argparse.ArgumentParser(description="GitHub Release Script")
813
subparsers = parser.add_subparsers(dest="command")
914

15+
create_parser = subparsers.add_parser("create")
16+
create_parser.add_argument("--owner", default="gardenlinux")
17+
create_parser.add_argument("--repo", default="gardenlinux")
18+
create_parser.add_argument("--tag", required=True)
19+
create_parser.add_argument("--commit", required=True)
20+
create_parser.add_argument('--latest', action='store_true', default=False)
21+
create_parser.add_argument("--dry-run", action="store_true", default=False)
22+
1023
upload_parser = subparsers.add_parser("upload")
1124
upload_parser.add_argument("--owner", default="gardenlinux")
1225
upload_parser.add_argument("--repo", default="gardenlinux")
@@ -16,7 +29,19 @@ def main():
1629

1730
args = parser.parse_args()
1831

19-
if args.command == "upload":
32+
if args.command == "create":
33+
body = create_github_release_notes(args.tag, args.commit)
34+
if args.dry_run:
35+
print("Dry Run ...")
36+
print("This release would be created:")
37+
print(body)
38+
else:
39+
release_id = create_github_release(
40+
args.owner, args.repo, args.tag, args.commit, args.latest, body
41+
)
42+
write_to_release_id_file(f"{release_id}")
43+
LOGGER.info(f"Release created with ID: {release_id}")
44+
elif args.command == "upload":
2045
upload_to_github_release_page(
2146
args.owner, args.repo, args.release_id, args.file_path, args.dry_run
2247
)

src/gardenlinux/github/release/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44

55
import requests
66

7-
from gardenlinux.constants import RELEASE_ID_FILE
7+
from gardenlinux.constants import RELEASE_ID_FILE, REQUESTS_TIMEOUTS
88
from gardenlinux.logger import LoggerSetup
99

10-
LOGGER = LoggerSetup.get_logger("gardenlinux.github", "INFO")
11-
12-
REQUESTS_TIMEOUTS = (5, 30) # connect, read
10+
LOGGER = LoggerSetup.get_logger("gardenlinux.github.release", "INFO")
1311

1412

1513
def create_github_release(owner, repo, tag, commitish, latest, body):
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from .helpers import get_package_list
2+
from .sections import (
3+
release_notes_changes_section,
4+
release_notes_compare_package_versions_section,
5+
release_notes_software_components_section,
6+
)
7+
8+
9+
def create_github_release_notes(gardenlinux_version, commitish):
10+
package_list = get_package_list(gardenlinux_version)
11+
12+
output = ""
13+
14+
output += release_notes_changes_section(gardenlinux_version)
15+
16+
output += release_notes_software_components_section(package_list)
17+
18+
output += release_notes_compare_package_versions_section(
19+
gardenlinux_version, package_list
20+
)
21+
22+
# TODO: image ids
23+
24+
output += "\n"
25+
output += "## Kernel Module Build Container (kmodbuild)"
26+
output += "\n"
27+
output += "```"
28+
output += "\n"
29+
output += f"ghcr.io/gardenlinux/gardenlinux/kmodbuild:{gardenlinux_version}"
30+
output += "\n"
31+
output += "```"
32+
output += "\n"
33+
return output
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import gzip
2+
import io
3+
4+
import requests
5+
6+
from gardenlinux.apt import DebsrcFile, GardenLinuxRepo
7+
from gardenlinux.apt.package_repo_info import compare_repo
8+
from gardenlinux.constants import GL_DEB_REPO_BASE_URL, REQUESTS_TIMEOUTS
9+
10+
11+
def get_package_list(gardenlinux_version):
12+
url = f"{GL_DEB_REPO_BASE_URL}/dists/{gardenlinux_version}/main/binary-amd64/Packages.gz"
13+
response = requests.get(url, timeout=REQUESTS_TIMEOUTS)
14+
response.raise_for_status()
15+
16+
d = DebsrcFile()
17+
18+
with io.BytesIO(response.content) as buf:
19+
with gzip.open(buf, "rt") as f:
20+
d.read(f)
21+
22+
return d
23+
24+
25+
def compare_apt_repo_versions(previous_version, current_version):
26+
previous_repo = GardenLinuxRepo(previous_version)
27+
current_repo = GardenLinuxRepo(current_version)
28+
pkg_diffs = sorted(compare_repo(previous_repo, current_repo), key=lambda t: t[0])
29+
30+
output = f"| Package | {previous_version} | {current_version} |\n"
31+
output += "|---------|--------------------|-------------------|\n"
32+
33+
for pkg in pkg_diffs:
34+
output += f"|{pkg[0]} | {pkg[1] if pkg[1] is not None else '-'} | {pkg[2] if pkg[2] is not None else '-'} |\n"
35+
return output
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import re
2+
import textwrap
3+
4+
import requests
5+
6+
from gardenlinux.constants import GLVD_BASE_URL, REQUESTS_TIMEOUTS
7+
from gardenlinux.logger import LoggerSetup
8+
9+
from .helpers import compare_apt_repo_versions
10+
11+
LOGGER = LoggerSetup.get_logger("gardenlinux.github.release_notes", "INFO")
12+
13+
14+
def release_notes_changes_section(gardenlinux_version):
15+
"""
16+
Get list of fixed CVEs, grouped by upgraded package.
17+
Note: This result is not perfect, feel free to edit the generated release notes and
18+
file issues in glvd for improvement suggestions https://github.com/gardenlinux/glvd/issues
19+
"""
20+
try:
21+
url = f"{GLVD_BASE_URL}/patchReleaseNotes/{gardenlinux_version}"
22+
response = requests.get(url, timeout=REQUESTS_TIMEOUTS)
23+
response.raise_for_status()
24+
data = response.json()
25+
26+
if len(data["packageList"]) == 0:
27+
return ""
28+
29+
output = [
30+
"## Changes",
31+
"The following packages have been upgraded, to address the mentioned CVEs:",
32+
]
33+
for package in data["packageList"]:
34+
upgrade_line = (
35+
f"- upgrade '{package['sourcePackageName']}' from `{package['oldVersion']}` "
36+
f"to `{package['newVersion']}`"
37+
)
38+
output.append(upgrade_line)
39+
40+
if package["fixedCves"]:
41+
for fixedCve in package["fixedCves"]:
42+
output.append(f" - {fixedCve}")
43+
44+
return "\n".join(output) + "\n\n"
45+
except Exception as exn:
46+
# There are expected error cases,
47+
# for example with versions not supported by glvd (1443.x)
48+
# or when the api is not available
49+
# Fail gracefully by adding the placeholder we previously used,
50+
# so that the release note generation does not fail.
51+
LOGGER.error(f"Failed to process GLVD API output: {exn}")
52+
return textwrap.dedent(
53+
"""
54+
## Changes
55+
The following packages have been upgraded, to address the mentioned CVEs:
56+
**todo release facilitator: fill this in**
57+
"""
58+
)
59+
60+
61+
def release_notes_software_components_section(package_list):
62+
output = "## Software Component Versions\n"
63+
output += "```"
64+
output += "\n"
65+
packages_regex = re.compile(
66+
r"^linux-image-amd64$|^systemd$|^containerd$|^runc$|^curl$|^openssl$|^openssh-server$|^libc-bin$"
67+
)
68+
for entry in package_list.values():
69+
if packages_regex.match(entry.deb_source):
70+
output += f"{entry!r}\n"
71+
output += "```"
72+
output += "\n\n"
73+
return output
74+
75+
76+
def release_notes_compare_package_versions_section(gardenlinux_version, package_list):
77+
output = ""
78+
version_components = gardenlinux_version.split(".")
79+
# Assumes we always have version numbers like 1443.2
80+
if len(version_components) == 2:
81+
try:
82+
major = int(version_components[0])
83+
patch = int(version_components[1])
84+
85+
if patch > 0:
86+
previous_version = f"{major}.{patch - 1}"
87+
88+
output += (
89+
f"## Changes in Package Versions Compared to {previous_version}\n"
90+
)
91+
output += compare_apt_repo_versions(
92+
previous_version, gardenlinux_version
93+
)
94+
elif patch == 0:
95+
output += f"## Full List of Packages in Garden Linux version {major}\n"
96+
output += "<details><summary>Expand to see full list</summary>\n"
97+
output += "<pre>"
98+
output += "\n"
99+
for entry in package_list.values():
100+
output += f"{entry!r}\n"
101+
output += "</pre>"
102+
output += "\n</details>\n\n"
103+
104+
except ValueError:
105+
LOGGER.error(
106+
f"Could not parse {gardenlinux_version} as the Garden Linux version, skipping version compare section"
107+
)
108+
else:
109+
LOGGER.error(
110+
f"Unexpected version number format {gardenlinux_version}, expected format (major is int).(patch is int)"
111+
)
112+
return output

tests/constants.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
# -*- coding: utf-8 -*-
2-
3-
import os
4-
from pathlib import Path
5-
61
from gardenlinux.git import Repository
72

83
TEST_DATA_DIR = "test-data"
@@ -26,7 +21,3 @@
2621

2722
TEST_GARDENLINUX_RELEASE = "1877.3"
2823
TEST_GARDENLINUX_COMMIT = "75df9f401a842914563f312899ec3ce34b24515c"
29-
30-
RELEASE_ID_FILE = ".github_release_id"
31-
32-
S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads"

tests/github/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66

7-
from ..constants import RELEASE_ID_FILE, S3_DOWNLOADS_DIR
7+
from gardenlinux.constants import RELEASE_ID_FILE, S3_DOWNLOADS_DIR
88

99

1010
@pytest.fixture
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import requests_mock
2+
3+
from gardenlinux.constants import GLVD_BASE_URL
4+
from gardenlinux.github.release_notes import (
5+
release_notes_changes_section,
6+
release_notes_compare_package_versions_section,
7+
)
8+
9+
from ..constants import TEST_GARDENLINUX_RELEASE
10+
11+
12+
def test_release_notes_changes_section_empty_packagelist():
13+
with requests_mock.Mocker() as m:
14+
m.get(
15+
f"{GLVD_BASE_URL}/patchReleaseNotes/{TEST_GARDENLINUX_RELEASE}",
16+
text='{"packageList": []}',
17+
status_code=200
18+
)
19+
assert release_notes_changes_section(TEST_GARDENLINUX_RELEASE) == "", \
20+
"Expected an empty result if GLVD returns an empty package list"
21+
22+
23+
def test_release_notes_changes_section_broken_glvd_response():
24+
with requests_mock.Mocker() as m:
25+
m.get(
26+
f"{GLVD_BASE_URL}/patchReleaseNotes/{TEST_GARDENLINUX_RELEASE}",
27+
text="<html><body><h1>Personal Home Page</h1></body></html>",
28+
status_code=200
29+
)
30+
assert "fill this in" in release_notes_changes_section(TEST_GARDENLINUX_RELEASE), \
31+
"Expected a placeholder message to be generated if GVLD response is not valid"
32+
33+
34+
def test_release_notes_compare_package_versions_section_semver_is_not_recognized():
35+
assert release_notes_compare_package_versions_section("1.2.0", []) == "", "Semver is not supported"
36+
37+
38+
def test_release_notes_compare_package_versions_section_unrecognizable_version():
39+
assert release_notes_compare_package_versions_section("garden.linux", []) == ""

0 commit comments

Comments
 (0)