From 0336b35c5a3fac7ef1b53cbda675803c19330b8c Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Thu, 5 Nov 2026 08:36:12 -0800 Subject: [PATCH] fix(release): retry a push GHCR refuses while another platform creates the package An image's first release pushes its amd64 and arm64 builds at once. The first push creates the package and links it to this repository; GHCR refuses the other with permission_denied: write_package until it has, which failed v0.9.1444 at agentenv-browser-use's arm64 push. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/infra_build.py | 10 ++++++++-- tst/unit/test_infra_build_script.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/scripts/infra_build.py b/.github/scripts/infra_build.py index c067f3054..a2ea9007b 100644 --- a/.github/scripts/infra_build.py +++ b/.github/scripts/infra_build.py @@ -82,6 +82,9 @@ # What pip in an agent image's build answers for the agentenv-framework-protocol its release published moments earlier, # until PyPI's index, which a CDN caches, lists it: pinned (an agent's), or a floor it names (the browser's recorder). _NOT_YET_SERVED = re.compile(r"No matching distribution found for agentenv-framework-protocol(\[[^\]]*\])?(==|>=)") +# What GHCR answers a push to a new package while another platform's push creates it, until it links the package to +# this repository. +_NOT_YET_LINKED = re.compile(r"denied: permission_denied: write_package") # How long to wait before each try again, so a burst of builds starting together spreads out. _RETRY_WAITS_SECONDS = (30, 60, 120) @@ -308,13 +311,16 @@ def _tried_again(attempt: Callable[[], subprocess.CompletedProcess]) -> subproce def _why_again(result: subprocess.CompletedProcess) -> str | None: """Why a failed run can work later, or None: a registry rate-limited it, as public.ecr.aws does past one anonymous - pull a second, which builds starting together pass; or a build's pip can't find the agentenv-framework-protocol its - release has just published, which PyPI's index lists a little after the upload.""" + pull a second, which builds starting together pass; a build's pip can't find the agentenv-framework-protocol its + release has just published, which PyPI's index lists a little after the upload; or GHCR refuses a push to a package + another platform's push is creating, as an image's first release pushes both at once.""" printed, command = f"{result.stdout}{result.stderr}", f"`{' '.join(result.args)[:_PRINTED_CHARS]}`" if _RATE_LIMITED.search(printed): return f"a registry rate-limited {command}" if _NOT_YET_SERVED.search(printed): return f"PyPI doesn't list yet the agentenv-framework-protocol {command} installs" + if list(result.args[:2]) == ["docker", "push"] and _NOT_YET_LINKED.search(printed): + return f"GHCR doesn't let this repository write yet the package {command} pushes to" return None diff --git a/tst/unit/test_infra_build_script.py b/tst/unit/test_infra_build_script.py index 6bf1132da..5fa6b5020 100644 --- a/tst/unit/test_infra_build_script.py +++ b/tst/unit/test_infra_build_script.py @@ -463,6 +463,25 @@ def test_retry_tries_again_while_pypi_doesnt_list_the_protocol_floor_a_build_nam assert len(runs) == 2 and waits == [script._RETRY_WAITS_SECONDS[0]] +def test_retry_tries_again_a_push_ghcr_refuses_while_another_platform_creates_the_package(script, monkeypatch, capsys): + """An image's first release pushes both platforms at once; the second waits on GHCR linking the new package.""" + runs, waits = _attempts(script, monkeypatch, [("denied: permission_denied: write_package\n", 1), ("pushed\n", 0)]) + + assert _run(script, monkeypatch, "retry", "docker", "push", "img") == 0 + + assert len(runs) == 2 and waits == [script._RETRY_WAITS_SECONDS[0]] + assert ("::warning::GHCR doesn't let this repository write yet the package `docker push img` pushes to; trying again in" + in capsys.readouterr().out) + + +def test_retry_doesnt_try_again_a_denied_command_that_isnt_a_push(script, monkeypatch, capsys): + runs, waits = _attempts(script, monkeypatch, [("denied: permission_denied: write_package\n", 1)]) + + assert _run(script, monkeypatch, "retry", "docker", "build", ".") == 1 + + assert len(runs) == 1 and waits == [] + + def test_retry_doesnt_try_again_a_build_missing_another_package(script, monkeypatch, capsys): runs, waits = _attempts(script, monkeypatch, [("ERROR: No matching distribution found for nonexistent-pkg==1.0\n", 1)])