Files
notifier/.gitea/workflows/build-and-publish.yml
T
igodwin ba4133bf5d
Build and Publish Container / build-and-publish (push) Failing after 2s
CI / Vulnerability scan (push) Failing after 2s
CI / Lint (push) Failing after 1s
CI / Test (push) Failing after 1s
ci: build and publish multi-arch images with auto patch versioning
Push to main mints the next vX.Y.Z patch tag, builds and pushes a
multi-arch image to the registry, and tags the repo. A manual
bump-version workflow handles minor/major bumps.

This workflow deliberately holds no deployment-repo credentials: the
repo is public, so the GitOps side polls the registry and pulls new
tags itself rather than being pushed to from here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 09:29:10 -07:00

170 lines
6.1 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.
#
# 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: |
IMAGE_REF="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}"
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 \
-t "$IMAGE_REF" \
--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
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