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)])