From bd0a52269b77a9be7b60ed4e6b397c500ba0323a Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sat, 14 Nov 2026 14:56:40 -0800 Subject: [PATCH] CI: only main publishes :latest, and only from a tag on main A dispatch from another branch, or a v* tag cut on a branch, could otherwise become the image every user pulls. An older tag on main is still accepted so a rollback works. Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/build-stt.yml | 13 +++++++++++++ .gitea/workflows/build.yml | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/.gitea/workflows/build-stt.yml b/.gitea/workflows/build-stt.yml index 8f58e48..571faa2 100644 --- a/.gitea/workflows/build-stt.yml +++ b/.gitea/workflows/build-stt.yml @@ -41,6 +41,7 @@ jobs: env: EVENT: ${{ gitea.event_name }} RELEASE_TAG: ${{ gitea.event.inputs.release_tag }} + REF: ${{ gitea.ref }} run: | set -euo pipefail case "$EVENT" in @@ -49,7 +50,15 @@ jobs: ;; schedule|workflow_dispatch) CHANNEL=latest + # `:latest` reaches every user, so it only comes from main: a + # dispatch from another branch, or a tag cut on one, is refused. + # An older release tag is allowed on purpose — that is a rollback. + if [ "$REF" != "refs/heads/main" ]; then + echo "Refusing to publish :latest from $REF (only main)" >&2 + exit 1 + fi git fetch --tags --force + git fetch origin main if [ -z "${RELEASE_TAG:-}" ]; then RELEASE_TAG=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1) fi @@ -57,6 +66,10 @@ jobs: echo "Not a release tag: '$RELEASE_TAG'" >&2 exit 1 fi + if ! git merge-base --is-ancestor "refs/tags/$RELEASE_TAG" FETCH_HEAD; then + echo "$RELEASE_TAG is not on main; refusing to publish it as :latest" >&2 + exit 1 + fi git checkout --detach "refs/tags/$RELEASE_TAG" echo "Building release $RELEASE_TAG" ;; diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index b668e79..d2ea308 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -46,6 +46,7 @@ jobs: env: EVENT: ${{ gitea.event_name }} RELEASE_TAG: ${{ gitea.event.inputs.release_tag }} + REF: ${{ gitea.ref }} run: | set -euo pipefail case "$EVENT" in @@ -54,7 +55,15 @@ jobs: ;; schedule|workflow_dispatch) CHANNEL=latest + # `:latest` reaches every user, so it only comes from main: a + # dispatch from another branch, or a tag cut on one, is refused. + # An older release tag is allowed on purpose — that is a rollback. + if [ "$REF" != "refs/heads/main" ]; then + echo "Refusing to publish :latest from $REF (only main)" >&2 + exit 1 + fi git fetch --tags --force + git fetch origin main if [ -z "${RELEASE_TAG:-}" ]; then RELEASE_TAG=$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1) fi @@ -62,6 +71,10 @@ jobs: echo "Not a release tag: '$RELEASE_TAG'" >&2 exit 1 fi + if ! git merge-base --is-ancestor "refs/tags/$RELEASE_TAG" FETCH_HEAD; then + echo "$RELEASE_TAG is not on main; refusing to publish it as :latest" >&2 + exit 1 + fi git checkout --detach "refs/tags/$RELEASE_TAG" echo "Building release $RELEASE_TAG" ;;