From d296d88c4c452a207b37b1b9ad2be0cd042f1701 Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Thu, 11 Jun 2026 16:43:33 +0100 Subject: [PATCH 1/2] ci: tag images by semver and point latest at newest release Replace the raw latest-on-default-branch tag, which moved latest on every main push, with metadata-action's latest=auto flavor so latest follows the newest non-prerelease v* release. Add a {{major}} tag alongside the existing version and major.minor semver tags; branch and SHA tags remain for traceability of non-release builds. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/build-and-publish.yml | 11 ++++++++--- README.md | 7 +++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/build-and-publish.yml b/.gitea/workflows/build-and-publish.yml index 38ed3f9..1afd07a 100644 --- a/.gitea/workflows/build-and-publish.yml +++ b/.gitea/workflows/build-and-publish.yml @@ -39,13 +39,18 @@ jobs: 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=ref,event=branch - type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=ref,event=branch + type=ref,event=pr type=sha - type=raw,value=latest,enable={{is_default_branch}} + flavor: | + latest=auto - name: Build and push uses: docker/build-push-action@v6 diff --git a/README.md b/README.md index ca4ed26..99b5725 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,11 @@ registry write permission on most Gitea instances. Create the token under an account with write access to the target package namespace, then store it as a repository Actions secret named `PACKAGES_TOKEN`. -The published image is `//`, tagged by branch, semver -(for `v*` tags), commit SHA, and `latest` on the default branch. +The published image is `//`. Pushing a `v*` git tag +produces semantic-version tags (`{{version}}`, `{{major}}.{{minor}}`, +`{{major}}`), and `latest` is moved to that build when it is not a pre-release. +Non-release builds on `main` are tagged by branch name and commit SHA only, so +`latest` always points at the most recent release rather than the newest commit. ## Dependency updates -- 2.52.0 From 99084cc597e028dfc1cd9ffe596f7af0623333da Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Thu, 11 Jun 2026 17:04:40 +0100 Subject: [PATCH 2/2] feat: auto-release images from conventional commits on main 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- 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) --- .gitea/workflows/build-and-publish.yml | 117 +++++++++++++++++++------ README.md | 35 ++++++-- renovate.json | 3 + 3 files changed, 122 insertions(+), 33 deletions(-) diff --git a/.gitea/workflows/build-and-publish.yml b/.gitea/workflows/build-and-publish.yml index 1afd07a..091a2bd 100644 --- a/.gitea/workflows/build-and-publish.yml +++ b/.gitea/workflows/build-and-publish.yml @@ -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- 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" diff --git a/README.md b/README.md index 99b5725..d17cdda 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,8 @@ docker run --rm -p 8080:8080 dlr ## CI `.gitea/workflows/build-and-publish.yml` builds the container with Gitea Actions -and publishes it to this Gitea instance's container registry on pushes to `main` -and on `v*` tags. Pull requests build the image but do not push. The registry -host is derived from the Gitea server URL. +on every push to `main` and on pull requests. Pull requests build the image but +do not push. The registry host is derived from the Gitea server URL. Authentication requires a Personal Access Token with package read/write scope, because the automatically provided `GITEA_TOKEN` does not carry container @@ -55,11 +54,27 @@ registry write permission on most Gitea instances. Create the token under an account with write access to the target package namespace, then store it as a repository Actions secret named `PACKAGES_TOKEN`. -The published image is `//`. Pushing a `v*` git tag -produces semantic-version tags (`{{version}}`, `{{major}}.{{minor}}`, -`{{major}}`), and `latest` is moved to that build when it is not a pre-release. -Non-release builds on `main` are tagged by branch name and commit SHA only, so -`latest` always points at the most recent release rather than the newest commit. +### Automatic releases + +The published image is `//`. Releases are derived from +[Conventional Commits](https://www.conventionalcommits.org/). On each push to +`main`, the workflow inspects the commits since the last `v*` tag and computes +the next version: + +- `feat:` → minor bump, +- `fix:` / `perf:` → patch bump, +- `!` or `BREAKING CHANGE` → major bump, +- anything else (`chore`, `ci`, `docs`, `build`) → no release. + +When a release is warranted, the image is published with `X.Y.Z`, `X.Y`, `X` and +`latest` tags, and the workflow records an annotated `vX.Y.Z` git tag so the next +release is computed from it. Pushes to `main` that warrant no release are +published under a `sha-` tag only, so `latest` always points at the most +recent release rather than the newest commit. + +Recording the release tag requires the workflow's `contents: write` permission; +if the instance forbids the automatic token from pushing, supply a PAT with +repository write scope and push the tag with it instead. ## Dependency updates @@ -80,6 +95,10 @@ CDN, Renovate will track it if it is either annotated with a comment, e.g. or referenced through a versioned jsDelivr / unpkg npm URL, which is detected automatically. +Renovate is configured to commit updates as `fix(deps): …`. Each merged Renovate +PR therefore registers as a patch-level change, so the release workflow above +cuts a new patch release and tags the image automatically. + ## Files | File | Purpose | diff --git a/renovate.json b/renovate.json index f0af024..5e4e043 100644 --- a/renovate.json +++ b/renovate.json @@ -6,6 +6,9 @@ ":semanticCommits" ], "labels": ["renovate"], + "semanticCommits": "enabled", + "semanticCommitType": "fix", + "semanticCommitScope": "deps", "github-actions": { "fileMatch": ["^\\.gitea/workflows/[^/]+\\.ya?ml$"] }, -- 2.52.0