feat: auto-release images from conventional commits on main
Build and publish container / build (pull_request) Successful in 2m58s

On each push to main, derive the next semantic version from the
conventional-commit messages since the last v* tag (feat -> minor,
fix/perf -> patch, \! or BREAKING CHANGE -> major) and, when a release is
warranted, build and publish the image tagged X.Y.Z, X.Y, X and latest,
then record an annotated vX.Y.Z tag for the next computation. Non-release
pushes publish a sha-<short> image only.

Configure Renovate to commit updates as fix(deps): so each merged Renovate
PR registers as a patch change and is released and tagged automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Emma Thorpe
2026-06-11 17:04:40 +01:00
parent d296d88c4c
commit 99084cc597
3 changed files with 122 additions and 33 deletions
+92 -25
View File
@@ -3,29 +3,99 @@ name: Build and publish container
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
defaults:
run:
shell: bash
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
contents: write
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history and tags are required to derive the next version
# from the conventional-commit messages since the last release.
fetch-depth: 0
- name: Determine registry host
run: echo "REGISTRY=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_ENV"
# Derive the release version from conventional commits since the last
# v* tag: feat -> minor, fix/perf -> patch, ! or BREAKING CHANGE -> major.
# Anything else (chore, ci, docs, build) produces no release; those builds
# are published under a sha-<short> tag only.
- name: Compute version and image tags
id: version
run: |
set -euo pipefail
image="${REGISTRY}/${GITHUB_REPOSITORY,,}"
last_tag="$(git tag --list 'v*' --sort=-v:refname | head -n1 || true)"
if [ -n "$last_tag" ]; then
range="${last_tag}..HEAD"
base="${last_tag#v}"
else
range=""
base="0.0.0"
fi
subjects="$(git log ${range} --format='%s')"
bodies="$(git log ${range} --format='%B')"
bump="none"
if printf '%s\n' "$bodies" | grep -qiE 'BREAKING[ -]CHANGE' \
|| printf '%s\n' "$subjects" | grep -qE '^[a-z]+([(][^)]*[)])?!:'; then
bump="major"
elif printf '%s\n' "$subjects" | grep -qE '^feat([(][^)]*[)])?:'; then
bump="minor"
elif printf '%s\n' "$subjects" | grep -qE '^(fix|perf)([(][^)]*[)])?:'; then
bump="patch"
fi
major="${base%%.*}"
rest="${base#*.}"
minor="${rest%%.*}"
patch="${rest##*.}"
release="false"
if [ "${GITHUB_EVENT_NAME}" = "push" ] && [ "$bump" != "none" ]; then
release="true"
case "$bump" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
esac
version="${major}.${minor}.${patch}"
{
echo "tags<<__EOT__"
echo "${image}:${version}"
echo "${image}:${major}.${minor}"
echo "${image}:${major}"
echo "${image}:latest"
echo "__EOT__"
} >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
else
short="$(git rev-parse --short HEAD)"
{
echo "tags<<__EOT__"
echo "${image}:sha-${short}"
echo "__EOT__"
} >> "$GITHUB_OUTPUT"
fi
echo "release=${release}" >> "$GITHUB_OUTPUT"
echo "Computed bump=${bump}, release=${release}, base=${base}"
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
# Uses a Personal Access Token with package read/write scope, stored as
# the PACKAGES_TOKEN secret. The auto-provided GITEA_TOKEN does not carry
# container-registry write permission on most Gitea instances.
- name: Log in to the Gitea container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
@@ -34,28 +104,25 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.PACKAGES_TOKEN }}
- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}
# Semantic version tags are produced from pushed v* git tags.
# `latest` follows the newest non-prerelease release via latest=auto.
# Branch and SHA tags are kept for traceability of non-release builds.
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch
type=ref,event=pr
type=sha
flavor: |
latest=auto
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.version.outputs.tags }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
# Record the release as an annotated git tag so the next run computes the
# following version from it. This push does not re-trigger the workflow,
# which only listens on the main branch and pull requests.
- name: Tag the release
if: steps.version.outputs.release == 'true'
run: |
set -euo pipefail
v="v${{ steps.version.outputs.version }}"
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.${REGISTRY}"
git tag -a "$v" -m "$v"
git push origin "$v"