feat: bump moves here, and verify-base takes images by name
CI / Tests, lint (push) Successful in 12s
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.
This commit is contained in:
1 parent
9232b54c6d
commit
ae39af4665
6 files changed
+459
-8
No files matched your search
+22
-2
@@ -17,8 +17,10 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
ACTIONS = sorted((ROOT / "actions").glob("*/action.yml"))
|
||||
|
||||
|
||||
def test_the_four_actions():
|
||||
assert [a.parent.name for a in ACTIONS] == ["judge", "sign", "verdict", "verify-base"]
|
||||
def test_the_actions():
|
||||
names = [a.parent.name for a in ACTIONS]
|
||||
|
||||
assert names == ["bump", "judge", "sign", "verdict", "verify-base"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action", ACTIONS, ids=lambda a: a.parent.name)
|
||||
@@ -46,3 +48,21 @@ def test_a_verdict_that_could_not_be_reached_fails_the_step():
|
||||
assert spec["outputs"]["improved"]["value"] == "${{ steps.verdict.outputs.improved }}"
|
||||
assert "['error']" in step["run"] and "sys.exit(" in step["run"]
|
||||
assert "cat report.md" in step["run"].splitlines()
|
||||
|
||||
|
||||
def test_bumping_needs_its_token_from_the_caller():
|
||||
text = (ROOT / "actions/bump/action.yml").read_text()
|
||||
|
||||
assert "BUMP_TOKEN" in text and "secrets." not in text
|
||||
|
||||
|
||||
def test_a_base_can_be_named_where_the_dockerfile_cannot():
|
||||
"""base-python builds FROM ${BASE_OS}, an argument the nightly sets, and copies
|
||||
CPython from Docker's python:3.12-slim, which is not signed with netOrk's key.
|
||||
It names the one image to verify instead."""
|
||||
spec = yaml.safe_load((ROOT / "actions/verify-base/action.yml").read_text())
|
||||
runs = "\n".join(step["run"] for step in spec["runs"]["steps"])
|
||||
|
||||
assert not spec["inputs"]["dockerfiles"].get("required")
|
||||
assert "refs" in spec["inputs"]
|
||||
assert "bin/verify-signature.sh" in runs and "bin/verify-base.sh" in runs
|
||||
@@ -0,0 +1,167 @@
|
||||
"""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"]
|
||||
Reference in new issue
Block a user