@@ -270,12 +270,41 @@ def detect_arch_flag() -> Optional[str]:
270270 return None
271271
272272
273+ def _ghcr_login_with_gh_token () -> bool :
274+ """Log in to ghcr.io using the user's gh CLI token. Idempotent.
275+
276+ Required when the package is private (GHCR's default for new packages).
277+ Anyone with `gh auth login` already has the credentials we need.
278+ """
279+ token_proc = subprocess .run (
280+ ["gh" , "auth" , "token" ], capture_output = True , text = True ,
281+ )
282+ token = token_proc .stdout .strip ()
283+ if token_proc .returncode != 0 or not token :
284+ return False
285+ user_proc = subprocess .run (
286+ ["gh" , "api" , "user" , "--jq" , ".login" ],
287+ capture_output = True , text = True ,
288+ )
289+ user = user_proc .stdout .strip () or os .environ .get ("USER" , "github" )
290+ proc = subprocess .run (
291+ ["docker" , "login" , "ghcr.io" , "-u" , user , "--password-stdin" ],
292+ input = token , text = True , capture_output = True ,
293+ )
294+ if proc .returncode != 0 :
295+ warn (f"docker login ghcr.io failed: { proc .stderr .strip () or proc .stdout .strip ()} " )
296+ return False
297+ info (f"Logged into ghcr.io as { user } " )
298+ return True
299+
300+
273301def ensure_runner_image () -> str :
274302 """Return the Docker image act should use as the ubuntu-latest runner.
275303
276304 Resolution order:
277305 1. Already cached locally as RUNNER_IMAGE_LOCAL → use it.
278- 2. Pull RUNNER_IMAGE_REGISTRY from GHCR, retag as RUNNER_IMAGE_LOCAL.
306+ 2. Pull RUNNER_IMAGE_REGISTRY from GHCR. If the pull 401s (private
307+ package), log in with `gh auth token` and retry once.
279308 3. Dev fallback: build from a sibling Dockerfile (only present when
280309 running from a qa-cli checkout, not when bootstrapped via `uv run`).
281310 """
@@ -287,9 +316,18 @@ def ensure_runner_image() -> str:
287316 info (f"Using cached runner image { RUNNER_IMAGE_LOCAL } " )
288317 return RUNNER_IMAGE_LOCAL
289318
319+ def _try_pull () -> bool :
320+ return subprocess .run (["docker" , "pull" , RUNNER_IMAGE_REGISTRY ]).returncode == 0
321+
290322 info (f"Pulling runner image from { RUNNER_IMAGE_REGISTRY } …" )
291- pull = subprocess .run (["docker" , "pull" , RUNNER_IMAGE_REGISTRY ])
292- if pull .returncode == 0 :
323+ if not _try_pull () and _ghcr_login_with_gh_token ():
324+ info ("Retrying pull after ghcr.io login…" )
325+ _try_pull ()
326+ inspect2 = subprocess .run (
327+ ["docker" , "image" , "inspect" , RUNNER_IMAGE_REGISTRY ],
328+ capture_output = True ,
329+ )
330+ if inspect2 .returncode == 0 :
293331 subprocess .run (
294332 ["docker" , "tag" , RUNNER_IMAGE_REGISTRY , RUNNER_IMAGE_LOCAL ],
295333 check = True ,
@@ -310,9 +348,9 @@ def ensure_runner_image() -> str:
310348 fatal (
311349 "Could not obtain runner image. Tried:\n "
312350 f" • local cache: docker image inspect { RUNNER_IMAGE_LOCAL } \n "
313- f" • registry: docker pull { RUNNER_IMAGE_REGISTRY } \n "
351+ f" • registry: docker pull { RUNNER_IMAGE_REGISTRY } (after gh-based login attempt) \n "
314352 f" • local build: { dockerfile } (not found — uv-run scripts don't fetch siblings)\n "
315- "Check Docker is running and that you can reach ghcr.io."
353+ "Check Docker is running, `gh auth status` works, and you can reach ghcr.io."
316354 )
317355 return "" # unreachable; appeases type checker
318356
0 commit comments