From 4de543c146195baafc510122a5de0513543c653a Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 3 Jul 2026 22:40:25 +0000 Subject: [PATCH] ci(release): derive canary version from git tags (single source of truth) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every release workflow hardcoded a canary base version (0.5.0 in Apple/Android/rpm/flatpak/deb, 0.3 in windows-msix/windows-host/decky) that had to be hand-bumped on each stable release and wasn't. With stable at v0.6.0, every canary was a version *behind* stable — e.g. the Apple canary showed up on TestFlight as 0.5.0 while 0.6.0 was already published. Add scripts/ci/pf-version.{sh,ps1} (bash + pwsh twin) as the single source of truth: stable = the vX.Y.Z tag; canary = latest stable tag with minor+1, patch 0 (v0.6.0 -> 0.7.0), so canary is always exactly one minor ahead of the newest release with zero maintenance. Falls back to the workspace Cargo.toml version when no tag is fetchable. All workflows now eval/call it and format their own channel suffix off $PF_BASE; only the canary branch changed, stable branches and per-channel suffixes are untouched. channels.md drops the old manual "bump the canary base" release step. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/android.yml | 3 +- .gitea/workflows/deb.yml | 11 +++-- .gitea/workflows/decky.yml | 8 ++- .gitea/workflows/release.yml | 5 +- .gitea/workflows/rpm.yml | 11 +++-- .gitea/workflows/windows-host.yml | 7 ++- .gitea/workflows/windows-msix.yml | 7 ++- docs-site/content/docs/channels.md | 22 +++++++-- scripts/ci/pf-version.ps1 | 55 +++++++++++++++++++++ scripts/ci/pf-version.sh | 79 ++++++++++++++++++++++++++++++ 10 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 scripts/ci/pf-version.ps1 create mode 100644 scripts/ci/pf-version.sh diff --git a/.gitea/workflows/android.yml b/.gitea/workflows/android.yml index 665fae2..d9b2248 100644 --- a/.gitea/workflows/android.yml +++ b/.gitea/workflows/android.yml @@ -78,9 +78,10 @@ jobs: - name: Version + channel if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) run: | + eval "$(bash scripts/ci/pf-version.sh)" # -> PF_BASE (one minor ahead of the latest stable tag) case "$GITHUB_REF" in refs/tags/v*) VN="${GITHUB_REF_NAME#v}"; TRACK="alpha" ;; # alpha = built-in closed testing - *) VN="0.5.0-ci${GITHUB_RUN_NUMBER}"; TRACK="internal" ;; + *) VN="${PF_BASE}-ci${GITHUB_RUN_NUMBER}"; TRACK="internal" ;; esac echo "VERSION_NAME=$VN" >> "$GITHUB_ENV" echo "PLAY_TRACK=$TRACK" >> "$GITHUB_ENV" diff --git a/.gitea/workflows/deb.yml b/.gitea/workflows/deb.yml index c42a9a6..865ab4a 100644 --- a/.gitea/workflows/deb.yml +++ b/.gitea/workflows/deb.yml @@ -36,16 +36,17 @@ jobs: - name: Version + channel # vX.Y.Z tag -> X.Y.Z, published to the `stable` apt distribution (a real release). - # A main push -> 0.5.0~ciN.g, published to the `canary` distribution: the '~' sorts - # below the eventual 0.5.0 tag, it climbs monotonically by run number, and the canary base - # stays one minor AHEAD of the latest stable so a stable->canary box re-point still moves - # forward (see channels.md). Computed BEFORE the build so it's stamped into the binary + # A main push -> ~ciN.g, published to the `canary` distribution: the '~' sorts + # below the eventual tag, it climbs monotonically by run number, and the canary base is + # derived one minor AHEAD of the latest stable tag (scripts/ci/pf-version.sh) so a + # stable->canary box re-point still moves forward (see channels.md). Computed BEFORE the build so it's stamped into the binary # (PUNKTFUNK_BUILD_VERSION -> build.rs -> --version). run: | + eval "$(bash scripts/ci/pf-version.sh)" # -> PF_BASE (one minor ahead of the latest stable tag) SHORT=$(echo "$GITHUB_SHA" | cut -c1-8) case "$GITHUB_REF" in refs/tags/v*) V="${GITHUB_REF_NAME#v}"; DIST=stable ;; - *) V="0.5.0~ci${GITHUB_RUN_NUMBER}.g${SHORT}"; DIST=canary ;; + *) V="${PF_BASE}~ci${GITHUB_RUN_NUMBER}.g${SHORT}"; DIST=canary ;; esac echo "VERSION=$V" >> "$GITHUB_ENV" echo "DISTRIBUTION=$DIST" >> "$GITHUB_ENV" diff --git a/.gitea/workflows/decky.yml b/.gitea/workflows/decky.yml index 0270a86..f410fe4 100644 --- a/.gitea/workflows/decky.yml +++ b/.gitea/workflows/decky.yml @@ -63,7 +63,8 @@ jobs: pnpm run build # rollup -> clients/decky/dist/index.js - name: Version + channel + stamp - # Tag vX.Y.Z -> X.Y.Z (stable `latest/` alias + Gitea Release); main push -> 0.3. + # Tag vX.Y.Z -> X.Y.Z (stable `latest/` alias + Gitea Release); main push -> . + # (base one minor ahead of the latest stable tag via scripts/ci/pf-version.sh) # (`canary/` alias). Decky reads a plugin's INSTALLED version from package.json (NOT # plugin.json), and the plugin's own update check (clients/decky/main.py check_update) # compares against it — so the build version is STAMPED into package.json here (mirrored @@ -72,9 +73,12 @@ jobs: # (ci10 < ci9), which would break update detection; the run number is monotonic. working-directory: ${{ gitea.workspace }} run: | + eval "$(bash scripts/ci/pf-version.sh)" # -> PF_MAJOR/PF_MINOR (base one minor ahead of latest stable) case "$GITHUB_REF" in refs/tags/v*) V="${GITHUB_REF_NAME#v}"; ALIAS=latest ;; - *) V="0.3.${GITHUB_RUN_NUMBER}"; ALIAS=canary ;; + # Canary MUST be a plain monotonic numeric semver (see the note above): .., + # where major.minor track one minor ahead of the latest stable and the run number climbs. + *) V="${PF_MAJOR}.${PF_MINOR}.${GITHUB_RUN_NUMBER}"; ALIAS=canary ;; esac BASE="https://$REGISTRY/api/packages/$OWNER/generic/$PACKAGE" echo "VERSION=$V" >> "$GITHUB_ENV" diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index cb6df66..7b3eabd 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -99,13 +99,14 @@ jobs: - name: Version from tag run: | + eval "$(bash scripts/ci/pf-version.sh)" # -> PF_BASE, PF_CHANNEL, PF_STABLE_TAG (single source of truth) case "$GITHUB_REF" in refs/tags/v*) V="${GITHUB_REF_NAME#v}"; V="${V%%-*}" ;; # App Store marketing version is numeric X.Y.Z (drop -rc) - *) V="0.5.0" ;; # canary marketing version; the build number disambiguates + *) V="$PF_BASE" ;; # canary marketing version = one minor ahead of the latest stable tag; the build number disambiguates esac echo "VERSION=$V" >> "$GITHUB_ENV" echo "BUILD_NUM=$GITHUB_RUN_NUMBER" >> "$GITHUB_ENV" - echo "version $V build $GITHUB_RUN_NUMBER" + echo "version $V build $GITHUB_RUN_NUMBER (channel $PF_CHANNEL, latest stable ${PF_STABLE_TAG})" - name: Rust toolchain (mac + iOS + tvOS slices) run: | diff --git a/.gitea/workflows/rpm.yml b/.gitea/workflows/rpm.yml index 64048bc..6b1bf69 100644 --- a/.gitea/workflows/rpm.yml +++ b/.gitea/workflows/rpm.yml @@ -68,16 +68,17 @@ jobs: restore-keys: cargo-home- - name: Version + channel - # vX.Y.Z tag -> X.Y.Z-1 in the base group (a real release); main push -> 0.5.0-0.ciN.g - # in the `-canary` group, whose "0." release sorts below the eventual 0.5.0-1 yet - # climbs by run number. The canary base stays one minor ahead of the latest stable so a - # stable->canary box re-point still moves forward. The spec %build stamps + # vX.Y.Z tag -> X.Y.Z-1 in the base group (a real release); main push -> -0.ciN.g + # in the `-canary` group, whose "0." release sorts below the eventual -1 yet + # climbs by run number. The canary base is derived one minor ahead of the latest stable tag + # (scripts/ci/pf-version.sh) so a stable->canary box re-point still moves forward. The spec %build stamps # PUNKTFUNK_BUILD_VERSION from these macros into the binary (--version provenance). run: | + eval "$(bash scripts/ci/pf-version.sh)" # -> PF_BASE (one minor ahead of the latest stable tag) SHORT=$(echo "$GITHUB_SHA" | cut -c1-8) case "$GITHUB_REF" in refs/tags/v*) V="${GITHUB_REF_NAME#v}"; R="1"; GROUP="${{ matrix.group }}" ;; - *) V="0.5.0"; R="0.ci${GITHUB_RUN_NUMBER}.g${SHORT}"; GROUP="${{ matrix.group }}-canary" ;; + *) V="$PF_BASE"; R="0.ci${GITHUB_RUN_NUMBER}.g${SHORT}"; GROUP="${{ matrix.group }}-canary" ;; esac echo "PF_VERSION=$V" >> "$GITHUB_ENV" echo "PF_RELEASE=$R" >> "$GITHUB_ENV" diff --git a/.gitea/workflows/windows-host.yml b/.gitea/workflows/windows-host.yml index 49484cd..78831ac 100644 --- a/.gitea/workflows/windows-host.yml +++ b/.gitea/workflows/windows-host.yml @@ -16,7 +16,8 @@ # Versioning (free-form; not MSIX's 4-part rule) — single project version: # vX.Y.Z tag -> X.Y.Z (THE release; published + stable `latest/` alias + attached to the # unified Gitea Release). -# main push / dispatch -> 0.3. (canary; `canary/` alias; climbs by run number). +# main push / dispatch -> . (canary; `canary/` alias; base one minor +# ahead of the latest stable tag via scripts/ci/pf-version.ps1, run climbs). # # Signing reuses the client's MSIX_CERT_PFX_B64 / MSIX_CERT_PASSWORD secrets (CN=unom). Without them # an ephemeral self-signed cert is generated and its public .cer published next to the installer @@ -102,10 +103,12 @@ jobs: if (-not $env:VBCABLE_DIR) { "VBCABLE_DIR=C:\Users\Public\vbcable" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 } + $pf = & "$env:GITHUB_WORKSPACE/scripts/ci/pf-version.ps1" # single source of truth: base is one minor ahead of the latest stable tag $v = if ($env:GITHUB_REF -like 'refs/tags/v*') { $env:GITHUB_REF_NAME -replace '^v', '' } else { - "0.3.$($env:GITHUB_RUN_NUMBER)" + # Canary: .. — major.minor track one minor ahead of stable, run climbs monotonically. + "$($pf.PF_MAJOR).$($pf.PF_MINOR).$($env:GITHUB_RUN_NUMBER)" } "HOST_VERSION=$v" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "PUNKTFUNK_BUILD_VERSION=$v" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 diff --git a/.gitea/workflows/windows-msix.yml b/.gitea/workflows/windows-msix.yml index 520f8db..6cc2c82 100644 --- a/.gitea/workflows/windows-msix.yml +++ b/.gitea/workflows/windows-msix.yml @@ -16,7 +16,8 @@ # vX.Y.Z tag -> X.Y.Z.0 (THE release; any -rc/+meta pre-release suffix is dropped for MSIX). # Published to the generic registry + the stable `latest/` alias + attached to the # unified Gitea Release alongside every other platform's artifact. -# main push / dispatch -> 0.3..0 (canary; climbs monotonically by run number). +# main push / dispatch -> ..0 (canary; base is one minor ahead of the +# latest stable tag via scripts/ci/pf-version.ps1, run number climbs monotonically). # Published to the generic registry + the `canary/` alias. # Both arches share the version; artifacts are arch-suffixed (..._x64.msix / ..._arm64.msix). # @@ -78,11 +79,13 @@ jobs: "CARGO_TARGET_DIR=${{ matrix.td }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "FFMPEG_DIR=${{ matrix.ffmpeg }}" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 rustup target add ${{ matrix.target }} + $pf = & "$env:GITHUB_WORKSPACE/scripts/ci/pf-version.ps1" # single source of truth: base is one minor ahead of the latest stable tag $parts = if ($env:GITHUB_REF -like 'refs/tags/v*') { # MSIX needs a purely-numeric 4-part version: drop any -rc/+meta pre-release suffix. (($env:GITHUB_REF_NAME -replace '^v', '') -replace '[-+].*$', '').Split('.') } else { - @('0', '3', $env:GITHUB_RUN_NUMBER) + # Canary: ...0 — major.minor track one minor ahead of stable, run climbs monotonically. + @($pf.PF_MAJOR, $pf.PF_MINOR, $env:GITHUB_RUN_NUMBER) } while ($parts.Count -lt 4) { $parts += '0' } $v = ($parts[0..3] -join '.') diff --git a/docs-site/content/docs/channels.md b/docs-site/content/docs/channels.md index 34c5e3d..053303a 100644 --- a/docs-site/content/docs/channels.md +++ b/docs-site/content/docs/channels.md @@ -57,9 +57,25 @@ one-line edit of `/etc/apt/sources.list.d/punktfunk.list` (`stable` ↔ `canary` attaches its artifact to the `v0.2.0` Gitea Release. Concurrent attaches are safe — the shared `scripts/ci/gitea-release.{sh,ps1}` helper creates the release once and the rest reuse it. 5. **Promote the app stores manually** (CI only uploads to testing tracks — see below). -6. After a release reaches the current canary base, bump the canary base one minor ahead in - `deb.yml` / `rpm.yml` (and the `0.3.` strings in the other workflows) so a stable→canary - re-point still moves forward. Rule: **canary base = one minor ahead of the latest stable.** + +That's the whole ritual: **push a tag, done.** There is nothing else to hand-edit. + +### Versioning is derived — never hand-edited + +Every workflow gets its version number from one place, `scripts/ci/pf-version.{sh,ps1}` +(the pwsh twin is for the Windows runners), so the number can never drift out of sync: + +- **stable** (a `vX.Y.Z` tag) → the tag version (`-rc`/`+meta` dropped where a strictly-numeric + version is required — MSIX, the App Store marketing version). +- **canary** (a `main` push) → **exactly one minor ahead of the latest stable tag** (latest + `v0.6.0` → canary base `0.7.0`), with each channel's own build suffix (`-ciN`, `~ciN`, + `..`, …). Cutting `v0.7.0` automatically advances canary to `0.8.0` on the + next `main` push. + +This means canary is **always ahead of stable** with zero maintenance — the old footgun where a +canary showed up on TestFlight as `0.5.0` while `0.6.0` was already published is structurally +impossible now. If you ever need the next release to be something other than the next minor (a +major bump, or a patch), just tag it — the canary base re-derives from whatever the latest tag is. Pre-release tags work too: `v0.2.0-rc1` builds a real release (the `-rc1` suffix is dropped where a strictly-numeric version is required — MSIX, the App Store marketing version). diff --git a/scripts/ci/pf-version.ps1 b/scripts/ci/pf-version.ps1 new file mode 100644 index 0000000..648be1f --- /dev/null +++ b/scripts/ci/pf-version.ps1 @@ -0,0 +1,55 @@ +# Single source of truth for punktfunk release/canary version numbers (Windows runners). +# +# The pwsh twin of scripts/ci/pf-version.sh — see that file for the full rationale. Same rule: +# * stable (vX.Y.Z tag) -> PF_BASE = X.Y.Z (minus any -rc/+meta suffix) +# * canary (main push) -> PF_BASE = (always one ahead) +# +# Returns a hashtable AND (when $env:GITHUB_ENV is set) appends the same keys there for later +# steps. Usage in a pwsh workflow step: +# $pf = & "$env:GITHUB_WORKSPACE/scripts/ci/pf-version.ps1" +# $base = $pf.PF_BASE # e.g. "0.7.0" +# # numeric-version channels (MSIX/host installer) build "..": +# $v = "$($pf.PF_MAJOR).$($pf.PF_MINOR).$env:GITHUB_RUN_NUMBER" +$ErrorActionPreference = 'Stop' +# A non-zero native-command exit (e.g. a `git fetch` with no network) must NOT abort — the +# Cargo.toml fallback below covers it. On PS 7.4+ this pref would otherwise throw under -Stop. +$PSNativeCommandUseErrorActionPreference = $false + +$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path + +# checkout is shallow + tagless by default — canary needs the tag list. Best-effort. +try { git -C $root fetch --tags --force --quiet 2>$null } catch { } + +$stable = (git -C $root tag -l 'v*' 2>$null) | + ForEach-Object { if ($_ -match '^v(\d+\.\d+\.\d+)$') { $Matches[1] } } | + Sort-Object { [version]$_ } | + Select-Object -Last 1 +if (-not $stable) { + $stable = (Select-String -Path (Join-Path $root 'Cargo.toml') -Pattern '^version = "(\d+\.\d+\.\d+)"' | + Select-Object -First 1).Matches.Groups[1].Value +} +if (-not $stable) { $stable = '0.0.0' } + +if ($env:GITHUB_REF -like 'refs/tags/v*') { + $channel = 'stable' + $base = (($env:GITHUB_REF_NAME -replace '^v', '') -replace '[-+].*$', '') +} else { + $channel = 'canary' + $p = $stable.Split('.') + $base = "$($p[0]).$([int]$p[1] + 1).0" +} + +$b = $base.Split('.') +$out = [ordered]@{ + PF_CHANNEL = $channel + PF_BASE = $base + PF_MAJOR = $b[0] + PF_MINOR = $b[1] + PF_PATCH = $b[2] + PF_STABLE_TAG = $stable +} +foreach ($k in $out.Keys) { + Write-Output ("{0}={1}" -f $k, $out[$k]) | Out-Null + if ($env:GITHUB_ENV) { "$k=$($out[$k])" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 } +} +$out diff --git a/scripts/ci/pf-version.sh b/scripts/ci/pf-version.sh new file mode 100644 index 0000000..a1ff068 --- /dev/null +++ b/scripts/ci/pf-version.sh @@ -0,0 +1,79 @@ +# shellcheck shell=bash +# Single source of truth for punktfunk release/canary version numbers (Linux + macOS runners). +# +# WHY THIS EXISTS: every release workflow used to hardcode a canary base version +# (`0.5.0`, `0.3`, …). Those magic numbers had to be hand-bumped on every stable release +# and nobody did — so canary channels silently fell *behind* stable (a canary showed up on +# TestFlight as 0.5.0 while 0.6.0 was already published). This script computes the base +# version DETERMINISTICALLY from the git tags instead, so there is nothing to hand-edit and +# no future agent can get it wrong. +# +# THE RULE (chosen 2026-07-03): +# * stable (a `vX.Y.Z` tag push) → PF_BASE = X.Y.Z (the tag, minus any -rc/+meta suffix). +# * canary (a main push) → PF_BASE = . +# i.e. latest release v0.6.0 → canary base 0.7.0. Cutting v0.7.0 auto-advances canary to +# 0.8.0. Canary is therefore ALWAYS exactly one minor ahead of the newest stable release. +# +# USAGE (bash workflows) — eval it, then format your channel's suffix off $PF_BASE: +# eval "$(bash scripts/ci/pf-version.sh)" +# case "$GITHUB_REF" in +# refs/tags/v*) V="${GITHUB_REF_NAME#v}" ;; # stable +# *) V="${PF_BASE}-ci${GITHUB_RUN_NUMBER}" ;; # canary suffix is per-channel +# esac +# +# It prints `KEY=VALUE` lines (eval-able) to stdout and — when $GITHUB_ENV is set — also +# appends them there for later steps. Exports: +# PF_CHANNEL stable | canary +# PF_BASE the base semver X.Y.Z (see THE RULE) +# PF_MAJOR/MINOR/PATCH the components of PF_BASE (numeric-version channels build +# `..` from these — MSIX/decky need monotonic ints) +# PF_STABLE_TAG the latest stable release version the canary base was derived from (for logs) +# +# The pwsh twin scripts/ci/pf-version.ps1 implements the identical rule for the Windows runners. +set -euo pipefail + +_root="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../.." && pwd)" + +# actions/checkout is shallow and fetches NO tags by default — canary needs the full tag list +# to find the latest stable. Best-effort fetch; the Cargo.toml fallback below covers a fresh +# repo with no tags at all. +git -C "$_root" fetch --tags --force --quiet 2>/dev/null || true + +# Latest stable release = highest strict vX.Y.Z tag (pre-releases like v0.7.0-rc1 are ignored). +_stable="$( + git -C "$_root" tag -l 'v*' \ + | sed -n 's/^v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)$/\1/p' \ + | sort -V | tail -n1 +)" +if [ -z "${_stable:-}" ]; then + # No tags yet — seed from the workspace Cargo.toml version so canary still has a base. + _stable="$(sed -n 's/^version = "\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' "$_root/Cargo.toml" | head -n1)" +fi +_stable="${_stable:-0.0.0}" + +case "${GITHUB_REF:-}" in + refs/tags/v*) + _channel="stable" + _base="${GITHUB_REF_NAME#v}" + _base="${_base%%-*}" # drop -rc / pre-release for the numeric marketing/package version + _base="${_base%%+*}" # drop +build metadata + ;; + *) + _channel="canary" + _maj="${_stable%%.*}"; _rest="${_stable#*.}"; _min="${_rest%%.*}" + _base="${_maj}.$((_min + 1)).0" + ;; +esac + +_pf_major="${_base%%.*}"; _pf_rest="${_base#*.}"; _pf_minor="${_pf_rest%%.*}"; _pf_patch="${_pf_rest##*.}" + +_emit() { + printf '%s=%s\n' "$1" "$2" + if [ -n "${GITHUB_ENV:-}" ]; then printf '%s=%s\n' "$1" "$2" >> "$GITHUB_ENV"; fi +} +_emit PF_CHANNEL "$_channel" +_emit PF_BASE "$_base" +_emit PF_MAJOR "$_pf_major" +_emit PF_MINOR "$_pf_minor" +_emit PF_PATCH "$_pf_patch" +_emit PF_STABLE_TAG "$_stable"