◐ Off-By-One · answer catalog

container-registry-image-path-drift-after-repository-owner-change

2 answer(s)yaml-markdowngithub-actionsyaml-markdowngithub-actions

A Go repository was transferred from to . CI publishes with a dynamic expression:

📦 Source in repository (JSON)

Answer 1

The solution is written to ~/SOLUTION.md. Here it is:

Fixing GHCR Image-Path Drift After a GitHub Repository Owner Change

1. Summary

A Go repository was transferred from <old-owner> to <new-owner>. CI publishes with a dynamic expression:

images: ghcr.io/${{ github.repository }}

github.repository resolves to <new-owner>/<repo> after the transfer, so CI automatically started publishing ghcr.io/<new-owner>/<repo>. But README, docker-compose.prod.yml, the cross-platform quickstart, and the usage skill still hard-coded ghcr.io/<old-owner>/<repo>.

Both paths return HTTP 401 to anonymous docker manifest inspect, which hides the real defect: the old path is not published, while the new path is private and requires auth. The drift is a reference bug, not a package-visibility change.

2. Root-cause analysis

Layer Before migration After migration
Git remote <old-owner>/<repo> <new-owner>/<repo>
Workflow images: ghcr.io/${{ github.repository }} same (unchanged)
Resolved publish path ghcr.io/<old-owner>/<repo> ghcr.io/<new-owner>/<repo>
README / Compose / quickstart / skill old path (hard-coded) still old → stale

The workflow's namespace is derived from context, so the publish target moved with the transfer. Every literal consumer did not. The old path is unpublished; the new path is private and returns 401 anonymously. No package visibility policy changed.

3. Determine the actual published path

git remote get-url origin                                   # -> <new-owner>/<repo>
grep -RIn "ghcr.io/\${{ github.repository }}" .github/workflows/
gh api "orgs/<new-owner>/packages/container/<repo>" --jq '.name' 2>/dev/null || true

# every stale reference
grep -RIn --exclude-dir=.git "ghcr.io/<old-owner>/<repo>" .

Canonical path: ghcr.io/<new-owner>/<repo>. Note GHCR requires lowercase names — verify the workflow expression yields lowercase.

4. The exact fix

4.1 Optionally normalize casing in the workflow so the image resolves to a single lowercase value:

env:
  IMAGE: ghcr.io/${{ github.repository }}
# normalize: echo "IMAGE=${IMAGE,,}" >> "$GITHUB_ENV"

4.2 Replace active references, then hand-check the four surfaces (README, docker-compose.prod.yml, quickstart docs, current usage skill):

grep -RIl --exclude-dir=.git "ghcr.io/<old-owner>/<repo>" . | while read -r f; do
  sed -i 's#ghcr\.io/<old-owner>/<repo>#ghcr.io/<new-owner>/<repo>#g' "$f"
done
git diff --stat
- image: ghcr.io/<old-owner>/<repo>:latest
+ image: ghcr.io/<new-owner>/<repo>:latest

4.3 Preserve history explicitly — do not rewrite changelogs/release notes. Label them:

> **Historical (pre-migration):** prior to the transfer, images were published
> as `ghcr.io/<old-owner>/<repo>`. No longer the published path.

Rule: changelogs/dated records keep the old string with a historical marker; active runbooks/README/Compose/quickstart/skill use only the new path.

4.4 Keep source builds as the zero-auth recommendation:

git clone https://github.com/<new-owner>/<repo>.git && cd <repo>
go build ./... && go run . --help

4.5 Document authenticated pulls without claiming visibility changed:

echo "$CR_PAT" | docker login ghcr.io -u <github-username> --password-stdin
docker pull ghcr.io/<new-owner>/<repo>:latest
The image is a private GHCR package. Pulling needs a token with `read:packages`.
The owner move changed the path; it did not change visibility.

5. Verification

# 5.1 no stale path in active files (historical mentions excluded)
grep -RIn --exclude-dir=.git "ghcr.io/<old-owner>/<repo>" README.md docker-compose.prod.yml docs/ skills/ \
  | grep -v -i historical && echo "STALE" || echo clean

# 5.2 compose resolves new image
docker compose -f docker-compose.prod.yml config | grep -i "image:"

# 5.3 build/vet/test
go build ./... && go vet ./... && go test -count=1 -short ./...

# 5.4 authenticated path check
docker manifest inspect ghcr.io/<new-owner>/<repo>:latest >/dev/null && echo "new OK"

Acceptance checklist: remote derives new owner; workflow resolves new path; all four active surfaces updated; old path only in labeled history; source build still the zero-auth path; docker login documented with read:packages and no visibility claim; docker compose config shows new image; build/vet/test pass; Actions green.

Key insight: the anonymous 401 on both paths is a red herring — the dynamic ${{ github.repository }} moved the publish target while hard-coded literals stayed behind. Fix the literals, not the package visibility.

Evidence & signatures

# Evidence
- Problem class: container-registry-image-path-drift-after-repository-owner-change
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T06:47:17.299Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A repository moved from one GitHub owner to another. The CI workflow used images: ghcr.io/${{ github.repository }}, so it automatically began publishing ghcr.io/<new-owner>/<repo>, while README, docker-compose.prod.yml, cross-platform quickstart, and usage guidance still named ghcr.io/<old-owner>/<repo>. Both anonymous manifest probes returned HTTP 401, masking the path drift as one generic package-visibility problem. Resolve the actual published path from git remote plus the workflow expression, update every active user-facing/runtime reference, preserve historical evidence as explicitly historical, keep source builds as the zero-auth recommendation, and document docker login ghcr.io with a read:packages token without claiming package visibility changed. Verify no old path remains in active files, docker compose config resolves the new image, build/vet/tests pass, and CI is green. Acceptance checks may include current usage skills as well as README and Compose; update them explicitly rather than treating every old path mention as immutable history.", "environment": "GitHub Actions docker/metadata-action publishing to GHCR for a Go repository whose GitHub owner changed; README, Compose, quickstart docs, and usage skill retain an old hard-coded owner", "language": "yaml-markdown", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "container-registry-image-path-drift-after-repository-owner-change", "provider": "openrouter", "solved_at": "2026-09-24T06:47:17.299Z", "version": "consensus master 589cec3"}

Answer 2

The solution is written to ~/SOLUTION.md. Here it is:

Fixing GHCR Image-Path Drift After a GitHub Repository Owner Change

1. Summary

A Go repository was transferred from <old-owner> to <new-owner>. CI publishes with a dynamic expression:

images: ghcr.io/${{ github.repository }}

github.repository resolves to <new-owner>/<repo> after the transfer, so CI automatically started publishing ghcr.io/<new-owner>/<repo>. But README, docker-compose.prod.yml, the cross-platform quickstart, and the usage skill still hard-coded ghcr.io/<old-owner>/<repo>.

Both paths return HTTP 401 to anonymous docker manifest inspect, which hides the real defect: the old path is not published, while the new path is private and requires auth. The drift is a reference bug, not a package-visibility change.

2. Root-cause analysis

Layer Before migration After migration
Git remote <old-owner>/<repo> <new-owner>/<repo>
Workflow images: ghcr.io/${{ github.repository }} same (unchanged)
Resolved publish path ghcr.io/<old-owner>/<repo> ghcr.io/<new-owner>/<repo>
README / Compose / quickstart / skill old path (hard-coded) still old → stale

The workflow's namespace is derived from context, so the publish target moved with the transfer. Every literal consumer did not. The old path is unpublished; the new path is private and returns 401 anonymously. No package visibility policy changed.

3. Determine the actual published path

git remote get-url origin                                   # -> <new-owner>/<repo>
grep -RIn "ghcr.io/\${{ github.repository }}" .github/workflows/
gh api "orgs/<new-owner>/packages/container/<repo>" --jq '.name' 2>/dev/null || true

# every stale reference
grep -RIn --exclude-dir=.git "ghcr.io/<old-owner>/<repo>" .

Canonical path: ghcr.io/<new-owner>/<repo>. Note GHCR requires lowercase names — verify the workflow expression yields lowercase.

4. The exact fix

4.1 Optionally normalize casing in the workflow so the image resolves to a single lowercase value:

env:
  IMAGE: ghcr.io/${{ github.repository }}
# normalize: echo "IMAGE=${IMAGE,,}" >> "$GITHUB_ENV"

4.2 Replace active references, then hand-check the four surfaces (README, docker-compose.prod.yml, quickstart docs, current usage skill):

grep -RIl --exclude-dir=.git "ghcr.io/<old-owner>/<repo>" . | while read -r f; do
  sed -i 's#ghcr\.io/<old-owner>/<repo>#ghcr.io/<new-owner>/<repo>#g' "$f"
done
git diff --stat
- image: ghcr.io/<old-owner>/<repo>:latest
+ image: ghcr.io/<new-owner>/<repo>:latest

4.3 Preserve history explicitly — do not rewrite changelogs/release notes. Label them:

> **Historical (pre-migration):** prior to the transfer, images were published
> as `ghcr.io/<old-owner>/<repo>`. No longer the published path.

Rule: changelogs/dated records keep the old string with a historical marker; active runbooks/README/Compose/quickstart/skill use only the new path.

4.4 Keep source builds as the zero-auth recommendation:

git clone https://github.com/<new-owner>/<repo>.git && cd <repo>
go build ./... && go run . --help

4.5 Document authenticated pulls without claiming visibility changed:

echo "$CR_PAT" | docker login ghcr.io -u <github-username> --password-stdin
docker pull ghcr.io/<new-owner>/<repo>:latest
The image is a private GHCR package. Pulling needs a token with `read:packages`.
The owner move changed the path; it did not change visibility.

5. Verification

# 5.1 no stale path in active files (historical mentions excluded)
grep -RIn --exclude-dir=.git "ghcr.io/<old-owner>/<repo>" README.md docker-compose.prod.yml docs/ skills/ \
  | grep -v -i historical && echo "STALE" || echo clean

# 5.2 compose resolves new image
docker compose -f docker-compose.prod.yml config | grep -i "image:"

# 5.3 build/vet/test
go build ./... && go vet ./... && go test -count=1 -short ./...

# 5.4 authenticated path check
docker manifest inspect ghcr.io/<new-owner>/<repo>:latest >/dev/null && echo "new OK"

Acceptance checklist: remote derives new owner; workflow resolves new path; all four active surfaces updated; old path only in labeled history; source build still the zero-auth path; docker login documented with read:packages and no visibility claim; docker compose config shows new image; build/vet/test pass; Actions green.

Key insight: the anonymous 401 on both paths is a red herring — the dynamic ${{ github.repository }} moved the publish target while hard-coded literals stayed behind. Fix the literals, not the package visibility.

Evidence & signatures

# Evidence
- Problem class: container-registry-image-path-drift-after-repository-owner-change
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T06:47:17.299Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A repository moved from one GitHub owner to another. The CI workflow used images: ghcr.io/${{ github.repository }}, so it automatically began publishing ghcr.io/<new-owner>/<repo>, while README, docker-compose.prod.yml, cross-platform quickstart, and usage guidance still named ghcr.io/<old-owner>/<repo>. Both anonymous manifest probes returned HTTP 401, masking the path drift as one generic package-visibility problem. Resolve the actual published path from git remote plus the workflow expression, update every active user-facing/runtime reference, preserve historical evidence as explicitly historical, keep source builds as the zero-auth recommendation, and document docker login ghcr.io with a read:packages token without claiming package visibility changed. Verify no old path remains in active files, docker compose config resolves the new image, build/vet/tests pass, and CI is green. Acceptance checks may include current usage skills as well as README and Compose; update them explicitly rather than treating every old path mention as immutable history.", "environment": "GitHub Actions docker/metadata-action publishing to GHCR for a Go repository whose GitHub owner changed; README, Compose, quickstart docs, and usage skill retain an old hard-coded owner", "language": "yaml-markdown", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "container-registry-image-path-drift-after-repository-owner-change", "provider": "openrouter", "solved_at": "2026-09-24T06:47:17.299Z", "version": "consensus master 589cec3"}
Generated from the verified corpus · MIT licensedBack to the catalog