mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
6d245ab4da
The stable release asks the issuer for the registration dialect of the version it cuts and commits it in the bump commit. Fetching and checking run in a job of their own that holds no write token and keeps no git credentials: scripts/ci/check-registration-dialect.mjs refuses a file that imports or names anything beyond hashing, then runs it on its self-check inputs. Only a file that passes reaches the bump as an artifact. When the issuer is unreachable or its answer fails the check, the release goes ahead with the dialect already on main and says so in the log and the summary. Prereleases get no dialect of their own; dev takes the stable one through the merge of main into dev after a release, and docker-dev.yml warns while dev still speaks the original registration or a dialect older than 150 days.
267 lines
11 KiB
YAML
267 lines
11 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.
|
|
#
|
|
# A prerelease gets no registration dialect of its own; it carries the one of
|
|
# the last stable release, which dev gets by merging main into dev after that
|
|
# release. The build warns when that dialect is the original one or old.
|
|
|
|
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 }}
|
|
|
|
# The dev push, the main push and any manual run each leave a ci-ok on
|
|
# this commit. scripts/ci/ci-ok-verdict.mjs counts the newest attempt of
|
|
# each, refuses while one is still going, and requires the one that
|
|
# finished last to be green, so a later green run supersedes a flaky or
|
|
# cancelled one. It prints the link of every run it counted.
|
|
- 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)"
|
|
runs="$RUNNER_TEMP/ci-ok-runs.json"
|
|
gh api "repos/$GITHUB_REPOSITORY/commits/$sha/check-runs?check_name=ci-ok&filter=all&per_page=100" > "$runs"
|
|
node scripts/ci/ci-ok-verdict.mjs "$runs" "$sha"
|
|
|
|
- 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"
|
|
|
|
# A prerelease registers new installations in the dialect of the last
|
|
# stable release, which reaches dev when main is merged into dev after
|
|
# the release. The issuer takes a dialect for 180 days, so this warns
|
|
# (without stopping the build) while dev still speaks the original
|
|
# registration or a dialect older than 150 days.
|
|
- name: Check the registration dialect's age
|
|
run: |
|
|
set -eu
|
|
file=server/src/nest/service-identity/registration-dialect.ts
|
|
id="$(sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$file")"
|
|
created="$(sed -n "s/^ createdAt: '\(.*\)',$/\1/p" "$file")"
|
|
if [ "$id" = 0 ]; then
|
|
message="This build registers in the original dialect, which the issuer accepts only for a transition period. Merge main into dev once a stable release carries a dialect of its own."
|
|
else
|
|
age=$(( ($(date -u +%s) - $(date -u -d "$created" +%s)) / 86400 ))
|
|
[ "$age" -gt 150 ] || { echo "This build registers in dialect $id from $created ($age days old)."; exit 0; }
|
|
message="This build registers in dialect $id from $created, $age days old; new installations of it stop registering on $(date -u -d "$created + 180 days" +%Y-%m-%d). Merge main into dev after the next stable release."
|
|
fi
|
|
echo "::warning::$message"
|
|
printf '### Registration dialect\n%s\n' "$message" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
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
|