CI / Tests, lint (push) Successful in 12s
- image_tools/bump.py from base-os, with an action. base-python used it from
a pinned checkout of base-os, with the bot's token; one place serves both.
A dependent whose workflow does not exist (netork's ui-nightly.yml, on
2026-10-22) is named and the others go on; the run fails at the end, as
for a repository the token cannot see. bump_all holds that loop, tested.
- verify-base takes refs besides Dockerfiles: base-python builds FROM
${BASE_OS}, which its nightly sets, and copies CPython from Docker's
python:3.12-slim, which is not signed with netOrk's key.
168 lines
6.7 KiB
Python
168 lines
6.7 KiB
Python
"""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/<owner>/<repo>
|
|
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"]
|