Skip to content

Commit 9391c9c

Browse files
tylergannonclaude
andcommitted
Auto-login to ghcr.io with gh token on private-package 401
GHCR's default for new packages is private. Rather than require a manual UI flip to make the runner image public, fall back to a `gh auth token | docker login ghcr.io --password-stdin` retry. Anyone with `gh auth login` already has the credentials. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b08bdbd commit 9391c9c

1 file changed

Lines changed: 43 additions & 5 deletions

File tree

qa.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
273301
def 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

Comments
 (0)