From 0068075fbace563c2aca7d7a5c78a2e8b2a84a80 Mon Sep 17 00:00:00 2001 From: Matthias Linhuber Date: Thu, 27 Aug 2026 00:03:45 +0200 Subject: [PATCH] feat: shared tag-format check The org has three spellings for the same thing - 1.1.0, v1.1.0 and v.1.1.1 - so nothing can reliably answer "which release produced this image". One spelling from here on: the git tag is vX.Y.Z, and the image tag and chart version are that string without the v. Reusable rather than copied into six repos, so the grammar lives in one place. The error message names the specific mistake, because the three bad forms are each one character away from correct. tests/test-tag-format.sh extracts the pattern from the workflow rather than restating it, and exercises it against the tags this org has actually produced. Existing history is not retagged; this only stops new ones. --- .github/workflows/check-tag-format.yml | 71 ++++++++++++++++++++++++++ .github/workflows/ci.yml | 12 +++++ tests/test-tag-format.sh | 33 ++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 .github/workflows/check-tag-format.yml create mode 100755 tests/test-tag-format.sh diff --git a/.github/workflows/check-tag-format.yml b/.github/workflows/check-tag-format.yml new file mode 100644 index 0000000..9fd9df8 --- /dev/null +++ b/.github/workflows/check-tag-format.yml @@ -0,0 +1,71 @@ +# Reject a git tag that does not name a version. +# +# The org has accumulated three spellings for the same thing - `1.1.0`, +# `v1.1.0` and `v.1.1.1` - which is why nothing could reliably resolve "the +# release that produced this image". One spelling is enforced from here on: +# +# git tag vX.Y.Z v2.0.0, v2.1.0-rc.1 +# image tag X.Y.Z 2.0.0 +# chart version X.Y.Z 2.0.0 +# +# so `appVersion` and the image tag are literally the same string, and the git +# tag is that string with a `v`. Existing history is not retagged; this only +# stops new ones. +# +# Called by each repo on a tag push. Reusable rather than copied so the grammar +# lives in one place. + +name: Check tag format + +on: + workflow_call: + inputs: + tag: + description: "Tag to check. Defaults to the ref that triggered the caller." + type: string + required: false + default: "" + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Validate + env: + INPUT_TAG: ${{ inputs.tag }} + CALLER_REF: ${{ github.ref }} + run: | + set -euo pipefail + + TAG="$INPUT_TAG" + if [[ -z "$TAG" ]]; then + case "$CALLER_REF" in + refs/tags/*) TAG="${CALLER_REF#refs/tags/}" ;; + *) echo "::notice::not a tag ref (${CALLER_REF}); nothing to check"; exit 0 ;; + esac + fi + + echo "checking: ${TAG}" + + # vMAJOR.MINOR.PATCH, optionally -prerelease and +build. + # Deliberately strict: no `v.`, no bare `1.1.0`, no `release-1.1.0`. + SEMVER='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' + if [[ "$TAG" =~ $SEMVER ]]; then + echo "::notice::${TAG} is a valid release tag" + exit 0 + fi + + echo "::error::'${TAG}' is not a valid release tag." + echo "::error::Expected vMAJOR.MINOR.PATCH, e.g. v2.0.0 or v2.1.0-rc.1." + case "$TAG" in + v.*) echo "::error:: '${TAG}' has a dot after the v. Use v${TAG#v.}" ;; + [0-9]*) echo "::error:: '${TAG}' is missing the leading v. Use v${TAG}" ;; + V*) echo "::error:: the v is lowercase. Use v${TAG#V}" ;; + esac + echo "::error::The image tag and chart version are the same string without the v," + echo "::error::so a tag that does not parse leaves nothing able to resolve which" + echo "::error::release produced a given image." + exit 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39ce63b..080981a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,3 +32,15 @@ jobs: sudo chmod +x /usr/local/bin/yq - name: Run tests run: ./tests/test-derive-tags.sh + + tag-format: + name: Tag format tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install yq + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + - name: Run tests + run: ./tests/test-tag-format.sh diff --git a/tests/test-tag-format.sh b/tests/test-tag-format.sh new file mode 100755 index 0000000..a44eb2a --- /dev/null +++ b/tests/test-tag-format.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# The grammar in check-tag-format.yml, exercised against the tags the org has +# actually produced. Extracted from the workflow rather than restated, so the +# two cannot drift. + +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WF="$ROOT/.github/workflows/check-tag-format.yml" +FAILED=0 +ok() { printf ' PASS %s\n' "$1"; } +bad() { printf ' FAIL %s\n' "$1"; FAILED=1; } + +SEMVER=$(yq -r '.jobs.check.steps[0].run' "$WF" | grep -oE "SEMVER='.*'" | sed "s/SEMVER='//;s/'$//") +[[ -n "$SEMVER" ]] || { echo "could not extract the pattern from $WF"; exit 1; } +echo "pattern: $SEMVER" +echo + +accept() { [[ "$1" =~ $SEMVER ]]; } + +echo "=== must accept ===" +for t in v2.0.0 v1.1.0 v2.1.0-rc.1 v10.20.30 v1.0.0-next.7 v0.1.0 v2.0.0-pr.24.abc1234; do + accept "$t" && ok "$t" || bad "should accept $t" +done + +echo +echo "=== must reject (all three spellings this org has actually used) ===" +for t in 1.1.0 v.1.1.1 V2.0.0 release-1.1.0 v1.1 v1.1.0.1 latest main "v1.1.0 " "" theia-workspace-garbage-collector-0.1.0; do + accept "$t" && bad "should reject '${t:-}'" || ok "rejects '${t:-}'" +done + +echo +[[ $FAILED -eq 0 ]] && echo "ALL PASS" || echo "SOME FAILED" +exit $FAILED