Repository navigation
dockerio #282
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: dockerio | |
| on: | |
| push: | |
| workflow_dispatch: | |
| inputs: | |
| image: | |
| description: 'Docker Hub image (e.g., nginx, library/alpine)' | |
| required: true | |
| type: string | |
| schedule: | |
| - cron: '0 0 * * *' # Daily at midnight UTC | |
| env: | |
| # Define images to mirror (format: source -> mirrored to ghcr.io/$REPO/docker.io/source) | |
| IMAGES: | | |
| netbirdio/management | |
| netbirdio/dashboard | |
| netbirdio/relay | |
| netbirdio/signal | |
| library/haproxy | |
| library/busybox | |
| library/alpine | |
| rclone/rclone | |
| kanidm/server | |
| pingcap/tcpcopy | |
| weechat/weechat | |
| jobs: | |
| # Build matrix of images to mirror | |
| prepare: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.set-matrix.outputs.matrix }} | |
| steps: | |
| - id: set-matrix | |
| run: | | |
| if [ -n "${{ inputs.image }}" ]; then | |
| # Manual trigger - single image | |
| echo "matrix=[\"${{ inputs.image }}\"]" >> $GITHUB_OUTPUT | |
| else | |
| # Scheduled - all images from IMAGES env | |
| MATRIX=$(echo "$IMAGES" | grep -v '^$' | jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| echo "matrix=$MATRIX" >> $GITHUB_OUTPUT | |
| fi | |
| mirror: | |
| needs: prepare | |
| runs-on: ubuntu-latest | |
| permissions: | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 4 # Parallel image mirroring | |
| matrix: | |
| image: ${{ fromJson(needs.prepare.outputs.matrix) }} | |
| steps: | |
| - name: Setup crane | |
| uses: imjasonh/setup-crane@v0.4 | |
| - name: Login to GHCR | |
| run: crane auth login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} | |
| - name: Login to Docker Hub (optional, increases rate limits) | |
| env: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| run: | | |
| if [ -n "$DOCKERHUB_TOKEN" ]; then | |
| crane auth login docker.io -u "$DOCKERHUB_USERNAME" -p "$DOCKERHUB_TOKEN" | |
| fi | |
| continue-on-error: true | |
| - name: Mirror ${{ matrix.image }} | |
| run: | | |
| SOURCE="docker.io/${{ matrix.image }}" | |
| DEST="ghcr.io/${{ github.repository }}/docker.io/${{ matrix.image }}" | |
| # Retry with exponential backoff | |
| retry() { | |
| local max=5 delay=30 attempt=1 | |
| while [ $attempt -le $max ]; do | |
| if "$@" 2>&1; then return 0; fi | |
| [ $attempt -lt $max ] && echo "⏳ Retry $attempt/$max in ${delay}s..." && sleep $delay && delay=$((delay * 2)) | |
| attempt=$((attempt + 1)) | |
| done | |
| return 1 | |
| } | |
| # Copy single tag if digest differs | |
| copy_tag() { | |
| local tag="$1" | |
| local src_digest dst_digest | |
| src_digest=$(retry crane digest "$SOURCE:$tag" 2>/dev/null) || return 1 | |
| dst_digest=$(crane digest "$DEST:$tag" 2>/dev/null) || dst_digest="" | |
| if [ "$src_digest" = "$dst_digest" ]; then | |
| echo "⏭️ $tag (unchanged)" | |
| else | |
| retry crane copy "$SOURCE:$tag" "$DEST:$tag" && echo "✅ $tag" | |
| fi | |
| } | |
| echo "Mirroring: $SOURCE -> $DEST" | |
| # Get last 10 tags + ensure latest is included first | |
| TAGS=$(retry crane ls "$SOURCE") | |
| if retry crane manifest "$SOURCE:latest" >/dev/null 2>&1; then | |
| TAGS=$(echo -e "latest\n$TAGS" | awk '!seen[$0]++') | |
| fi | |
| echo "Tags: $TAGS" | |
| # Export for subshells | |
| export SOURCE DEST | |
| export -f retry copy_tag | |
| # Copy in parallel | |
| echo "$TAGS" | xargs -P4 -I{} bash -c 'copy_tag "{}"' |