From 1c6f4c9eb8a7c7edfdcf080d045ce7847260014d Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Tue, 23 Jun 2026 16:11:05 +0200 Subject: [PATCH] fix(release): build the tagged commit and stamp the real version (#525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.114.0 binaries reported version 0.0.0 in the web UI. Two root causes, both fixed here. 1. The release build relied solely on Go's VCS stamping of info.Main.Version and never injected a version. When v0.114.0 was re-released via workflow_dispatch from `main` (one commit past the tag) with a shallow checkout, no tag was reachable, so Go stamped a v0.0.0-- pseudo-version. The asset filenames used the validated input version, so the files were named v0.114.0 but reported 0.0.0 at runtime. 2. The `release` and `workflow_dispatch` triggers followed two distinct patterns. On `release` every job's checkout landed on the tagged commit (GITHUB_SHA == tag); on `workflow_dispatch` they all built whatever branch the run started from. So a manual dispatch built the wrong source entirely (binaries and Docker images alike). Changes: - Unify both triggers on the git tag. `validate` resolves the tag once (inputs.tag on dispatch, release.tag_name on a release event), verifies it exists in git, and exposes it as an output. Every other job checks out `ref: needs.validate.outputs.tag`, so the build is always the tagged commit regardless of trigger. The dispatch path now re-releases an existing tag (push the tag first) instead of creating one from a branch; it fails fast if the tag is missing. - Inject -X main.version/commit/date into the release binaries, mirroring the Dockerfile (which has done this since #422). version/commit no longer depend on git stamping; commit is read from the checked-out HEAD (not github.sha, which on dispatch is the branch HEAD). Both binaries and Docker images take the v-prefixed tag (needs.validate.outputs.tag) so the displayed version stays "v0.114.0", matching prior releases. - Guard updateBuildInfo() in all four cmd/*/main.go so an injected version (version != "dev") is never clobbered by a VCS pseudo-version. `go install …@vX.Y.Z` still resolves the tag via build info as before. - Collapse the duplicated `if event_name == workflow_dispatch` tag derivations and route tag/version through needs.validate.outputs.*. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 68 +++++++++++++++++++++++----------- cmd/soundtouch-backup/main.go | 6 ++- cmd/soundtouch-cli/main.go | 8 +++- cmd/soundtouch-player/main.go | 6 ++- cmd/soundtouch-service/main.go | 6 ++- 5 files changed, 68 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92b634e..9685dd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,7 @@ jobs: name: Validate Release runs-on: ubuntu-latest outputs: + tag: ${{ steps.version.outputs.tag }} version: ${{ steps.version.outputs.version }} is_prerelease: ${{ steps.version.outputs.is_prerelease }} @@ -30,16 +31,19 @@ jobs: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + # Both triggers resolve to the same thing: the release tag. On a + # `release` event inputs.tag is empty, so this falls back to the + # published release's tag. Every other job checks out this same + # tag (via needs.validate.outputs.tag) so the build is always the + # tagged commit, never whatever branch the dispatch ran on (#525). + ref: ${{ github.event.inputs.tag || github.event.release.tag_name }} fetch-depth: 0 - name: Validate tag format id: version run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - TAG_NAME="${{ github.event.inputs.tag }}" - else - TAG_NAME="${GITHUB_REF#refs/tags/}" - fi + # Single source of truth for the tag, regardless of trigger. + TAG_NAME="${{ github.event.inputs.tag || github.event.release.tag_name }}" echo "Tag name: $TAG_NAME" @@ -50,6 +54,15 @@ jobs: exit 1 fi + # Confirm the tag actually exists in git. The dispatch path + # re-releases an existing tag; it never creates one from a branch. + if ! git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then + echo "❌ Tag $TAG_NAME does not exist in git. Push the tag first, then re-run." + exit 1 + fi + + echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT + # Extract version without 'v' prefix VERSION=${TAG_NAME#v} echo "version=$VERSION" >> $GITHUB_OUTPUT @@ -103,6 +116,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ needs.validate.outputs.tag }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 @@ -125,6 +140,11 @@ jobs: CGO_ENABLED: 0 run: | # Common variables + # Single build timestamp shared across every binary in this job. + BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + # Commit of the checked-out tag (not GITHUB_SHA, which on a manual + # dispatch is the branch HEAD the run started from, not the tag). + COMMIT_SHA="$(git rev-parse HEAD)" ARCH_SUFFIX="${{ matrix.goos }}-${{ matrix.goarch }}" if [[ "${{ matrix.goarm }}" != "" ]]; then ARCH_SUFFIX="${ARCH_SUFFIX}v${{ matrix.goarm }}" @@ -150,9 +170,13 @@ jobs: # Ensure clean build environment for this binary rm -f "$OUTPUT_NAME" "$OUTPUT_NAME.sha256" "$OUTPUT_NAME.sha512" + # Inject the validated version (plus commit/date) so the binary + # reports the right version regardless of git checkout state. + # Relying on Go's VCS stamping alone yields v0.0.0-… when built + # from a shallow checkout or a non-tagged commit (see #525). if ! go build \ -trimpath \ - -ldflags="-s -w" \ + -ldflags="-s -w -X main.version=${{ needs.validate.outputs.tag }} -X main.commit=${COMMIT_SHA} -X main.date=${BUILD_DATE}" \ -o "$OUTPUT_NAME" \ "$CMD_PATH"; then echo "❌ Build failed for $BINARY_NAME" @@ -307,6 +331,7 @@ jobs: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + ref: ${{ needs.validate.outputs.tag }} fetch-depth: 0 - name: Download release assets @@ -318,11 +343,7 @@ jobs: - name: Generate release notes id: release_notes run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - TAG_NAME="${{ github.event.inputs.tag }}" - else - TAG_NAME="${{ github.event.release.tag_name }}" - fi + TAG_NAME="${{ needs.validate.outputs.tag }}" VERSION="${TAG_NAME#v}" # Short, accurate header. GitHub's auto-generated "What's Changed" @@ -367,8 +388,8 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: - tag_name: ${{ github.event.inputs.tag }} - name: ${{ github.event.inputs.tag }} + tag_name: ${{ needs.validate.outputs.tag }} + name: ${{ needs.validate.outputs.tag }} body_path: ${{ steps.release_notes.outputs.release_notes_file }} generate_release_notes: true draft: false @@ -400,7 +421,7 @@ jobs: - name: Upload additional assets to existing release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: - tag_name: ${{ github.event.release.tag_name }} + tag_name: ${{ needs.validate.outputs.tag }} files: | release-assets/soundtouch-cli-v* release-assets/soundtouch-service-v* @@ -420,10 +441,15 @@ jobs: steps: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ needs.validate.outputs.tag }} - - name: Set build date + - name: Set build metadata id: build_date - run: echo "date=$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT + run: | + echo "date=$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT + # Commit of the checked-out tag, not github.sha (the dispatch HEAD). + echo "commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 @@ -455,8 +481,8 @@ jobs: tags: ${{ steps.meta-service.outputs.tags }} labels: ${{ steps.meta-service.outputs.labels }} build-args: | - VERSION=v${{ needs.validate.outputs.version }} - COMMIT=${{ github.sha }} + VERSION=${{ needs.validate.outputs.tag }} + COMMIT=${{ steps.build_date.outputs.commit }} DATE=${{ steps.build_date.outputs.date }} cache-from: type=gha cache-to: type=gha,mode=max @@ -481,8 +507,8 @@ jobs: tags: ${{ steps.meta-player.outputs.tags }} labels: ${{ steps.meta-player.outputs.labels }} build-args: | - VERSION=v${{ needs.validate.outputs.version }} - COMMIT=${{ github.sha }} + VERSION=${{ needs.validate.outputs.tag }} + COMMIT=${{ steps.build_date.outputs.commit }} DATE=${{ steps.build_date.outputs.date }} cache-from: type=gha cache-to: type=gha,mode=max @@ -502,7 +528,7 @@ jobs: echo "🔐 Checksums generated and verified" echo "📋 Release notes automatically generated" echo "" - TAG_NAME="${{ github.event.inputs.tag || github.event.release.tag_name }}" + TAG_NAME="${{ needs.validate.outputs.tag }}" echo "🔗 Release URL: https://github.com/${{ github.repository }}/releases/tag/${TAG_NAME}" echo "" echo "Next steps:" diff --git a/cmd/soundtouch-backup/main.go b/cmd/soundtouch-backup/main.go index 6a89c63..6043b4c 100644 --- a/cmd/soundtouch-backup/main.go +++ b/cmd/soundtouch-backup/main.go @@ -14,7 +14,11 @@ var version = "dev" func init() { if info, ok := debug.ReadBuildInfo(); ok { - if info.Main.Version != "" && info.Main.Version != "(devel)" { + // Only fall back to build info when the version was not injected via + // -ldflags (i.e. still the "dev" default, e.g. `go install …@vX.Y.Z`). + // This keeps an explicitly stamped release version from being clobbered + // by a VCS pseudo-version (e.g. v0.0.0-… from a shallow checkout). + if version == "dev" && info.Main.Version != "" && info.Main.Version != "(devel)" { version = info.Main.Version } } diff --git a/cmd/soundtouch-cli/main.go b/cmd/soundtouch-cli/main.go index 2ebd074..2f4aa3c 100644 --- a/cmd/soundtouch-cli/main.go +++ b/cmd/soundtouch-cli/main.go @@ -73,8 +73,12 @@ func getFlagName(flag cli.Flag) string { // updateBuildInfo extracts version information from debug.BuildInfo and updates package variables func updateBuildInfo() { if info, ok := debug.ReadBuildInfo(); ok { - // Get version from module info - if info.Main.Version != "" && info.Main.Version != "(devel)" { + // Get version from module info. Only fall back to build info when the + // version was not injected via -ldflags (i.e. still the "dev" default, + // e.g. `go install …@vX.Y.Z`). This keeps an explicitly stamped release + // version from being clobbered by a VCS pseudo-version (e.g. v0.0.0-… + // from a shallow checkout). + if version == "dev" && info.Main.Version != "" && info.Main.Version != "(devel)" { version = info.Main.Version } diff --git a/cmd/soundtouch-player/main.go b/cmd/soundtouch-player/main.go index b557017..1f827bf 100644 --- a/cmd/soundtouch-player/main.go +++ b/cmd/soundtouch-player/main.go @@ -39,7 +39,11 @@ func updateBuildInfo() { repoURL = "https://" + info.Main.Path } - if info.Main.Version != "" && info.Main.Version != "(devel)" { + // Only fall back to build info when the version was not injected via + // -ldflags (i.e. still the "dev" default, e.g. `go install …@vX.Y.Z`). + // This keeps an explicitly stamped release version from being clobbered + // by a VCS pseudo-version (e.g. v0.0.0-… from a shallow checkout). + if version == "dev" && info.Main.Version != "" && info.Main.Version != "(devel)" { version = info.Main.Version } diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index 592e038..3d2ce5d 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -50,7 +50,11 @@ func updateBuildInfo() { repoURL = "https://" + info.Main.Path } - if info.Main.Version != "" && info.Main.Version != "(devel)" { + // Only fall back to build info when the version was not injected via + // -ldflags (i.e. still the "dev" default, e.g. `go install …@vX.Y.Z`). + // This keeps an explicitly stamped release version from being clobbered + // by a VCS pseudo-version (e.g. v0.0.0-… from a shallow checkout). + if version == "dev" && info.Main.Version != "" && info.Main.Version != "(devel)" { version = info.Main.Version }