import os
import unittest
from pathlib import Path
from unittest.mock import patch
import github_release as release
TAG = "v0.4.0"
SHA = "a" * 40
MANIFEST = {"archive": {"sha256": "b" * 64, "size": 123}}
ASSET = {"name": "archive", "digest": "sha256:" + "b" * 64, "size": 123}
class GitHubReleaseTests(unittest.TestCase):
@patch.object(release, "api")
def test_retagging_is_never_attempted(self, api):
api.side_effect = [{"object": {"type": "commit", "sha": "c" * 40}}]
with self.assertRaises(ValueError):
release.draft(TAG, SHA)
self.assertTrue(all(len(call.args) == 1 for call in api.call_args_list))
@patch.object(release, "api")
def test_new_release_starts_as_source_locked_draft(self, api):
api.side_effect = [None, [], None, {"sha": "d" * 40}, {}, {}]
with patch.object(
release, "build_release_body", return_value=f"Notes for {SHA}"
) as notes:
release.draft(TAG, SHA)
notes.assert_called_once_with(TAG, SHA, api)
payload = api.call_args_list[-1].args[1]
self.assertTrue(payload["draft"])
self.assertIn(SHA, payload["body"])
self.assertEqual(payload["target_commitish"], SHA)
def test_notes_failure_does_not_create_any_tag_or_release(self):
with (
patch.object(release, "tag_commit", return_value=None),
patch.object(release, "find_release", return_value=None),
patch.object(release, "require_newer"),
patch.object(
release,
"build_release_body",
side_effect=RuntimeError("notes unavailable"),
),
patch.object(release, "api") as api,
):
with self.assertRaises(RuntimeError):
release.draft(TAG, SHA)
api.assert_not_called()
@patch.object(release, "api")
def test_authenticated_listing_finds_existing_draft(self, api):
draft = {"tag_name": TAG, "draft": True}
api.return_value = [{"tag_name": "v0.3.18", "draft": False}, draft]
self.assertEqual(release.find_release(TAG), draft)
api.assert_called_once_with("releases?per_page=100&page=1")
def test_conflicting_existing_asset_is_rejected(self):
with self.assertRaises(ValueError):
release.verify_remote({"assets": [{**ASSET, "size": 0}]}, MANIFEST, True)
@patch.object(release.subprocess, "run")
@patch.object(release, "validate", return_value=MANIFEST)
@patch.object(release, "release_for", return_value={"draft": True, "assets": []})
def test_missing_asset_prevents_publish(self, source, validate, run):
with self.assertRaises(ValueError):
release.publish(TAG, SHA, Path("unused"))
run.assert_not_called()
@patch.object(release.subprocess, "run")
@patch.object(release, "validate", return_value=MANIFEST)
@patch.object(
release, "release_for", return_value={"draft": True, "assets": [ASSET]}
)
def test_retry_does_not_overwrite_identical_assets(self, source, validate, run):
release.upload(TAG, SHA, Path("unused"))
run.assert_not_called()
@patch.object(release.subprocess, "run")
@patch.object(release, "validate", return_value=MANIFEST)
@patch.object(
release, "release_for", return_value={"draft": False, "assets": [ASSET]}
)
def test_repeat_publish_of_identical_owned_release_is_noop(
self, source, validate, run
):
release.publish(TAG, SHA, Path("unused"))
run.assert_not_called()
@patch.object(release, "api", return_value={"tag_name": "v0.4.0"})
def test_old_or_equal_versions_cannot_replace_latest(self, api):
for tag in ["v0.3.18", "v0.4.0"]:
with self.subTest(tag=tag), self.assertRaises(ValueError):
release.require_newer(tag)
@patch.object(release.subprocess, "run")
@patch.object(release, "require_newer")
@patch.object(release, "validate", return_value=MANIFEST)
@patch.object(release, "tag_commit", return_value=SHA)
def test_full_failed_job_retry_after_publication_is_noop(
self, commit, validate, newer, run
):
published = {
"draft": False,
"assets": [ASSET],
"body": f"<!-- mitts-release-source:{SHA} -->",
}
with patch.object(release, "find_release", return_value=published):
release.draft(TAG, SHA)
release.upload(TAG, SHA, Path("unused"))
release.publish(TAG, SHA, Path("unused"))
newer.assert_not_called()
run.assert_not_called()
@patch.dict(
os.environ,
{"GITHUB_REF": "refs/heads/feature", "GITHUB_REPOSITORY": release.REPO},
)
@patch("sys.argv", ["github_release.py", "draft", TAG, SHA])
def test_cli_does_not_allow_writes_from_feature_branch(self):
with self.assertRaises(ValueError):
release.main()