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