name: Push Image description: Push a locally-built image to code.fritzlab.net and prune old numeric tags. inputs: image: description: Full image name without tag (e.g. code.fritzlab.net/fritzlab/chrony) required: true tag: description: Tag to push. Defaults to github.run_number when empty. required: false default: '' token: description: CI_BOT_TOKEN — registry login + Gitea package API for cleanup required: true org: description: Gitea org for package API (e.g. fritzlab, dns) required: true name: description: Package/image name (e.g. chrony, base, runner) required: true latest: description: Also push a :latest tag required: false default: 'true' keep: description: Numeric tags to retain after prune; older ones are deleted required: false default: '3' runs: using: composite steps: - name: Log in to registry uses: docker/login-action@v3 with: registry: code.fritzlab.net username: ci-bot password: ${{ inputs.token }} - name: Push shell: bash env: IMAGE: ${{ inputs.image }} TAG_INPUT: ${{ inputs.tag }} RUN_NUMBER: ${{ github.run_number }} LATEST: ${{ inputs.latest }} run: | set -euo pipefail TAG="${TAG_INPUT:-$RUN_NUMBER}" echo "Pushing ${IMAGE}:${TAG}" docker push "${IMAGE}:${TAG}" if [ "$LATEST" = "true" ]; then docker tag "${IMAGE}:${TAG}" "${IMAGE}:latest" echo "Pushing ${IMAGE}:latest" docker push "${IMAGE}:latest" fi - name: Prune old tags continue-on-error: true shell: bash env: TOKEN: ${{ inputs.token }} ORG: ${{ inputs.org }} NAME: ${{ inputs.name }} KEEP: ${{ inputs.keep }} run: | set -euo pipefail tea login add --name ci --url https://code.fritzlab.net \ --token "$TOKEN" --no-version-check >/dev/null 2>&1 || true # Paginate the package list. The org has more container versions than a # single API page returns (Gitea caps page size at MAX_RESPONSE_ITEMS), # so an unpaginated call silently drops a package's older tags from the # list and they never get pruned — that's how tags crept past KEEP. page=1 while :; do resp=$(tea api "/packages/${ORG}?type=container&limit=50&page=${page}") [ "$(jq 'length' <<<"$resp")" -eq 0 ] && break jq -r --arg n "$NAME" \ '.[] | select(.name==$n) | select(.version | test("^[0-9]+$")) | .version' \ <<<"$resp" page=$((page + 1)) done \ | sort -n | head -n -"$KEEP" \ | while read -r tag; do echo "deleting ${NAME}:$tag" tea api -X DELETE "/packages/${ORG}/container/${NAME}/${tag}" done - name: Untag local image # image-build loads the built image into the runner's dind (load: true). # Nothing needs the local tag after the push, but the tags used to pile up # forever in the pod's dind image store (dozens of stale 2-3GB tags). Untag # after pushing to reclaim the store; the buildkit BUILD cache is separate # and refcounts its own layers, so the next build of this image is still # warm. (When THIS repo is `runner`, untagging :latest costs the pod's next # job one ~380MB re-pull of the job image from the local registry — cheap.) # The image prune sweeps the now-dangling previous :latest that a re-tag # orphans; it only removes untagged images no container/tag references, so # shared layers survive. continue-on-error: true shell: bash env: IMAGE: ${{ inputs.image }} TAG_INPUT: ${{ inputs.tag }} RUN_NUMBER: ${{ github.run_number }} LATEST: ${{ inputs.latest }} run: | TAG="${TAG_INPUT:-$RUN_NUMBER}" docker rmi "${IMAGE}:${TAG}" || true if [ "$LATEST" = "true" ]; then docker rmi "${IMAGE}:latest" || true fi docker image prune -f || true