ddcbc04b71
Publishes an additional floating vX.Y tag alongside the immutable vX.Y.Z so `docker pull ...:vX.Y` fetches the newest patch. Skipped on --rebuild of an older version so the float never rolls backward. Deploys still pin vX.Y.Z; the cleanup reaper only targets three-part semver, so the float is never eligible for deletion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
181 lines
6.8 KiB
YAML
181 lines
6.8 KiB
YAML
name: Build and Publish Container
|
|
|
|
# Builds and publishes a multi-arch image on every push to main, minting the
|
|
# next patch version from git tags (vX.Y.Z) and pushing the tag back. Real
|
|
# releases also advance a floating minor tag (vX.Y -> newest patch) as a
|
|
# pull convenience; deploys still pin the immutable vX.Y.Z.
|
|
#
|
|
# NOTE (public repo): unlike private app repos, this workflow deliberately has
|
|
# NO step that pushes to the deployment (GitOps) repository and holds no
|
|
# credentials for it. Deployment repos are expected to poll the registry and
|
|
# pull new tags themselves.
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: 'Git tag (e.g. v0.1.5) or commit SHA to rebuild. Leave empty to build latest.'
|
|
required: false
|
|
default: ''
|
|
|
|
env:
|
|
REGISTRY: gitea.ivangodwin.com
|
|
IMAGE_NAME: ${{ gitea.repository }}
|
|
|
|
jobs:
|
|
build-and-publish:
|
|
runs-on: docker
|
|
|
|
permissions:
|
|
contents: write
|
|
packages: write
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
run: |
|
|
if [ -n "${{ inputs.ref }}" ]; then
|
|
git clone https://${{ gitea.actor }}:${{ gitea.token }}@gitea.ivangodwin.com/${{ gitea.repository }}.git .
|
|
git fetch --tags
|
|
git checkout "${{ inputs.ref }}"
|
|
else
|
|
git clone --depth 1 https://${{ gitea.actor }}:${{ gitea.token }}@gitea.ivangodwin.com/${{ gitea.repository }}.git .
|
|
git checkout ${{ gitea.sha }}
|
|
fi
|
|
|
|
- name: Determine next version
|
|
run: |
|
|
REF="${{ inputs.ref }}"
|
|
if [ -n "$REF" ] && echo "$REF" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
VERSION="$REF"
|
|
echo "REBUILD=true" >> $GITHUB_ENV
|
|
else
|
|
git fetch --tags
|
|
LATEST=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true)
|
|
if [ -z "$LATEST" ]; then
|
|
VERSION="v0.1.0"
|
|
else
|
|
MAJOR=$(echo "$LATEST" | cut -d. -f1 | tr -d 'v')
|
|
MINOR=$(echo "$LATEST" | cut -d. -f2)
|
|
PATCH=$(echo "$LATEST" | cut -d. -f3)
|
|
VERSION="v${MAJOR}.${MINOR}.$((PATCH + 1))"
|
|
fi
|
|
echo "REBUILD=false" >> $GITHUB_ENV
|
|
fi
|
|
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
|
|
|
- name: Log in to Gitea Container Registry
|
|
run: |
|
|
echo "${{ secrets.CI_TOKEN }}" | docker login -u "${{ secrets.CI_USER }}" --password-stdin ${{ env.REGISTRY }}
|
|
|
|
- name: Register QEMU emulators
|
|
run: |
|
|
docker run --rm --privileged tonistiigi/binfmt:latest --install all
|
|
|
|
- name: Set up Docker Buildx
|
|
run: |
|
|
docker buildx inspect multiarch >/dev/null 2>&1 \
|
|
|| docker buildx create --name multiarch --driver docker-container
|
|
docker buildx use multiarch
|
|
docker buildx inspect --bootstrap
|
|
|
|
- name: Build and push multi-arch image
|
|
run: |
|
|
TAGS="-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}"
|
|
# On real releases, also advance the floating minor tag (vX.Y) to this
|
|
# build so `docker pull ...:vX.Y` fetches the newest patch. Skipped on
|
|
# a --rebuild of an older version, which must not clobber the float.
|
|
if [ "${{ env.REBUILD }}" != "true" ]; then
|
|
MINOR_TAG=$(echo "${{ env.VERSION }}" | grep -oE '^v[0-9]+\.[0-9]+')
|
|
TAGS="${TAGS} -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${MINOR_TAG}"
|
|
fi
|
|
docker buildx build \
|
|
--platform linux/amd64,linux/arm64 \
|
|
--build-arg VERSION="${{ env.VERSION }}" \
|
|
--build-arg GIT_COMMIT="$(git rev-parse --short HEAD)" \
|
|
--build-arg BUILD_TIME="$(date -u '+%Y-%m-%d_%H:%M:%S_UTC')" \
|
|
--no-cache \
|
|
--provenance=false \
|
|
${TAGS} \
|
|
--push \
|
|
.
|
|
|
|
- name: Tag release
|
|
if: env.REBUILD != 'true'
|
|
run: |
|
|
git tag "${{ env.VERSION }}"
|
|
git push https://${{ gitea.actor }}:${{ gitea.token }}@gitea.ivangodwin.com/${{ gitea.repository }}.git "${{ env.VERSION }}"
|
|
|
|
- name: Clean up old container images
|
|
continue-on-error: true
|
|
timeout-minutes: 5
|
|
env:
|
|
CI_TOKEN: ${{ secrets.CI_TOKEN }}
|
|
run: |
|
|
# Best-effort cleanup of old container image versions; must never
|
|
# fail or stall the pipeline, so the whole body runs under a hard
|
|
# timeout and the step always exits 0 itself.
|
|
cat > /tmp/cleanup.sh <<'CLEAN'
|
|
#!/bin/sh
|
|
set -u
|
|
|
|
if ! apk add -q --no-cache curl; then
|
|
echo "curl unavailable; skipping cleanup."
|
|
exit 0
|
|
fi
|
|
|
|
API="https://gitea.ivangodwin.com/api/v1"
|
|
OWNER="igodwin"
|
|
NAME="notifier"
|
|
KEEP=5
|
|
|
|
BODY=$(curl -s --connect-timeout 15 --max-time 30 \
|
|
-H "Authorization: token ${CI_TOKEN}" \
|
|
"${API}/packages/${OWNER}?type=container&q=${NAME}&limit=200" || true)
|
|
|
|
if [ -z "$BODY" ] || [ "$(printf '%s' "$BODY" | tr -d ' \t\r\n')" = "[]" ]; then
|
|
echo "No packages found."
|
|
exit 0
|
|
fi
|
|
|
|
# Only three-part semver (vX.Y.Z) is eligible for deletion; the
|
|
# floating minor tag (vX.Y) never matches, so it is never reaped.
|
|
VERSIONS=$(printf '%s' "$BODY" \
|
|
| grep -oE '"version":"v[0-9]+\.[0-9]+\.[0-9]+"' \
|
|
| grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' \
|
|
| sort -u)
|
|
MINORS=$(printf '%s' "$VERSIONS" | grep -oE '^v[0-9]+\.[0-9]+' | sort -u)
|
|
|
|
for MINOR in $MINORS; do
|
|
PATCHES=$(printf '%s' "$VERSIONS" | grep "^${MINOR}\." | sort -t. -k3,3n)
|
|
TOTAL=$(printf '%s\n' "$PATCHES" | grep -c .)
|
|
|
|
if [ "$TOTAL" -le "$KEEP" ]; then
|
|
echo "${MINOR}: ${TOTAL} versions, nothing to delete"
|
|
continue
|
|
fi
|
|
|
|
TO_DELETE=$((TOTAL - KEEP))
|
|
echo "${MINOR}: ${TOTAL} versions, keeping ${KEEP}, deleting ${TO_DELETE}"
|
|
|
|
printf '%s\n' "$PATCHES" | head -n "$TO_DELETE" | while read -r VER; do
|
|
STATUS=$(curl -s --connect-timeout 15 --max-time 60 -o /dev/null -w "%{http_code}" \
|
|
-X DELETE -H "Authorization: token ${CI_TOKEN}" \
|
|
"${API}/packages/${OWNER}/container/${NAME}/${VER}" || echo 000)
|
|
case "$STATUS" in
|
|
200|202|204) echo " deleted ${NAME}:${VER} (HTTP ${STATUS})" ;;
|
|
*) echo " WARN: could not delete ${NAME}:${VER} (HTTP ${STATUS})" ;;
|
|
esac
|
|
done
|
|
done
|
|
CLEAN
|
|
|
|
timeout 240 sh /tmp/cleanup.sh
|
|
rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
echo "cleanup did not finish cleanly (rc=${rc}); ignoring and exiting green."
|
|
fi
|
|
exit 0
|