diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 0000000..7e46b17 --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,51 @@ +name: Sync DockerHub images with GHCR 🔁 + +## This workflow is a temporal solution for having all our docker images synced to GHCR. However, +## as soon as all the repo's CI is migrated to GitHub Actions, this can be removed. +## +## Project tracking the progress: https://github.com/orgs/jellyfin/projects/31 + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + sync: + runs-on: ubuntu-latest + name: Image sync 🔁 + # Errors will probably be caused by excesses in API quota, so we can safely continue. + # Remaining workflows will be removed in the next scheduled run. + continue-on-error: true + strategy: + fail-fast: false + matrix: + image: + - 'jellyfin/jellyfin' + + steps: + - name: Clone repository + - uses: actions/checkout@v2.3.4 + + # The 'echo' command masks the environment variable + - name: Prepare environment + run: chmod +x sync_image_ghcr.sh && echo "::add-mask::${GITHUB_TOKEN}" + + ## Logging in to DockerHub allows for more pulls without hitting DockerHub's quota (100 vs 200). + - name: Login to Docker Hub + uses: docker/login-action@v1.10.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1.10.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.JF_BOT_TOKEN }} + + - name: Run syncing script + run: ./sync_image_ghcr.sh ${{ matrix.image }} + env: + GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} diff --git a/sync_image_ghcr.sh b/sync_image_ghcr.sh new file mode 100644 index 0000000..689ed78 --- /dev/null +++ b/sync_image_ghcr.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -e +# Simple script that pushes missing/outdated tags from an image in DockerHub to GHCR, skipping up-to-date tags +# GitHub package registry. You need to be logged in using 'docker login' first: https://docs.github.com/es/packages/working-with-a-github-packages-registry/working-with-the-container-registry +target_repo="ghcr.io" +tag_file="tag_list.txt" +original_image="$1" +rm -rf $tag_file +url="https://hub.docker.com/v2/repositories/${original_image}/tags" +tag_count=$(wget -q "$url" -O - | jq -r '.count') + +echo "Fetching tags from DockerHub..." +while true; do + results=$(wget -q "$url" -O - | jq -r '.') + url=$(echo "$results" | jq -r '.next') + echo "$results" | jq -r '.results[] | {name: .name, last_pushed: .tag_last_pushed, digests: [.images[].digest]}' >> $tag_file + if [ "${url}" = "null" ] + then + break + else + continue + fi +done; +unset results, url + +sorted=$(cat "$tag_file" | jq -s 'sort_by(.last_pushed)') +echo "$sorted" > $tag_file +file_tag_count=$(jq length "$tag_file") + +if [ $tag_count = $file_tag_count ] +then + echo -e "All the data was retrieved correctly. Pushing missing/modified tags to DockerHub...\n" +else + echo "The retrieved data doesn't match the amount of tags expected by Docker API. Exiting script..." + exit 1 +fi + +unset sorted, file_tag_count, tag_count + +## This token is that GitHub provides is used to access the registry in read-only, so users are able to +## use GHCR without signing up to GitHub. By using this token for checking for the published images, we don't consume +## our own API quota. +dest_token=$(wget -q https://${target_repo}/token\?scope\="repository:${original_image}:pull" -O - | jq -r '.token') +tag_names=$(cat "$tag_file" | jq -r '.[] | .name') + +while read -r line; do + tag="$line" + source_digests=$(cat "$tag_file" | jq -r --arg TAG_NAME "$tag" '.[] | select(.name == $TAG_NAME) | .digests | sort | .[]' | cat) + target_manifest=$(wget --header="Authorization: Bearer ${dest_token}" -q https://${target_repo}/v2/${original_image}/manifests/${tag} -O - | cat) + target_digests=$(echo "$target_manifest" | jq '.manifests | .[] | .digest' | jq -s '. | sort' | jq -r '.[]' | cat) + if [ "$source_digests" = "$target_digests" ] + then + echo The tag $tag is fully updated in $target_repo + continue + else + echo Updating $tag in $target_repo + docker pull $original_image:$tag + docker tag $original_image:$tag $target_repo/$original_image:$tag + docker push $target_repo/$original_image:$tag + + # Delete pushed images from local system + docker image rm $original_image:$tag + docker image rm $target_repo/$original_image:$tag + fi +done <<< $tag_names + +rm -rf $tag_file +echo -e "\nAll the tags have been updated successfully" +exit 0