"""RC publication must retain the GA boundary and fail before any upload."""
import functools
import http.server
import tempfile
import threading
import unittest
from pathlib import Path
import release_gate
from release_gate import GateError
import test_release_gate
RC = "4.1.0-RC.1"
TAG_SHA = "b" * 40
RELEASE_SHA = "a" * 40
class QuietHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
pass
class ReleaseCandidateTest(unittest.TestCase):
def setUp(self):
self.fixture = test_release_gate.ReleaseGateTest()
self.fixture.setUp()
self.addCleanup(self.fixture.doCleanups)
self.root = self.fixture.root
def test_exact_rc_and_ga_are_allowed_but_other_candidates_are_not(self):
for version in ("4.1.0", RC):
self.fixture.write_poms(version, version, "1.2.3-RC.1")
release_gate.validate_poms(self.root, version)
for version in ("4.1.0-RC.2", "4.1.0-SNAPSHOT", "4.1.1", "LATEST", ""):
with self.subTest(version=version), self.assertRaises(GateError):
self.fixture.write_poms(version, version, "1.2.3")
release_gate.validate_poms(self.root, version)
def test_rc_rejects_wrong_framework_pin_and_mutable_dependencies(self):
for pin in ("4.1.0", "4.1.0-RC.2", "4.1.0-SNAPSHOT"):
self.fixture.write_poms(RC, pin, "1.2.3")
with self.assertRaises(GateError):
release_gate.validate_poms(self.root, RC)
for pin in ("LATEST", "RELEASE", "[1.0,2.0)", "1.2.3-SNAPSHOT", "${unknown}"):
self.fixture.write_poms(RC, RC, pin)
with self.subTest(pin=pin), self.assertRaises(GateError):
release_gate.validate_poms(self.root, RC)
self.fixture.write_poms(RC, RC, "1.2.3")
bom = self.root / "qqq-all-bom/pom.xml"
bom.write_text(bom.read_text().replace("1.0.0-RC.8", "LATEST"))
with self.assertRaises(GateError):
release_gate.validate_poms(self.root, RC)
def test_public_rc_distribution_poms_must_match_source_including_next(self):
self.fixture.write_poms(RC, RC, "1.2.3")
parent, bom = self.fixture.published_poms(resolved=True)
for path in (parent, bom):
path.write_text(path.read_text().replace("4.1.0", RC))
release_gate.validate_published_poms(parent, bom, RC, self.root / "qqq-all-bom/pom.xml")
original = bom.read_text()
for changed in (original.replace("1.0.0-RC.8", "1.0.0-RC.9"),
original.replace(RC, "4.1.0-RC.2"),
original.replace("1.2.3", "[1,2)")):
bom.write_text(changed)
with self.assertRaises(GateError):
release_gate.validate_published_poms(parent, bom, RC, self.root / "qqq-all-bom/pom.xml")
def test_upstream_rc_requires_exact_public_prerelease_and_ga_requires_final(self):
for version, prerelease in ((RC, True), ("4.1.0", False)):
release = {"tag_name": "v" + version, "draft": False, "prerelease": prerelease}
release_gate.validate_upstream_release(release, version)
for changed in ({**release, "draft": True}, {**release, "prerelease": not prerelease},
{**release, "tag_name": "v4.1.0-RC.2"}, {}):
with self.subTest(changed=changed), self.assertRaises(GateError):
release_gate.validate_upstream_release(changed, version)
def test_tag_requires_verified_signature_exact_name_object_and_commit(self):
tag = {"sha": TAG_SHA, "tag": RC, "object": {"type": "commit", "sha": RELEASE_SHA},
"verification": {"verified": True, "reason": "valid"}}
release_gate.validate_tag(tag, RC, RELEASE_SHA, TAG_SHA)
mutations = [
{**tag, "tag": "4.1.0"}, {**tag, "sha": "c" * 40},
{**tag, "object": {"type": "commit", "sha": "c" * 40}},
{**tag, "object": {"type": "tag", "sha": RELEASE_SHA}},
{**tag, "verification": {"verified": False, "reason": "unsigned"}},
{**tag, "verification": {"verified": True, "reason": "unknown_key"}}, {},
]
for changed in mutations:
with self.subTest(changed=changed), self.assertRaises(GateError):
release_gate.validate_tag(changed, RC, RELEASE_SHA, TAG_SHA)
def test_public_download_fails_on_missing_or_wrong_coordinates_and_parent(self):
self.fixture.write_poms(RC, RC, "1.2.3")
with tempfile.TemporaryDirectory() as directory:
central = Path(directory)
dependencies = [("com.kingsrook.qqq", "qqq-bom-pom", RC, "pom"),
("com.kingsrook.qqq", "qqq-frontend-next", "1.0.0-RC.8", "jar")]
dependencies += [("com.kingsrook.qbits", "qbit-" + name, "1.2.3", "jar")
for name in release_gate.QBITS]
for group, artifact, version, kind in dependencies:
folder = central / group.replace(".", "/") / artifact / version
folder.mkdir(parents=True)
stem = folder / (artifact + "-" + version)
Path(str(stem) + ".pom").write_text(
f"{group}{artifact}"
f"{version}")
if kind == "jar":
Path(str(stem) + ".jar").write_bytes(b"published fixture jar")
handler = functools.partial(QuietHandler, directory=directory)
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
self.addCleanup(server.server_close)
self.addCleanup(server.shutdown)
base_url = f"http://127.0.0.1:{server.server_port}"
dest = self.root / "public"
bom = self.root / "qqq-all-bom/pom.xml"
receipts = release_gate.download_public_inputs(bom, RC, dest, base_url)
self.assertEqual(19, len(receipts))
self.assertTrue(all(len(row["sha256"]) == 64 for row in receipts))
public_pom = central / "com/kingsrook/qqq/qqq-frontend-next/1.0.0-RC.8/qqq-frontend-next-1.0.0-RC.8.pom"
original = public_pom.read_text()
public_pom.write_text(original.replace("1.0.0-RC.8", "1.0.0-RC.9"))
with self.assertRaises(GateError):
release_gate.download_public_inputs(bom, RC, dest, base_url)
public_pom.write_text(original.replace("", "com.kingsrook"
"qbit-build-parentLATEST"))
with self.assertRaises(GateError):
release_gate.download_public_inputs(bom, RC, dest, base_url)
public_pom.write_text(original.replace("", "com.kingsrook"
"qbit-build-parent2.1.0-RC.1"))
parent = central / "com/kingsrook/qbit-build-parent/2.1.0-RC.1/qbit-build-parent-2.1.0-RC.1.pom"
parent.parent.mkdir(parents=True)
parent.write_text("com.kingsrookqbit-build-parent"
"2.1.0-RC.1"
"com.kingsrook.qqqqqq-bom-pom"
"4.0.0pomimport"
"")
with self.assertRaises(GateError):
release_gate.download_public_inputs(bom, RC, dest, base_url)
parent.write_text(parent.read_text().replace("4.0.0", RC))
self.assertEqual(20, len(release_gate.download_public_inputs(bom, RC, dest, base_url)))
public_pom.unlink()
with self.assertRaises(GateError):
release_gate.download_public_inputs(bom, RC, dest, base_url)
if __name__ == "__main__":
unittest.main()