Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions src/aio_fleet/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from aio_fleet.manifest import RepoConfig
from aio_fleet.release import (
find_release_target_commit,
git,
git_is_ancestor,
latest_changelog_version,
read_upstream_version,
)
Expand Down Expand Up @@ -102,7 +104,7 @@ def _is_dockerhub_tag(tag: str) -> bool:
return first in {"docker.io", "index.docker.io"} or "." not in first


def _verify_dockerhub_tag(tag: str, *, attempts: int = 3) -> str | None:
def _verify_dockerhub_tag(tag: str, *, attempts: int = 8) -> str | None:
parsed = _dockerhub_tag_parts(tag)
if parsed is None:
return f"{tag}: unsupported Docker Hub tag format"
Expand All @@ -124,12 +126,15 @@ def _verify_dockerhub_tag(tag: str, *, attempts: int = 3) -> str | None:
last_error = f"invalid Docker Hub JSON response: {error}"
except urllib.error.HTTPError as error:
if error.code == 404:
return f"{tag}: tag not found on Docker Hub"
last_error = f"HTTP {error.code}: {error.reason}"
last_error = "tag not found on Docker Hub"
else:
last_error = f"HTTP {error.code}: {error.reason}"
except urllib.error.URLError as error:
last_error = str(error.reason)
if attempt < attempts:
time.sleep(2 * attempt)
if last_error == "tag not found on Docker Hub":
return f"{tag}: {last_error}"
return f"{tag}: Docker Hub tag lookup failed: {last_error or 'unknown error'}"


Expand Down Expand Up @@ -189,7 +194,7 @@ def _release_package_tag(repo: RepoConfig, *, sha: str, component: str) -> str:
release_target_commit = find_release_target_commit(repo.path, changelog_version)
except (Exception, SystemExit):
release_target_commit = ""
if release_target_commit != sha:
if not _release_tag_sha_allowed(repo, release_target_commit, sha):
return ""

upstream_version = _read_component_upstream_version(repo, component)
Expand All @@ -201,3 +206,28 @@ def _release_package_tag(repo: RepoConfig, *, sha: str, component: str) -> str:
if repo.publish_profile == "upstream-aio-track":
return f"{upstream_version}-aio.{revision}"
return changelog_version


_RELEASE_FORMAT_SUBJECT = re.compile(
r"^chore\(release\): format .+ changelog(?: \(#\d+\))?$"
)


def _release_tag_sha_allowed(
repo: RepoConfig, release_target_commit: str, sha: str
) -> bool:
if release_target_commit == sha:
return True
try:
if not git_is_ancestor(repo.path, release_target_commit, sha):
return False
subjects = git(
repo.path, "log", "--format=%s", f"{release_target_commit}..{sha}"
)
except (Exception, SystemExit):
return False
return bool(subjects.strip()) and all(
_RELEASE_FORMAT_SUBJECT.match(subject.strip())
for subject in subjects.splitlines()
if subject.strip()
)
87 changes: 87 additions & 0 deletions tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,58 @@ def test_upstream_aio_track_release_tag_matches_changelog(monkeypatch) -> None:
assert "ghcr.io/jsonbored/sure-aio:0.7.0-aio.1" in tags.ghcr # nosec B101


def test_release_tag_allows_changelog_format_followup(monkeypatch) -> None:
repo = load_manifest(ROOT / "fleet.yml").repo("sure-aio")
release_sha = "c" * 40
publish_sha = "d" * 40

monkeypatch.setattr(
registry, "_read_component_upstream_version", lambda *_: "0.7.0"
)
monkeypatch.setattr(
registry, "latest_changelog_version", lambda *_args, **_kwargs: "0.7.0-aio.2"
)
monkeypatch.setattr(
registry, "find_release_target_commit", lambda *_args, **_kwargs: release_sha
)
monkeypatch.setattr(registry, "git_is_ancestor", lambda *_args: True)
monkeypatch.setattr(
registry,
"git",
lambda *_args: "chore(release): format sure changelog",
)

tags = registry.compute_registry_tags(repo, sha=publish_sha)

assert tags.release_package_tag == "0.7.0-aio.2" # nosec B101
assert "jsonbored/sure-aio:0.7.0-aio.2" in tags.dockerhub # nosec B101
assert "ghcr.io/jsonbored/sure-aio:0.7.0-aio.2" in tags.ghcr # nosec B101


def test_release_tag_rejects_arbitrary_post_release_commit(monkeypatch) -> None:
repo = load_manifest(ROOT / "fleet.yml").repo("sure-aio")
release_sha = "c" * 40
publish_sha = "d" * 40

monkeypatch.setattr(
registry, "_read_component_upstream_version", lambda *_: "0.7.0"
)
monkeypatch.setattr(
registry, "latest_changelog_version", lambda *_args, **_kwargs: "0.7.0-aio.2"
)
monkeypatch.setattr(
registry, "find_release_target_commit", lambda *_args, **_kwargs: release_sha
)
monkeypatch.setattr(registry, "git_is_ancestor", lambda *_args: True)
monkeypatch.setattr(registry, "git", lambda *_args: "fix(runtime): later change")

tags = registry.compute_registry_tags(repo, sha=publish_sha)

assert tags.release_package_tag == "" # nosec B101
assert "jsonbored/sure-aio:0.7.0-aio.2" not in tags.dockerhub # nosec B101
assert "ghcr.io/jsonbored/sure-aio:0.7.0-aio.2" not in tags.ghcr # nosec B101


def test_signoz_agent_publish_command_uses_component_context(monkeypatch) -> None:
repo = load_manifest(ROOT / "fleet.yml").repo("signoz-aio")

Expand Down Expand Up @@ -203,6 +255,7 @@ def read(self) -> bytes:

def test_dockerhub_verification_reports_missing_tag(monkeypatch) -> None:
monkeypatch.setattr(registry.shutil, "which", lambda _name: "docker")
monkeypatch.setattr(registry.time, "sleep", lambda _seconds: None)

def fake_urlopen(url: str, timeout: int):
raise HTTPError(url, 404, "Not Found", {}, None)
Expand All @@ -216,6 +269,40 @@ def fake_urlopen(url: str, timeout: int):
]


def test_dockerhub_verification_retries_new_tag_404(monkeypatch) -> None:
attempts = 0
sleeps: list[int] = []

class Response:
status = 200

def __enter__(self):
return self

def __exit__(self, *_args) -> None:
return None

def read(self) -> bytes:
return b"{}"

def fake_urlopen(url: str, timeout: int):
nonlocal attempts
attempts += 1
if attempts < 3:
raise HTTPError(url, 404, "Not Found", {}, None)
return Response()

monkeypatch.setattr(registry.shutil, "which", lambda _name: "docker")
monkeypatch.setattr(registry.urllib.request, "urlopen", fake_urlopen)
monkeypatch.setattr(registry.time, "sleep", sleeps.append)

assert (
registry.verify_registry_tags(["jsonbored/sure-aio:sha-new"]) == []
) # nosec B101
assert attempts == 3 # nosec B101
assert sleeps == [2, 4] # nosec B101


def test_ghcr_verification_uses_docker_imagetools(monkeypatch) -> None:
seen_commands: list[list[str]] = []
inspect_env = {"DOCKER_CONFIG": "/workspace/aio-fleet-docker"}
Expand Down
Loading