import copy import json import tempfile import unittest from pathlib import Path from unittest.mock import patch import resolve_python_cli_release as resolver class ResolvePythonCliReleaseTest(unittest.TestCase): def setUp(self) -> None: self.revision = "a" * 40 self.run = { "id": 123, "path": ".github/workflows/rust-release.yml", "event": "push", "status": "completed", "conclusion": "success", "head_repository": {"full_name": "openai/codex"}, "head_branch": "rust-v1.2.3", "head_sha": self.revision, } self.job = { "id": 456, "run_attempt": 1, "name": "release", "conclusion": "success", } self.commit = {"type": "commit", "sha": self.revision} self.release = { "id": 789, "draft": False, "prerelease": False, "tag_name": "rust-v1.2.3", } self.assets = [ {"name": name, "state": "uploaded", "size": 100} for name in [ "openai_codex_cli_bin-1.2.3-py3-none-macosx_10_9_x86_64.whl", "openai_codex_cli_bin-1.2.3-py3-none-macosx_11_0_arm64.whl", "openai_codex_cli_bin-1.2.3-py3-none-manylinux_2_17_aarch64.whl", "openai_codex_cli_bin-1.2.3-py3-none-manylinux_2_17_x86_64.whl", "openai_codex_cli_bin-1.2.3-py3-none-win_amd64.whl", "openai_codex_cli_bin-1.2.3-py3-none-win_arm64.whl", "codex-package-aarch64-unknown-linux-musl.tar.gz", "codex-package-x86_64-unknown-linux-musl.tar.gz", ] ] self.expected = { "source_sha": self.revision, "release_tag": "rust-v1.2.3", "version": "1.2.3", } def responses(self) -> list: return copy.deepcopy( [ self.run, {"jobs": [self.job]}, {"object": self.commit}, self.release, self.assets, ] ) def test_resolves_lightweight_and_annotated_tags_to_the_run_revision(self) -> None: for annotated in (False, True): with self.subTest(annotated=annotated): responses = self.responses() if annotated: responses.insert(2, {"object": {"type": "tag", "sha": "b" * 40}}) with patch.object(resolver, "github_api", side_effect=responses): self.assertEqual( resolver.resolve_release("openai/codex", "123"), self.expected ) def test_skips_cli_prereleases_before_reading_jobs_or_assets(self) -> None: for suffix in ("-alpha", "-alpha.1", "-alpha.1.2", "-beta", "-beta.1"): with self.subTest(suffix=suffix): run = {**self.run, "head_branch": f"rust-v1.2.3{suffix}"} with patch.object(resolver, "github_api", return_value=run) as api: self.assertIsNone(resolver.resolve_release("openai/codex", "123")) api.assert_called_once_with("repos/openai/codex/actions/runs/123") def test_rejects_unrelated_or_incomplete_runs(self) -> None: for field, value in ( ("id", 999), ("path", ".github/workflows/sdk.yml"), ("event", "pull_request"), ("status", "in_progress"), ("head_branch", "main"), ("head_sha", "main"), ("head_repository", {"full_name": "other/codex"}), ): with self.subTest(field=field, value=value): with patch.object( resolver, "github_api", return_value={**self.run, field: value} ): with self.assertRaises(ValueError): resolver.resolve_release("openai/codex", "123") def test_accepts_failed_ancillary_publisher_and_its_partial_rerun(self) -> None: for overall in ("failure", "cancelled"): with self.subTest(overall=overall): responses = self.responses() responses[0]["conclusion"] = overall responses[1]["jobs"].extend( [ { "id": 999, "run_attempt": 2, "name": "publish-winget", "conclusion": "failure", }, { **self.job, "id": 998, "run_attempt": 2, "conclusion": "skipped", }, ] ) with patch.object(resolver, "github_api", side_effect=responses): self.assertEqual( resolver.resolve_release("openai/codex", "123"), self.expected ) def test_requires_success_from_latest_executed_release_job(self) -> None: for jobs in ( [], [{**self.job, "conclusion": "skipped"}], [{**self.job, "conclusion": "failure"}], [{**self.job, "conclusion": "cancelled"}], [ {**self.job, "id": 999, "run_attempt": 2, "conclusion": "failure"}, self.job, ], ): with self.subTest(jobs=jobs): with patch.object( resolver, "github_api", side_effect=[self.run, {"jobs": jobs}] ): with self.assertRaisesRegex(ValueError, "successful release job"): resolver.resolve_release("openai/codex", "123") def test_reads_all_pages_of_jobs_and_assets(self) -> None: responses = self.responses() responses.insert(1, {"jobs": [{**self.job, "name": "other"}] * 100}) responses.insert(-1, [{"name": "other", "state": "uploaded", "size": 1}] * 100) with patch.object(resolver, "github_api", side_effect=responses) as api: self.assertEqual( resolver.resolve_release("openai/codex", "123"), self.expected ) api.assert_any_call( "repos/openai/codex/actions/runs/123/jobs?filter=all&per_page=100&page=2" ) api.assert_any_call( "repos/openai/codex/releases/789/assets?per_page=100&page=2" ) def test_rejects_a_moved_tag(self) -> None: responses = self.responses() responses[2] = {"object": {"type": "commit", "sha": "b" * 40}} with patch.object(resolver, "github_api", side_effect=responses): with self.assertRaisesRegex(ValueError, "no longer matches"): resolver.resolve_release("openai/codex", "123") def test_bounds_annotated_tag_resolution(self) -> None: tag = {"object": {"type": "tag", "sha": "b" * 40}} with patch.object( resolver, "github_api", side_effect=[self.run, {"jobs": [self.job]}, *([tag] * 9)], ) as api: with self.assertRaisesRegex(ValueError, "no longer matches"): resolver.resolve_release("openai/codex", "123") self.assertEqual(api.call_count, 11) def test_requires_a_published_stable_release_for_the_tag(self) -> None: for field, value in ( ("draft", True), ("prerelease", True), ("tag_name", "rust-v9.9.9"), ): responses = self.responses() responses[3][field] = value with self.subTest(field=field): with patch.object(resolver, "github_api", side_effect=responses): with self.assertRaisesRegex(ValueError, "published and stable"): resolver.resolve_release("openai/codex", "123") def test_requires_all_runtime_inputs_to_be_uploaded_and_nonempty(self) -> None: for assets in ( self.assets[:-1], [*self.assets[:-1], {**self.assets[-1], "size": 0}], [*self.assets[:-1], {**self.assets[-1], "state": "new"}], ): with self.subTest(assets=assets): responses = self.responses() responses[-1] = assets with patch.object(resolver, "github_api", side_effect=responses): with self.assertRaisesRegex(ValueError, "missing Python inputs"): resolver.resolve_release("openai/codex", "123") def test_rejects_invalid_inputs_before_github_access(self) -> None: with patch.object(resolver, "github_api") as api: for repository, run_id in ( ("other/codex", "123"), ("openai/codex", "../123"), ): with self.assertRaises(ValueError): resolver.resolve_release(repository, run_id) api.assert_not_called() def test_event_and_manual_retry_emit_the_same_revision_and_version(self) -> None: with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "output" event = Path(directory) / "event.json" for automatic in (True, False): output.write_text("") event.write_text( json.dumps({"workflow_run": self.run} if automatic else {}) ) responses = self.responses()[1:] if automatic else self.responses() with patch.object(resolver, "github_api", side_effect=responses) as api: resolver.main( [ "123", "--repository", "openai/codex", "--event-path", str(event), "--github-output", str(output), ] ) self.assertEqual( output.read_text(), f"publish=true\nsource_sha={self.revision}\nrelease_tag=rust-v1.2.3\nversion=1.2.3\n", ) if automatic: self.assertNotIn( unittest.mock.call("repos/openai/codex/actions/runs/123"), api.call_args_list, ) if __name__ == "__main__": unittest.main()