Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/mirror-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Mirror Images
on:
pull_request:
branches:
- main
paths:
- docker/mirror-images.txt
- .github/workflows/mirror-images.yml
push:
branches:
- main
paths:
- docker/mirror-images.txt
- .github/workflows/mirror-images.yml
workflow_dispatch:

jobs:
# Copies every image in docker/mirror-images.txt from Docker Hub to
# ghcr.io/<owner>/mirror/<path>:<tag>, byte-for-byte (same digest, all
# platforms). Same model as push-images in ci.yml: the built-in GITHUB_TOKEN
# with packages: write, no registry PAT, and on pull_request events nothing is
# pushed, only checked. The pinned digest is what gets copied, so a tag that
# moves upstream only raises a warning.
mirror-images:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
packages: write
env:
MIRROR_PREFIX: ghcr.io/${{ github.repository_owner }}/mirror
Comment thread
xnap marked this conversation as resolved.
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Log in to GHCR
if: github.event_name != 'pull_request'
run: |
echo "${{ secrets.GITHUB_TOKEN }}" |
skopeo login ghcr.io --username "${{ github.actor }}" --password-stdin
- name: Check or copy each image
run: |
set -euo pipefail
grep -Ev '^[[:space:]]*(#|$)' docker/mirror-images.txt | while read -r line; do
ref="${line%@*}"
digest="${line#*@}"
source="docker://docker.io/${ref%:*}@$digest"
skopeo inspect --raw "$source" > /dev/null
tag_digest="$(skopeo inspect --raw "docker://docker.io/$ref" | sha256sum | cut -d' ' -f1)"
if [[ "sha256:$tag_digest" != "$digest" ]]; then
echo "::warning::$ref now resolves to sha256:$tag_digest, not the pinned $digest; the pinned digest is still what gets mirrored."
fi
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "ok $ref@$digest"
continue
fi
dest="docker://$MIRROR_PREFIX/$ref"
skopeo copy --all --preserve-digests "$source" "$dest"
mirrored="sha256:$(skopeo inspect --raw "$dest" | sha256sum | cut -d' ' -f1)"
if [[ "$mirrored" != "$digest" ]]; then
echo "::error::$MIRROR_PREFIX/$ref has digest $mirrored after the copy, expected $digest."
exit 1
fi
echo "mirrored $MIRROR_PREFIX/$ref@$digest"
done
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ The primary downstream consumer of the images is a **separate repo,
scripts (`root/entrypoint.NN-*.sh` run as root, `guest/entrypoint.NN-*.sh` as
the runner user, in numeric order).
- `docker/compose.yml` — local/integration testing of all three images together.
- `.github/workflows/ci.yml` — the only workflow.
- `.github/workflows/ci.yml` — tests and publishes the three images.
- `.github/workflows/mirror-images.yml` + `docker/mirror-images.txt` — copies
pinned third-party images (the `time-loop/sd` shard Redis) from Docker Hub to
`ghcr.io/time-loop/mirror/*`, so runners never pull Docker Hub anonymously.
- `PUBLISH.md` — release + GHCR publishing instructions.

## CI / publishing
Expand Down
17 changes: 17 additions & 0 deletions PUBLISH.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ Do this for `ci-storage`, `ci-scaler`, and `ci-runner`. This may require org
admin privileges. Public visibility lets downstream consumers (e.g.
`time-loop/sd`) pull the images anonymously.

## Mirrored third-party images

`.github/workflows/mirror-images.yml` copies each image pinned in
`docker/mirror-images.txt` from Docker Hub to
`ghcr.io/time-loop/mirror/<path>:<tag>` with the same digest, using the same
`GITHUB_TOKEN` login as above. It runs when either file changes on `main`, and
on demand via "Run workflow". To mirror a new image, add its
`<path>:<tag>@<digest>` line and merge; the first run creates the package,
which then needs the same one-time switch to public:

- `mirror/bitnamilegacy/redis-cluster`
- `mirror/bitnamilegacy/redis`
- `mirror/library/alpine`

Add the image here before any consumer points at it: a consumer that pulls a
missing or still-private mirror package fails its pull.

## Release a new GitHub Action version

To release a new GitHub Action version to the GitHub Marketplace (example for v1
Expand Down
9 changes: 9 additions & 0 deletions docker/mirror-images.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Third-party images mirrored to ghcr.io/<owner>/mirror/<path>:<tag> by
# .github/workflows/mirror-images.yml, so self-hosted runners pull them from
# GHCR instead of anonymously from Docker Hub.
#
# One image per line: <Docker Hub path>:<tag>@<digest>. The digest pins what is
# copied; the mirror keeps the same digest, so consumers can pin it too.
bitnamilegacy/redis-cluster:6.2.16@sha256:d973a2aa8b6688190ca4e4544b2ff859ef1e9f8081518558270df34e23ff1df7
bitnamilegacy/redis:6.2.16@sha256:116419dc09f37b2c37e47e1a2cef258620ca1a30bae379b3660cf78fc0e7fc99
library/alpine:3.21@sha256:ce64758a109eb420d874a118f87920e625e12d3634e03b4a5573fd9f6e5d3507
Loading