Files
TREK/.github/workflows/docker-dev.yml
T
Maurice d88dc17e8e ci(release): build the bump commit, add a minor tag, make the bump an explicit choice
docker.yml built and published whatever `main` pointed to when the build
and merge jobs started, not the commit it had just bumped and tagged, so
anything that landed in between shipped under a version that did not
contain it. build, merge and release-helm now check out the bump commit,
the way docker-dev.yml already pinned its prerelease builds. The push of
that commit fast-forwards main from the commit the run started on, so a
race fails the release instead of skipping over it.

Nothing tied a release to a green CI run. Both release workflows now
refuse a commit unless every ci-ok check run on it has finished green,
and docker.yml refuses to run on anything but main.

bump=auto fell back to a patch whenever no prerelease was in flight,
which would have published the ORM release, the largest change the
project has made, as 4.3.4. auto now only finalizes a prerelease above
the last stable tag and otherwise stops, asking for patch, minor or
major. The inputs are unchanged. A patch that adds migrations gets a
warning, not a refusal, since patch releases have carried them before.

Stable releases also publish :MAJOR.MINOR, so an install can take the
fixes of one minor line without being moved onto the next on day one.
The tag tables in the wiki list it and the prerelease tag, and the root
CLAUDE.md describes the release and prerelease flow.
2026-10-08 13:04:43 +02:00

249 lines
9.3 KiB
YAML

name: Build & Push Docker Image (Prerelease)
# The canary. Dispatched by hand, usually on dev, it builds the dispatched
# commit as <next minor or major>-pre.N, publishes :VERSION, :MAJOR-pre and
# :latest-pre, and tags the commit. Nothing moves :latest. Like the stable
# release it refuses a commit whose ci-ok is not green. docker.yml with
# bump=auto later finalizes the line once main carries it.
on:
workflow_dispatch:
inputs:
bump:
description: 'Bump line for next prerelease (auto detects in-flight major)'
type: choice
options: [auto, minor, major]
default: auto
permissions:
contents: write
checks: read
concurrency:
group: prerelease-build
cancel-in-progress: false
jobs:
version-bump:
if: github.repository == 'liketrek/TREK'
runs-on: ubuntu-24.04
timeout-minutes: 120
outputs:
version: ${{ steps.bump.outputs.VERSION }}
sha: ${{ steps.bump.outputs.SHA }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# Every check run named ci-ok on this commit has to have finished green.
# A run still going counts as not green: wait for it and dispatch again.
- name: Require a green ci-ok on the commit being built
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
sha="$(git rev-parse HEAD)"
conclusions="$(gh api "repos/$GITHUB_REPOSITORY/commits/$sha/check-runs?check_name=ci-ok&per_page=100" \
--jq '[.check_runs[] | .conclusion // "pending"] | join(" ")')"
echo "ci-ok on $sha: ${conclusions:-none}"
if [ -z "$conclusions" ]; then
echo "::error::ci-ok never ran on $sha. Run Tests & SonarQube on this branch and dispatch again."
exit 1
fi
for c in $conclusions; do
if [ "$c" != "success" ]; then
echo "::error::ci-ok on $sha is not green ($conclusions)."
exit 1
fi
done
- name: Determine prerelease version
id: bump
env:
BUMP_INPUT: ${{ inputs.bump || 'auto' }}
run: |
git fetch --tags
# Capture the exact commit we're building so build/merge jobs are pinned to it
echo "SHA=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
# Get latest stable tag (exclude prerelease tags)
STABLE_TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | grep -v '\-pre\.' | sort -V | tail -1)
STABLE="${STABLE_TAG#v}"
STABLE="${STABLE:-0.0.0}"
echo "Latest stable: $STABLE"
IFS='.' read -r MAJOR MINOR _ <<< "$STABLE"
# Detect any in-flight major prerelease (v(MAJOR+1).0.0-pre.*). Stay on that line if found.
NEXT_MAJOR="$((MAJOR + 1)).0.0"
MAJOR_PRE_EXISTS=$(git tag -l "v${NEXT_MAJOR}-pre.*" | head -1)
if [ "$BUMP_INPUT" = "major" ] || { [ "$BUMP_INPUT" = "auto" ] && [ -n "$MAJOR_PRE_EXISTS" ]; }; then
TARGET="$NEXT_MAJOR"
else
TARGET="${MAJOR}.$((MINOR + 1)).0"
fi
echo "Target: $TARGET"
# Find the highest existing prerelease N for this target and increment
LAST_N=$(git tag -l "v${TARGET}-pre.*" | sed 's/.*-pre\.//' | sort -n | tail -1)
N=$(( ${LAST_N:-0} + 1 ))
NEW_VERSION="${TARGET}-pre.${N}"
echo "VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "$STABLE to $NEW_VERSION"
build:
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
needs: version-bump
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: Prepare platform tag-safe name
env:
PLATFORM: ${{ matrix.platform }}
run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV"
- uses: actions/checkout@v6
with:
ref: ${{ needs.version-bump.outputs.sha }}
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Image metadata
id: meta
run: |
echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Build and push by digest
id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
platforms: ${{ matrix.platform }}
outputs: type=image,name=mauriceboe/trek,push-by-digest=true,name-canonical=true,push=true
no-cache: true
build-args: |
APP_VERSION=${{ needs.version-bump.outputs.version }}
labels: |
org.opencontainers.image.revision=${{ steps.meta.outputs.revision }}
org.opencontainers.image.created=${{ steps.meta.outputs.created }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest artifact
uses: actions/upload-artifact@v6
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-24.04
timeout-minutes: 120
needs: [version-bump, build]
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.version-bump.outputs.sha }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download build digests
uses: actions/download-artifact@v7
with:
path: /tmp/digests
pattern: digests-*
# Deliberately NOT merged: the per-platform folder names are what lets
# the manifest below be assembled in a fixed order.
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push multi-arch manifest
working-directory: /tmp/digests
run: |
set -euo pipefail
VERSION="${{ needs.version-bump.outputs.version }}"
MAJOR_TAG="$(echo "$VERSION" | cut -d. -f1)-pre"
# amd64 first, always. This used to be a glob over hash-named files, so
# the order changed from one release to the next: 3.4.1 listed amd64
# first and 4.0.0 listed arm64 first. Clients that read the first entry
# of the index rather than matching their own platform then compare the
# wrong digest and report "no update" — which is what Unraid users hit
# on :latest after 4.0.0 shipped. The image was fine; the order was not.
digests=()
for platform in linux-amd64 linux-arm64; do
dir="digests-$platform"
[ -d "$dir" ] || { echo "::error::missing digest artifact for $platform"; exit 1; }
for f in "$dir"/*; do
[ -e "$f" ] || { echo "::error::no digest file in $dir"; exit 1; }
digests+=("mauriceboe/trek@sha256:$(basename "$f")")
done
done
# The labels live on each platform image; the index gets the ones a
# client reads before it picks a platform (#1498).
docker buildx imagetools create \
--annotation "index:org.opencontainers.image.source=https://github.com/liketrek/TREK" \
--annotation "index:org.opencontainers.image.version=$VERSION" \
--annotation "index:org.opencontainers.image.licenses=AGPL-3.0-only" \
-t "mauriceboe/trek:latest-pre" \
-t "mauriceboe/trek:$MAJOR_TAG" \
-t "mauriceboe/trek:$VERSION" \
"${digests[@]}"
- name: Inspect manifest
run: docker buildx imagetools inspect mauriceboe/trek:latest-pre
- name: Push git tag
run: |
VERSION="${{ needs.version-bump.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v$VERSION"
git push origin "v$VERSION"
- name: Clean up old prerelease tags
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
KEEP=20
VERSION="${{ needs.version-bump.outputs.version }}"
BASE_VERSION="${VERSION%%-pre.*}"
git fetch --tags
# Sort by numeric prerelease N (field after -pre.) to get correct ascending order
mapfile -t ALL_TAGS < <(git tag -l "v${BASE_VERSION}-pre.*" | awk -F'-pre\\.' '{print $2" "$0}' | sort -n | awk '{print $2}')
TOTAL=${#ALL_TAGS[@]}
DELETE_COUNT=$((TOTAL - KEEP))
if [ "$DELETE_COUNT" -gt 0 ]; then
for TAG in "${ALL_TAGS[@]:0:$DELETE_COUNT}"; do
echo "Deleting old prerelease tag: $TAG"
git push origin --delete "$TAG"
done
fi