ci: tag images by semver and point latest at newest release #3
@@ -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"
|
||||
|
||||
@@ -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 `<gitea-host>/<owner>/<repo>`. 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 `<gitea-host>/<owner>/<repo>`. 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-<short>` 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 |
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
":semanticCommits"
|
||||
],
|
||||
"labels": ["renovate"],
|
||||
"semanticCommits": "enabled",
|
||||
"semanticCommitType": "fix",
|
||||
"semanticCommitScope": "deps",
|
||||
"github-actions": {
|
||||
"fileMatch": ["^\\.gitea/workflows/[^/]+\\.ya?ml$"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user