"""Moving a pin in the repos that use an image (#1308). When an image improves, its repo opens (or updates) one pull request in every repo that pins it, rewriting only the pin, and starts that repo's own nightly run, which rebuilds on the new pin and verifies itself. """ from __future__ import annotations import base64 from image_tools.bump import Dependent, bump_all, load_dependents, open_or_update_pr, rewrite_pin OLD = "registry.netork.io/netork/base-os:13-20261001.1@sha256:" + "a" * 64 NEW = "registry.netork.io/netork/base-os:13-20261020.1@sha256:" + "b" * 64 class TestRewritePin: def test_every_from_line_moves(self): text = f"FROM {OLD} AS build\nRUN x\nFROM {OLD}\n" assert ( rewrite_pin(text, "netork/base-os", NEW) == f"FROM {NEW} AS build\nRUN x\nFROM {NEW}\n" ) def test_a_compose_image_line_moves(self): text = f"services:\n cache:\n image: {OLD}\n" assert ( rewrite_pin(text, "netork/base-os", NEW) == f"services:\n cache:\n image: {NEW}\n" ) def test_another_image_stays(self): other = "registry.netork.io/netork/base-python:3.12.15-13-20261001.1@sha256:" + "c" * 64 text = f"FROM {other}\n" assert rewrite_pin(text, "netork/base-os", NEW) == text def test_an_image_whose_name_only_starts_alike_stays(self): text = "FROM registry.netork.io/netork/base-os-extra:1@sha256:" + "d" * 64 + "\n" assert rewrite_pin(text, "netork/base-os", NEW) == text class FakeGitea: def __init__(self, files: dict[str, str], prs: list[dict] | None = None, branches=()): self.files = dict(files) self.prs = list(prs or []) self.branches = set(branches) self.calls: list[tuple[str, str]] = [] self.missing_workflows: set[str] = set() self.dispatched: list[str] = [] def __call__(self, method: str, path: str, body: dict | None = None): self.calls.append((method, path)) if method == "GET" and path.count("/") == 3: # /repos// return {"full_name": path.split("/repos/", 1)[1]} if method == "GET" and "/contents/" in path: name = path.split("/contents/", 1)[1].split("?", 1)[0] content = base64.b64encode(self.files[name].encode()).decode() return {"content": content, "sha": f"sha-{name}"} if method == "PUT" and "/contents/" in path: name = path.split("/contents/", 1)[1] self.files[name] = base64.b64decode(body["content"]).decode() return {} if method == "POST" and path.endswith("/branches"): self.branches.add(body["new_branch_name"]) return {} if method == "GET" and "/branches/" in path: name = path.rsplit("/branches/", 1)[1] if name not in self.branches: raise FileNotFoundError(name) return {} if method == "GET" and "/pulls?" in path: return [p for p in self.prs if p["state"] == "open"] if method == "POST" and path.endswith("/pulls"): pr = {"number": 7, "state": "open", "head": {"ref": body["head"]}, "body": body["body"]} self.prs.append(pr) return pr if method == "POST" and path.endswith("/dispatches"): workflow = path.split("/workflows/", 1)[1].split("/", 1)[0] if workflow in self.missing_workflows: raise FileNotFoundError(path) self.dispatched.append(workflow) return {} if method == "PATCH" and "/pulls/" in path: self.prs[0]["body"] = body["body"] return self.prs[0] raise AssertionError(f"unexpected {method} {path}") DEP = Dependent(repo="NetOrk/base-python", files=("Dockerfile",), workflow="nightly.yml") class TestPullRequest: def test_it_opens_one_pr_with_only_the_pin_changed(self): api = FakeGitea({"Dockerfile": f"FROM {OLD}\nRUN echo hi\n"}) pr = open_or_update_pr(api, DEP, "netork/base-os", NEW, "13-20261020.1", "report") assert pr["number"] == 7 assert api.files["Dockerfile"] == f"FROM {NEW}\nRUN echo hi\n" assert "chore/base-os-13-20261020.1" in api.branches assert api.prs[0]["body"] == "report" def test_a_second_run_updates_the_open_pr(self): branch = "chore/base-os-13-20261020.1" api = FakeGitea( {"Dockerfile": f"FROM {NEW}\n"}, prs=[{"number": 7, "state": "open", "head": {"ref": branch}, "body": "old"}], branches={branch}, ) open_or_update_pr(api, DEP, "netork/base-os", NEW, "13-20261020.1", "new report") assert len(api.prs) == 1 assert api.prs[0]["body"] == "new report" assert not [c for c in api.calls if c[0] == "POST" and c[1].endswith("/branches")] def test_a_file_without_the_pin_is_left_alone(self): api = FakeGitea({"Dockerfile": "FROM something-else\n"}) assert open_or_update_pr(api, DEP, "netork/base-os", NEW, "13-20261020.1", "r") is None assert not [c for c in api.calls if c[0] in ("PUT", "POST")] def test_the_dependents_are_listed_in_the_repo(tmp_path): path = tmp_path / "dependents.toml" path.write_text( '[[dependent]]\nrepo = "NetOrk/netork"\nfiles = ["docker/ui/Dockerfile"]\n' 'workflow = "ui-nightly.yml"\n' ) assert load_dependents(path) == [ Dependent("NetOrk/netork", ("docker/ui/Dockerfile",), "ui-nightly.yml") ] class TestEveryDependent: """2026-10-22: netork's dependents entry named ui-nightly.yml, which does not exist yet; the dispatch's 404 ended the run with a traceback after the last pull request. A dependent that cannot be started is named, the others go on, and the run fails at the end, as for a repo the token cannot see.""" DEPS = [ Dependent(repo="NetOrk/netork", files=("Dockerfile",), workflow="ui-nightly.yml"), Dependent(repo="NetOrk/valkey", files=("Dockerfile",), workflow="nightly.yml"), ] def test_a_missing_workflow_is_named_and_the_rest_goes_on(self): api = FakeGitea({"Dockerfile": f"FROM {OLD}\n"}) api.missing_workflows = {"ui-nightly.yml"} lines: list[str] = [] ok = bump_all(api, self.DEPS, "netork/base-os", NEW, "13-20261020.1", "r", lines.append) assert not ok assert api.dispatched == ["nightly.yml"] assert any("NetOrk/netork" in ln and "ui-nightly.yml" in ln for ln in lines) assert any(ln.startswith("NetOrk/valkey: PR #7") for ln in lines) def test_all_well_is_ok(self): api = FakeGitea({"Dockerfile": f"FROM {OLD}\n"}) assert bump_all(api, self.DEPS[1:], "netork/base-os", NEW, "13-20261020.1", "r", print) assert api.dispatched == ["nightly.yml"]