| 1 | import importlib.util |
| 2 | import subprocess |
| 3 | import sys |
| 4 | from pathlib import Path |
| 5 | |
| 6 | |
| 7 | PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| 8 | MODULE_PATH = PROJECT_ROOT / ".github" / "scripts" / "docker_release_plan.py" |
| 9 | |
| 10 | |
| 11 | def load_module(): |
| 12 | spec = importlib.util.spec_from_file_location("docker_release_plan", MODULE_PATH) |
| 13 | module = importlib.util.module_from_spec(spec) |
| 14 | assert spec.loader is not None |
| 15 | sys.modules[spec.name] = module |
| 16 | spec.loader.exec_module(module) |
| 17 | return module |
| 18 | |
| 19 | |
| 20 | def git(repo: Path, *args: str) -> str: |
| 21 | result = subprocess.run( |
| 22 | ["git", *args], |
| 23 | cwd=repo, |
| 24 | check=True, |
| 25 | capture_output=True, |
| 26 | text=True, |
| 27 | ) |
| 28 | return result.stdout.strip() |
| 29 | |
| 30 | |
| 31 | def commit_file(repo: Path, name: str, content: str, message: str) -> str: |
| 32 | (repo / name).write_text(content, encoding="utf-8") |
| 33 | git(repo, "add", name) |
| 34 | git(repo, "commit", "-m", message) |
| 35 | return git(repo, "rev-parse", "HEAD") |
| 36 | |
| 37 | |
| 38 | def seed_remote_refs(repo: Path, *branches: str) -> None: |
| 39 | for branch in branches: |
| 40 | git(repo, "update-ref", f"refs/remotes/origin/{branch}", git(repo, "rev-parse", branch)) |
| 41 | |
| 42 | |
| 43 | def test_docker_publish_workflow_tracks_branch_promotions(): |
| 44 | workflow_path = PROJECT_ROOT / ".github" / "workflows" / "docker-publish.yml" |
| 45 | content = workflow_path.read_text(encoding="utf-8") |
| 46 | |
| 47 | assert 'branches:\n - "testing"\n - "main"' in content |
| 48 | assert 'tags:\n - "v*"' in content |
| 49 | assert "workflow_dispatch:" in content |
| 50 | assert "inputs:" in content |
| 51 | assert "tag:" in content |
| 52 | assert 'ref: ${{ matrix.source_tag }}' in content |
| 53 | assert "SOURCE_REF_TYPE: ${{ github.ref_type }}" in content |
| 54 | assert "BEFORE_SHA: ${{ github.event_name == 'push' && github.event.before || '' }}" in content |
| 55 | |
| 56 | |
| 57 | def test_plan_branch_push_builds_when_tag_reaches_allowed_branch(monkeypatch, tmp_path: Path): |
| 58 | release_plan = load_module() |
| 59 | |
| 60 | git(tmp_path, "init", "-b", "main") |
| 61 | git(tmp_path, "config", "user.name", "Test User") |
| 62 | git(tmp_path, "config", "user.email", "test@example.com") |
| 63 | |
| 64 | commit_file(tmp_path, "README.md", "base\n", "base") |
| 65 | git(tmp_path, "tag", "v1.6") |
| 66 | git(tmp_path, "branch", "testing") |
| 67 | |
| 68 | git(tmp_path, "checkout", "-b", "development") |
| 69 | git(tmp_path, "checkout", "main") |
| 70 | git(tmp_path, "merge", "--ff-only", "development") |
| 71 | |
| 72 | git(tmp_path, "checkout", "development") |
| 73 | commit_file(tmp_path, "feature.txt", "release\n", "release v1.7") |
| 74 | git(tmp_path, "tag", "v1.7") |
| 75 | |
| 76 | testing_before = git(tmp_path, "rev-parse", "testing") |
| 77 | git(tmp_path, "checkout", "testing") |
| 78 | git(tmp_path, "merge", "--no-ff", "development", "-m", "promote v1.7 to testing") |
| 79 | |
| 80 | git(tmp_path, "checkout", "main") |
| 81 | git(tmp_path, "merge", "--no-ff", "development", "-m", "promote v1.7 to main") |
| 82 | seed_remote_refs(tmp_path, "testing", "main") |
| 83 | |
| 84 | monkeypatch.chdir(tmp_path) |
| 85 | monkeypatch.setenv("ALLOWED_BRANCHES", "testing main") |
| 86 | monkeypatch.setenv("MAIN_BRANCH", "main") |
| 87 | monkeypatch.setenv("DOCKER_IMAGE_REPO", "example/agent-zero") |
| 88 | monkeypatch.setenv("RELEASE_TAG_REGEX", r"^v([0-9]+)\.([0-9]+)$") |
| 89 | monkeypatch.setenv("MIN_RELEASE_MAJOR", "1") |
| 90 | monkeypatch.setenv("MIN_RELEASE_MINOR", "0") |
| 91 | monkeypatch.setenv("EVENT_NAME", "push") |
| 92 | monkeypatch.setenv("SOURCE_REF_TYPE", "branch") |
| 93 | monkeypatch.setenv("MANUAL_TAG", "") |
| 94 | monkeypatch.setenv("AFTER_SHA", git(tmp_path, "rev-parse", "testing")) |
| 95 | |
| 96 | monkeypatch.setenv("SOURCE_REF_NAME", "testing") |
| 97 | monkeypatch.setenv("BEFORE_SHA", testing_before) |
| 98 | config = release_plan.load_config() |
| 99 | branch_states = release_plan.collect_branch_states(config) |
| 100 | testing_candidates, testing_notes = release_plan.plan_branch_push(config, branch_states) |
| 101 | |
| 102 | assert testing_notes == [] |
| 103 | assert len(testing_candidates) == 1 |
| 104 | assert testing_candidates[0].branch == "testing" |
| 105 | assert testing_candidates[0].source_tag == "v1.7" |
| 106 | assert testing_candidates[0].mode == "push_promoted_tag" |
| 107 | assert testing_candidates[0].publish_version is False |
| 108 | assert testing_candidates[0].publish_branch_tag is True |
| 109 | |
| 110 | monkeypatch.setenv("SOURCE_REF_NAME", "main") |
| 111 | monkeypatch.setenv("BEFORE_SHA", git(tmp_path, "rev-list", "--max-parents=0", "HEAD")) |
| 112 | monkeypatch.setenv("AFTER_SHA", git(tmp_path, "rev-parse", "main")) |
| 113 | config = release_plan.load_config() |
| 114 | branch_states = release_plan.collect_branch_states(config) |
| 115 | main_candidates, main_notes = release_plan.plan_branch_push(config, branch_states) |
| 116 | |
| 117 | assert main_notes == [] |
| 118 | assert len(main_candidates) == 1 |
| 119 | assert main_candidates[0].branch == "main" |
| 120 | assert main_candidates[0].source_tag == "v1.7" |
| 121 | assert main_candidates[0].mode == "push_promoted_tag" |
| 122 | assert main_candidates[0].publish_version is True |
| 123 | assert main_candidates[0].publish_branch_tag is True |