Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ python-dotenv = "^1.1.1"
pytest = "^8.4.1"
pytest-cov = "^6.2.1"
isort = "^6.0.1"
requests-mock = "^1.12.1"

[tool.poetry.group.docs.dependencies]
sphinx-rtd-theme = "^3.0.2"
Expand Down
28 changes: 28 additions & 0 deletions src/gardenlinux/github/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import argparse

from .release import upload_to_github_release_page


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

upload_parser = subparsers.add_parser("upload")
upload_parser.add_argument("--owner", default="gardenlinux")
upload_parser.add_argument("--repo", default="gardenlinux")
upload_parser.add_argument("--release_id", required=True)
upload_parser.add_argument("--file_path", required=True)
upload_parser.add_argument("--dry-run", action="store_true", default=False)

args = parser.parse_args()

if args.command == "upload":
upload_to_github_release_page(
args.owner, args.repo, args.release_id, args.file_path, args.dry_run
)
else:
parser.print_help()


if __name__ == "__main__":
main()
46 changes: 46 additions & 0 deletions src/gardenlinux/github/release/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os

import requests

from gardenlinux.logger import LoggerSetup

LOGGER = LoggerSetup.get_logger("gardenlinux.github", "INFO")

REQUESTS_TIMEOUTS = (5, 30) # connect, read


def upload_to_github_release_page(
github_owner, github_repo, gardenlinux_release_id, file_to_upload, dry_run
):
if dry_run:
LOGGER.info(
f"Dry run: would upload {file_to_upload} to release {gardenlinux_release_id} in repo {github_owner}/{github_repo}"
)
return

token = os.environ.get("GITHUB_TOKEN")
if not token:
raise ValueError("GITHUB_TOKEN environment variable not set")

headers = {
"Authorization": f"token {token}",
"Content-Type": "application/octet-stream",
}

upload_url = f"https://uploads.github.com/repos/{github_owner}/{github_repo}/releases/{gardenlinux_release_id}/assets?name={os.path.basename(file_to_upload)}"

try:
with open(file_to_upload, "rb") as f:
file_contents = f.read()
except IOError as e:
LOGGER.error(f"Error reading file {file_to_upload}: {e}")
return

response = requests.post(upload_url, headers=headers, data=file_contents, timeout=REQUESTS_TIMEOUTS)
if response.status_code == 201:
LOGGER.info("Upload successful")
else:
LOGGER.error(
f"Upload failed with status code {response.status_code}: {response.text}"
)
response.raise_for_status()
7 changes: 7 additions & 0 deletions tests/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-

import os
from pathlib import Path

from gardenlinux.git import Repository

TEST_DATA_DIR = "test-data"
Expand All @@ -20,3 +23,7 @@
TEST_COMMIT = Repository(GL_ROOT_DIR).commit_id[:8]
TEST_VERSION = "1000.0"
TEST_VERSION_STABLE = "1000"

TEST_GARDENLINUX_RELEASE = "1877.3"

S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads"
Empty file added tests/github/__init__.py
Empty file.
28 changes: 28 additions & 0 deletions tests/github/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os
import shutil

import pytest

from ..constants import S3_DOWNLOADS_DIR


@pytest.fixture
def downloads_dir():
os.makedirs(S3_DOWNLOADS_DIR, exist_ok=True)
yield
shutil.rmtree(S3_DOWNLOADS_DIR)


@pytest.fixture
def github_token():
os.environ["GITHUB_TOKEN"] = "foobarbazquux"
yield
del os.environ["GITHUB_TOKEN"]


@pytest.fixture
def artifact_for_upload(downloads_dir):
artifact = S3_DOWNLOADS_DIR / "artifact.log"
artifact.touch()
yield artifact
artifact.unlink()
117 changes: 117 additions & 0 deletions tests/github/test_upload_to_github_release_page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import sys

import pytest
import requests
import requests_mock

import gardenlinux.github.__main__ as gh
from gardenlinux.github.release import upload_to_github_release_page

from ..constants import TEST_GARDENLINUX_RELEASE


def test_upload_to_github_release_page_dryrun(caplog, artifact_for_upload):
with requests_mock.Mocker():
assert upload_to_github_release_page(
"gardenlinux",
"gardenlinux",
TEST_GARDENLINUX_RELEASE,
artifact_for_upload,
dry_run=True) is None
assert any("Dry run: would upload" in record.message for record in caplog.records), "Expected a dry‑run log entry"


def test_upload_to_github_release_page_needs_github_token(downloads_dir, artifact_for_upload):
with requests_mock.Mocker():
with pytest.raises(ValueError) as exn:
upload_to_github_release_page(
"gardenlinux",
"gardenlinux",
TEST_GARDENLINUX_RELEASE,
artifact_for_upload,
dry_run=False)
assert str(exn.value) == "GITHUB_TOKEN environment variable not set", \
"Expected an exception to be raised on missing GITHUB_TOKEN environment variable"


def test_upload_to_github_release_page(downloads_dir, caplog, github_token, artifact_for_upload):
with requests_mock.Mocker(real_http=True) as m:
m.post(
f"https://uploads.github.com/repos/gardenlinux/gardenlinux/releases/{TEST_GARDENLINUX_RELEASE}/assets?name=artifact.log",
text="{}",
status_code=201
)

upload_to_github_release_page(
"gardenlinux",
"gardenlinux",
TEST_GARDENLINUX_RELEASE,
artifact_for_upload,
dry_run=False)
assert any("Upload successful" in record.message for record in caplog.records), \
"Expected an upload confirmation log entry"


def test_upload_to_github_release_page_unreadable_artifact(downloads_dir, caplog, github_token, artifact_for_upload):
artifact_for_upload.chmod(0)

upload_to_github_release_page(
"gardenlinux",
"gardenlinux",
TEST_GARDENLINUX_RELEASE,
artifact_for_upload,
dry_run=False)
assert any("Error reading file" in record.message for record in caplog.records), \
"Expected an error message log entry"


def test_upload_to_github_release_page_failed(downloads_dir, caplog, github_token, artifact_for_upload):
with requests_mock.Mocker(real_http=True) as m:
m.post(
f"https://uploads.github.com/repos/gardenlinux/gardenlinux/releases/{TEST_GARDENLINUX_RELEASE}/assets?name=artifact.log",
text="{}",
status_code=503
)

with pytest.raises(requests.exceptions.HTTPError):
upload_to_github_release_page(
"gardenlinux",
"gardenlinux",
TEST_GARDENLINUX_RELEASE,
artifact_for_upload,
dry_run=False)
assert any("Upload failed with status code 503:" in record.message for record in caplog.records), \
"Expected an error HTTP status code to be logged"


def test_script_parse_args_wrong_command(monkeypatch, capfd):
monkeypatch.setattr(sys, "argv", ["gh", "rejoice"])

with pytest.raises(SystemExit):
gh.main()
captured = capfd.readouterr()

assert "argument command: invalid choice: 'rejoice'" in captured.err, "Expected help message printed"


def test_script_parse_args_upload_command_required_args(monkeypatch, capfd):
monkeypatch.setattr(sys, "argv", ["gh", "upload", "--owner", "gardenlinux", "--repo", "gardenlinux"])

with pytest.raises(SystemExit):
gh.main()
captured = capfd.readouterr()

assert "the following arguments are required: --release_id, --file_path" in captured.err, \
"Expected help message on missing arguments for 'upload' command"


def test_script_upload_dry_run(monkeypatch, capfd):
monkeypatch.setattr(sys, "argv", ["gh", "upload", "--owner", "gardenlinux", "--repo",
"gardenlinux", "--release_id", TEST_GARDENLINUX_RELEASE, "--file_path", "foo", "--dry-run"])
monkeypatch.setattr("gardenlinux.github.__main__.upload_to_github_release_page",
lambda a1, a2, a3, a4, dry_run: print(f"dry-run: {dry_run}"))

gh.main()
captured = capfd.readouterr()

assert captured.out == "dry-run: True\n"