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