#!/usr/bin/env bash # # Resolve the version this push should release (t42). # # Why a script: this decision is what makes a release visible to HACS, and every # wrong answer is silent — the tag and the release still exist, only the manifest # never moves. So the logic lives here, where tests/test_release_versioning.py # drives every branch locally, instead of inline in a workflow nobody can run. # # Resolution (M = manifest version, D = today in UTC, tags = existing v-tags): # 1. M is tagged AND that tag is the commit being released # -> RELEASE=no (SOURCE=released): a re-run, this commit is already out # 2. M is not tagged -> VERSION=M (SOURCE=manifest) # an unreleased manifest version wins: bump it to force a release # 3. D is not tagged -> VERSION=D (SOURCE=date) # the one automatic release per UTC day # 4. otherwise -> VERSION=D.N (SOURCE=same-day) # N is the LOWEST untagged N >= 2, so further releases in the same UTC day # become 2026.10.11.2, 2026.10.11.3, … instead of silently doing nothing # # The workflow writes the resolved VERSION back into the manifest whenever it # differs, then tags THAT commit, so the released artifact and its tag always # agree. The defect this fixes: v2026.10.09 shipped while master's manifest still # said 2026.10.08, leaving HACS comparing against a version that never moved. # # Usage: scripts/determine-release-version.sh [--manifest PATH] [--today YYYY.MM.DD] # [--tags-from FILE] [--head SHA] # # --manifest manifest to read (default: custom_components/nvidia_shield/manifest.json) # --today override the UTC date — for tests and dry runs only # --tags-from read "TAG [SHA]" lines instead of asking git — for tests and dry runs # --head the commit being released (default: git rev-parse HEAD) # # Stdout, and $GITHUB_OUTPUT as well when it is set (which is what the workflow # consumes): VERSION, TAG, SOURCE, BUMP, RELEASE. Commentary goes to stderr. # RELEASE=no is the explicit no-op signal: nothing may be tagged or published. # Exit status is 1 only for a usage or manifest error, so "no release" is not a # failed job. set -euo pipefail MANIFEST="custom_components/nvidia_shield/manifest.json" TAGS_FROM="" HEAD_SHA="" TODAY="" usage() { sed -n '/^# Usage:/,/^# Exit status/p' "$0" | sed 's/^# \{0,1\}//' } while [[ $# -gt 0 ]]; do case "$1" in --manifest) MANIFEST="${2:?--manifest needs a path}"; shift 2 ;; --today) TODAY="${2:?--today needs YYYY.MM.DD}"; shift 2 ;; --tags-from) TAGS_FROM="${2:?--tags-from needs a path}"; shift 2 ;; --head) HEAD_SHA="${2:?--head needs a sha}"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 1 ;; esac done note() { printf '%s\n' "$*" >&2; } [[ -f "$MANIFEST" ]] || { note "manifest not found: $MANIFEST"; exit 1; } command -v jq >/dev/null 2>&1 || { note "jq is required to read the manifest"; exit 1; } MANIFEST_VERSION="$(jq -r '.version // empty' "$MANIFEST")" [[ -n "$MANIFEST_VERSION" ]] || { note "no .version in $MANIFEST"; exit 1; } # Tolerate a hand-written leading "v" (the tag adds its own); anything else that # is not YYYY.MM.DD[.N] is a mistake worth failing for. MANIFEST_VERSION="${MANIFEST_VERSION#v}" [[ "$MANIFEST_VERSION" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?$ ]] || { note "manifest version '$MANIFEST_VERSION' is not YYYY.MM.DD[.N]"; exit 1; } if [[ -n "$TODAY" ]]; then [[ "$TODAY" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}$ ]] || { note "--today must be YYYY.MM.DD"; exit 1; } DATE_VERSION="$TODAY" else # UTC, always: a runner's local timezone must never decide a version number. DATE_VERSION="$(date -u +%Y.%m.%d)" fi if [[ -n "$TAGS_FROM" ]]; then [[ -f "$TAGS_FROM" ]] || { note "tag list not found: $TAGS_FROM"; exit 1; } else command -v git >/dev/null 2>&1 || { note "git is required to list tags"; exit 1; } fi tag_exists() { if [[ -n "$TAGS_FROM" ]]; then awk -v wanted="$1" '$1 == wanted { found = 1 } END { exit(found ? 0 : 1) }' "$TAGS_FROM" else git rev-parse -q --verify "refs/tags/$1" >/dev/null fi } tag_sha() { if [[ -n "$TAGS_FROM" ]]; then awk -v wanted="$1" '$1 == wanted && NF > 1 { print $2; found = 1 } END { exit(found ? 0 : 1) }' "$TAGS_FROM" else git rev-parse -q --verify "refs/tags/$1^{commit}" fi } if [[ -z "$HEAD_SHA" ]] && [[ -z "$TAGS_FROM" ]]; then HEAD_SHA="$(git rev-parse -q --verify HEAD 2>/dev/null || true)" fi VERSION="" SOURCE="" RELEASE="yes" if tag_exists "v${MANIFEST_VERSION}" && [[ -n "$HEAD_SHA" ]] \ && [[ "$(tag_sha "v${MANIFEST_VERSION}" 2>/dev/null || true)" == "$HEAD_SHA" ]]; then VERSION="$MANIFEST_VERSION"; SOURCE="released"; RELEASE="no" note "v${VERSION} is already tagged on this commit — nothing to release" elif ! tag_exists "v${MANIFEST_VERSION}"; then VERSION="$MANIFEST_VERSION"; SOURCE="manifest" elif ! tag_exists "v${DATE_VERSION}"; then VERSION="$DATE_VERSION"; SOURCE="date" else N=2 while tag_exists "v${DATE_VERSION}.${N}"; do N=$((N + 1)); done VERSION="${DATE_VERSION}.${N}"; SOURCE="same-day" fi if [[ "$RELEASE" == "yes" ]] && tag_exists "v${VERSION}"; then # Backstop, not a normal path: the resolution above only ever picks an # untagged version, so reaching this means the tag set changed underneath us # (a race with a parallel run — the concurrency group is the primary guard). # Never tag or publish something that already exists. RELEASE="no"; SOURCE="tagged" note "v${VERSION} already exists — nothing to release" fi BUMP="no" if [[ "$VERSION" != "$MANIFEST_VERSION" ]]; then BUMP="yes" fi if [[ "$RELEASE" == "yes" ]]; then note "releasing v${VERSION} (${SOURCE}); manifest says ${MANIFEST_VERSION}, bump=${BUMP}" else note "no release (${SOURCE})" fi emit() { printf '%s=%s\n' "$1" "$2" if [[ -n "${GITHUB_OUTPUT:-}" ]]; then printf '%s=%s\n' "$1" "$2" >> "$GITHUB_OUTPUT" fi } emit VERSION "$VERSION" emit TAG "v${VERSION}" emit SOURCE "$SOURCE" emit BUMP "$BUMP" emit RELEASE "$RELEASE"