chore(release): prepare v2.0 infrastructure (Makefile, release.sh, Gitea release workflow, installers, modern versioning)

This commit is contained in:
Greg Gauthier 2026-06-05 23:39:40 +01:00
parent 07586a8fc0
commit dd34a5b21c
13 changed files with 494 additions and 49 deletions

View File

@ -0,0 +1,96 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
name: Create Release
runs-on: ubuntu-gitea
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Build for multiple platforms
run: |
VERSION=${GITHUB_REF#refs/tags/}
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
mkdir -p build
for plat in 'linux/amd64' 'linux/arm64' 'darwin/amd64' 'darwin/arm64' 'windows/amd64'; do
IFS='/' read -r OS ARCH <<< "$plat"
BIN="gostations-${OS}-${ARCH}"
if [ "$OS" = "windows" ]; then BIN="${BIN}.exe"; fi
GOOS="$OS" GOARCH="$ARCH" go build -trimpath -ldflags "-s -w \
-X 'github.com/gmgauthier/gostations/internal/version.Version=${VERSION}' \
-X 'github.com/gmgauthier/gostations/internal/version.Commit=${COMMIT}' \
-X 'github.com/gmgauthier/gostations/internal/version.BuildDate=${DATE}' \
-X 'main.version=${VERSION}'" -o "build/${BIN}" .
done
- name: Prepare assets
run: |
VERSION=${GITHUB_REF#refs/tags/}
for bin in build/gostations-* ; do
if [ ! -f "$bin" ]; then continue; fi
OSARCH=$(basename "$bin" | sed 's/gostations-//' | sed 's/\.exe$//')
tar czf "build/gostations-${OSARCH}-${VERSION}.tar.gz" -C build "$(basename "$bin")"
done
sha256sum build/gostations-*.tar.gz | tee build/checksums.txt
# Include useful scripts from repo (if present)
[ -f scripts/gostations-install.sh ] && cp scripts/gostations-install.sh build/
[ -f scripts/gostations-install.ps1 ] && cp scripts/gostations-install.ps1 build/
# Clean raw binaries (we ship the tarballs)
for plat in 'linux/amd64' 'linux/arm64' 'darwin/amd64' 'darwin/arm64' 'windows/amd64'; do
IFS='/' read -r OS ARCH <<< "$plat"
BIN="gostations-${OS}-${ARCH}"
if [ "$OS" = "windows" ]; then BIN="${BIN}.exe"; fi
rm -f "build/${BIN}"
done
- name: Install dependencies
run: apt update && apt install -y jq
- name: Create Release & Upload Assets
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
VERSION=${GITHUB_REF#refs/tags/}
GITEA_API=https://repos.gmgauthier.com/api/v1
REPO=${GITHUB_REPOSITORY}
curl -X POST "${GITEA_API}/repos/${REPO}/releases" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"${VERSION}\",
\"name\": \"gostations ${VERSION}\",
\"body\": \"## Quick Install\n\n### Bash (Linux/macOS)\n\n\`\`\`bash\ncurl -L https://repos.gmgauthier.com/gmgauthier/gostations/releases/download/${VERSION}/gostations-install.sh | VERSION=${VERSION} bash\n\`\`\`\n\n### PowerShell (Windows/macOS/Linux)\n\n\`\`\`powershell\nirm https://repos.gmgauthier.com/gmgauthier/gostations/releases/download/${VERSION}/gostations-install.ps1 | iex\n\`\`\`\n\nPlatform binaries (tar.gz) + checksums.txt are attached. See README.md and CHANGELOG (if present) for details. Legacy wmenu UI still available with --legacy.\"
}" > release.json
RELEASE_ID=$(jq .id release.json)
for asset in build/* ; do
name=$(basename "$asset")
mime="application/octet-stream"
[[ "$name" =~ \.tar\.gz$ ]] && mime="application/gzip"
[[ "$name" =~ \.(txt|sh|ps1)$ ]] && mime="text/plain"
curl -X POST "${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${name}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: ${mime}" \
--data-binary "@$asset"
done

38
CHANGELOG.md Normal file
View File

@ -0,0 +1,38 @@
# Changelog
All notable changes to gostations will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.0.0] - 2026-06-XX
### Added
- Full modern TUI (Bubble Tea + bubbles/list + lipgloss) as the **default** experience.
- Two-stage UI: station selection list → dedicated playback view.
- Playback view inspired by classic Winamp:
- Metadata viewer area showing live streamed song titles (via mpv IPC).
- Control buttons / keys: skip back/forward, volume (UP/DOWN arrows + on-screen vertical bar), mute toggle, play/pause, stop (returns to list).
- mpv JSON IPC player implementation (background playback, no terminal takeover, responsive controls and metadata observation).
- Favorites (★) support:
- TUI hotkey `f` to toggle.
- CLI: `gostations fav list|add|del [index|search|url]`.
- Initial view shows your Favorites if any (with ★ markers).
- Server-side search: while the filter is active, pressing ENTER performs a fresh lookup and replaces the list.
- `--legacy` flag to force the old wmenu UI (preserved for now).
- All previous CLI subcommands (`find`, `play`, `fav ...`) continue to work for scripting.
### Changed
- Default UI is now the new TUI (no more wmenu unless --legacy).
- Player abstraction extended for controls and metadata.
- Build/release process modernized (Makefile, cross-compilation, Gitea release workflow + installers) to match other projects.
### Fixed
- Various legacy subExecute / player execution issues from the old architecture.
- Test coverage for new TUI playback and player features.
See the git history for the full list of changes leading to 2.0.
## [0.2] - Previous
Legacy wmenu-based UI + initial internal refactoring.

88
Makefile Normal file
View File

@ -0,0 +1,88 @@
.PHONY: test test-short test-cover lint build install clean help cross release-notes
# Versioning (override on command line or via env for releases)
VERSION ?= dev-$(shell git describe --tags --always --dirty 2>/dev/null || echo unknown)
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)
MODULE = github.com/gmgauthier/gostations
LDFLAGS = -s -w \
-X '$(MODULE)/internal/version.Version=$(VERSION)' \
-X '$(MODULE)/internal/version.Commit=$(COMMIT)' \
-X '$(MODULE)/internal/version.BuildDate=$(DATE)' \
-X 'main.version=$(VERSION)'
test:
go test ./... -v -race
test-short:
go test -short ./... -v -race
test-cover:
@mkdir -p build
go test ./... -coverprofile=build/coverage.out
go tool cover -html=build/coverage.out -o build/coverage.html
@echo "✅ Coverage report: open build/coverage.html in your browser"
lint:
@which golangci-lint > /dev/null || (echo "❌ golangci-lint not found. Install with:" && echo " go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest" && exit 1)
golangci-lint run
build:
@mkdir -p build
go build -trimpath -ldflags "$(LDFLAGS)" -o build/gostations .
@echo "✅ Dev build: VERSION=$(VERSION) COMMIT=$(COMMIT) DATE=$(DATE)"
@build/gostations -v || true
install: build
mkdir -p ~/.local/bin
cp build/gostations ~/.local/bin/gostations
chmod +x ~/.local/bin/gostations
@echo "✅ gostations installed to ~/.local/bin/gostations"
clean:
rm -rf build/
# Cross compile (used by release workflow)
cross:
@mkdir -p build
@for plat in 'linux/amd64' 'linux/arm64' 'darwin/amd64' 'darwin/arm64' 'windows/amd64'; do \
IFS='/' read -r OS ARCH <<< "$$plat"; \
BIN="gostations-$$OS-$$ARCH"; \
if [ "$$OS" = "windows" ]; then BIN="$$BIN.exe"; fi; \
echo "Building $$BIN..."; \
GOOS=$$OS GOARCH=$$ARCH go build -trimpath -ldflags "$(LDFLAGS)" -o "build/$$BIN" . ; \
done
@echo "✅ Cross builds complete in build/"
# Helper to print release notes body (used by release process)
release-notes:
@echo "## Installation"
@echo ""
@echo "Download the appropriate archive for your platform from the release assets."
@echo ""
@echo "### Quick install (Linux/macOS)"
@echo ""
@echo '```bash'
@echo 'curl -L https://repos.gmgauthier.com/gmgauthier/gostations/releases/download/$(VERSION)/gostations-install.sh | VERSION=$(VERSION) bash'
@echo '```'
@echo ""
@echo "### Quick install (Windows / PowerShell)"
@echo ""
@echo '```powershell'
@echo 'irm https://repos.gmgauthier.com/gmgauthier/gostations/releases/download/$(VERSION)/gostations-install.ps1 | iex'
@echo '```'
@echo ""
@echo "See README.md for full details and configuration (radiostations.ini)."
help:
@echo "Available targets:"
@echo " test Run all tests (with race)"
@echo " test-short Run tests with -short (skips live integration)"
@echo " test-cover Tests + HTML coverage report"
@echo " lint Run golangci-lint"
@echo " build Optimized dev build (uses git describe for VERSION)"
@echo " install Build + install to ~/.local/bin/gostations"
@echo " cross Build all release platforms into build/"
@echo " release-notes Print suggested Gitea release body text"
@echo " clean Remove build/"

View File

@ -1 +1 @@
0.2 2.0.0

View File

@ -1,17 +1,7 @@
# NOTE: The following commands assumes you have Git For Windows # Modernized. Prefer `make build` / `make install` (via Git Bash or WSL recommended).
# installed, which comes with a bunch of GNU tools packaged for windows: # This remains for basic compatibility.
Set-Variable -Name GIT_COMMIT -Value "$(git rev-list -1 HEAD)"
Set-Variable -Name CANONICAL_VERSION -Value "$(cat.exe ./VERSION)-$(uname)"
Set-Variable -Name VERSION_STRING -Value "$CANONICAL_VERSION-$GIT_COMMIT"
Set-Variable -Name buildpath -Value "build/$(uname)/gostations.exe" make build
make install
go mod tidy
go build -o "$buildpath" -ldflags "-X main.version=$VERSION_STRING"
& $buildpath -v
Copy-Item $buildpath $HOME/.local/bin -Force

View File

@ -1,18 +1,8 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Modernized build script. Prefer `make build` or `make install`.
# This script remains for compatibility.
GIT_COMMIT=$(git rev-list -1 HEAD) set -e
export GIT_COMMIT
CANONICAL_VERSION=$(cat ./VERSION)-$(uname)
export CANONICAL_VERSION
VERSION_STRING="$CANONICAL_VERSION-$GIT_COMMIT"
export VERSION_STRING
buildpath="build/$(uname)/gostations" make build
make install
go mod tidy
go build -o "$buildpath" -ldflags "-X main.version=$VERSION_STRING"
"$buildpath" -v
cp "$buildpath" ~/.local/bin

View File

@ -1,19 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# CI build (old runner compatibility). Uses Makefile for consistency.
mkdir -p build set -e
GIT_COMMIT=$(git rev-list -1 HEAD) make build
export GIT_COMMIT
CANONICAL_VERSION=$(cat ./VERSION)-$(uname)
export CANONICAL_VERSION
VERSION_STRING="$CANONICAL_VERSION-$GIT_COMMIT"
export VERSION_STRING
buildpath="build/$(uname)/gostations"
/usr/local/go/bin/go mod tidy
/usr/local/go/bin/go build -o "$buildpath" -ldflags "-X main.version=$VERSION_STRING"
"$buildpath" -v

View File

@ -1,7 +1,7 @@
package version package version
// These vars are set at build time via -ldflags, e.g. // These vars are set at build time via -ldflags (see Makefile and .gitea/workflows/release.yml), e.g.
// -ldflags "-X github.com/gmgauthier/gostations/internal/version.Version=0.3 -X .../Commit=$(git rev-list -1 HEAD)" // -ldflags "-X github.com/gmgauthier/gostations/internal/version.Version=2.0.0 -X .../Commit=... -X .../BuildDate=..."
var ( var (
Version = "dev" Version = "dev"
Commit = "" Commit = ""

95
release.sh Executable file
View File

@ -0,0 +1,95 @@
#!/bin/bash
# release.sh — One-command release driver for gostations (modeled on grokkit)
# Usage: ./release.sh v2.0.0
#
# This will:
# - Create the git tag (so downstream CI/release can see it)
# - Help generate/update CHANGELOG (via grokkit if available)
# - Commit the changes
# - Push tag + commit
# - Print suggested Gitea release notes body
set -euo pipefail
VERSION="${1:-}"
if [[ -z "$VERSION" || ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)? ]]; then
echo "❌ Usage: $0 vX.Y.Z"
echo " Example: $0 v2.0.0"
exit 1
fi
echo "🚀 Starting release process for gostations $VERSION..."
# Safety check: clean working tree
if [[ -n $(git status --porcelain) ]]; then
echo "❌ Working tree is dirty. Commit or stash changes first."
exit 1
fi
# Final human confirmation
echo ""
echo "This will:"
echo " 1. Create git tag $VERSION"
echo " 2. (If grokkit available) Run grokkit changelog + commit for CHANGELOG.md"
echo " 3. Push the commit + tag"
echo " 4. Print ready-to-paste text for the Gitea release page"
echo ""
read -p "Proceed with release $VERSION? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
# 1. Create the tag early (CI release workflow triggers on tag)
echo "🏷️ Creating tag $VERSION..."
git tag "$VERSION"
# 2. Changelog / release prep (best effort using grokkit if present on PATH)
if command -v grokkit >/dev/null 2>&1; then
echo "📝 Generating/updating CHANGELOG.md via grokkit..."
grokkit changelog --version "$VERSION" || echo "⚠️ grokkit changelog returned non-zero (continuing)"
if [[ -n $(git status --porcelain -- CHANGELOG.md) ]]; then
echo "📦 Committing changelog changes via grokkit..."
git add CHANGELOG.md
grokkit commit || echo "⚠️ grokkit commit may need manual follow-up"
fi
else
echo " grokkit not found on PATH. Skipping automated changelog."
echo " You can manually edit CHANGELOG.md before the next steps if desired."
echo " Then run: git add CHANGELOG.md && git commit -m 'chore(release): $VERSION'"
fi
# 3. Push (tag was created earlier; push any new commit + tags)
echo "📤 Pushing commit (if any) + tag..."
git push || true
git push --tags
# 4. Print nice release notes for Gitea
echo ""
echo "✅ Release $VERSION pushed!"
echo ""
echo "📋 Copy-paste the following into the Gitea release notes body:"
echo "------------------------------------------------------------"
make -s release-notes VERSION="$VERSION" || cat <<EOF
## gostations ${VERSION}
New major release: full modern TUI (Bubble Tea) is now the default.
- Two-stage UI: station browser → dedicated playback view (Winamp-inspired)
- Live stream metadata display
- Playback controls: skip, volume (↑/↓ + vertical bar), mute, play/pause, stop (returns to list)
- mpv JSON IPC for responsive controls + metadata (no terminal takeover)
- Favorites () management in both TUI and CLI (fav list/add/del [index])
- Server-side search on ENTER while filtering
- Legacy wmenu UI still available via --legacy (for now)
- All previous CLI subcommands (find, play, fav) preserved
See the commit history and updated README for details.
EOF
echo "------------------------------------------------------------"
echo ""
echo "🎉 Now go to Gitea and create the release using the tag $VERSION."
echo " The workflow will automatically build cross-platform assets and attach them."

View File

@ -0,0 +1,49 @@
param(
[string]$Version = $env:VERSION
)
if (-not $Version) {
Write-Error "Provide -Version or set VERSION env var, e.g. VERSION=2.0.0"
exit 1
}
$Version = $Version.TrimStart('v')
$GITEA_BASE = "https://repos.gmgauthier.com/gmgauthier/gostations"
$OS = if ($IsWindows) { "windows" } elseif ($IsMacOS) { "darwin" } else { "linux" }
$ARCH = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq "X64") { "amd64" } else { "arm64" }
$ASSET = "gostations-$OS-$ARCH-v$Version.tar.gz"
Write-Host "Installing gostations $Version for $OS/$ARCH..."
$tempDir = New-TemporaryFile | % { rm $_; mkdir $_ }
try {
Push-Location $tempDir
Write-Host "Downloading $ASSET..."
Invoke-WebRequest -Uri "$GITEA_BASE/releases/download/v$Version/$ASSET" -OutFile "asset.tar.gz"
Write-Host "Downloading checksums.txt..."
Invoke-WebRequest -Uri "$GITEA_BASE/releases/download/v$Version/checksums.txt" -OutFile "checksums.txt"
# Extract (tar on Windows via tar if available, or 7z, but assume tar in modern PS)
Write-Host "Extracting..."
tar -xzf asset.tar.gz
$binary = "gostations-$OS-$ARCH"
if ($OS -eq "windows") { $binary += ".exe" }
$installDir = "$HOME\.local\bin"
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
Move-Item -Force $binary "$installDir\gostations.exe" -ErrorAction SilentlyContinue
Move-Item -Force $binary "$installDir\gostations" -ErrorAction SilentlyContinue
Write-Host "✅ gostations $Version installed to $installDir\gostations"
& "$installDir\gostations" -v
} finally {
Pop-Location
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
}

78
scripts/gostations-install.sh Executable file
View File

@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION=${VERSION:-${1:?Provide VERSION env or arg, e.g. VERSION=2.0.0 bash gostations-install.sh}}
# Strip leading 'v' if present
VERSION=${VERSION#v}
GITEA_BASE=https://repos.gmgauthier.com/gmgauthier/gostations
# Platform detection
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$OS" in
linux) OS=linux ;;
darwin) OS=darwin ;;
esac
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64) ARCH=amd64 ;;
arm64|aarch64) ARCH=arm64 ;;
esac
ASSET="gostations-${OS}-${ARCH}-v${VERSION}.tar.gz"
echo "Installing gostations ${VERSION} for ${OS}/${ARCH}..."
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "${TEMP_DIR}"' EXIT
cd "${TEMP_DIR}"
# Download asset + checksums
echo "Downloading ${ASSET}..."
curl -fL "${GITEA_BASE}/releases/download/v${VERSION}/${ASSET}" -o asset.tar.gz
echo "Downloading checksums.txt..."
curl -fL "${GITEA_BASE}/releases/download/v${VERSION}/checksums.txt" -o checksums.txt
# Robust checksum verification
echo "Verifying checksum..."
CHECKSUM_CMD=""
if command -v sha256sum >/dev/null 2>&1; then
CHECKSUM_CMD="sha256sum"
elif command -v shasum >/dev/null 2>&1; then
CHECKSUM_CMD="shasum -a 256"
fi
if [ -n "$CHECKSUM_CMD" ] && [ -f checksums.txt ]; then
HASH=$(grep -oE '[0-9a-f]{64}\s+build/[^ ]*' checksums.txt | grep "${ASSET}" | cut -d' ' -f1 || true)
if [ -z "$HASH" ]; then
echo "⚠️ No checksum entry found for ${ASSET} continuing without verification"
else
echo "${HASH} asset.tar.gz" | $CHECKSUM_CMD --check - || {
echo "❌ Checksum mismatch for ${ASSET}!"
exit 1
}
echo "✅ Checksum verified successfully"
fi
else
echo "⚠️ Checksum tool not found (sha256sum/shasum) skipping verification"
fi
# Extract
echo "Extracting asset..."
tar xzf asset.tar.gz
BINARY="gostations-${OS}-${ARCH}"
if [ "$OS" = "windows" ]; then BINARY="${BINARY}.exe"; fi
# Install
INSTALL_DIR="${HOME}/.local/bin"
mkdir -p "${INSTALL_DIR}"
mv "${BINARY}" "${INSTALL_DIR}/gostations" 2>/dev/null || mv "${BINARY}" "${INSTALL_DIR}/gostations.exe" 2>/dev/null || true
chmod +x "${INSTALL_DIR}/gostations" 2>/dev/null || true
echo "✅ gostations ${VERSION} installed to ${INSTALL_DIR}/gostations"
echo "Add to PATH if needed: export PATH=\"${INSTALL_DIR}:\$PATH\""
gostations -v || true

View File

@ -14,11 +14,18 @@ import (
playerpkg "github.com/gmgauthier/gostations/internal/player" playerpkg "github.com/gmgauthier/gostations/internal/player"
"github.com/gmgauthier/gostations/internal/radio" "github.com/gmgauthier/gostations/internal/radio"
"github.com/gmgauthier/gostations/internal/ui" "github.com/gmgauthier/gostations/internal/ui"
ver "github.com/gmgauthier/gostations/internal/version"
) )
var version string var version string // kept for ldflags compat with legacy build scripts; prefer internal/version
func showVersion() { func showVersion() {
// Prefer modern internal/version (ldflags in Makefile + release workflow)
if ver.Version != "dev" || ver.Commit != "" {
fmt.Println(ver.String())
return
}
// Fallback for legacy build scripts that only -X main.version=...
fmt.Println(version) fmt.Println(version)
} }

View File

@ -9,6 +9,7 @@ import (
"testing" "testing"
"github.com/gmgauthier/gostations/internal/radio" "github.com/gmgauthier/gostations/internal/radio"
ver "github.com/gmgauthier/gostations/internal/version"
) )
func TestShowVersion_Unit(t *testing.T) { func TestShowVersion_Unit(t *testing.T) {
@ -27,6 +28,7 @@ func TestShowVersion_Unit(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Test legacy main var path
originalVersion := version originalVersion := version
version = tt.version version = tt.version
defer func() { version = originalVersion }() defer func() { version = originalVersion }()
@ -46,7 +48,31 @@ func TestShowVersion_Unit(t *testing.T) {
got := buf.String() got := buf.String()
if got != tt.expected { if got != tt.expected {
t.Errorf("showVersion() = %q, want %q", got, tt.expected) t.Errorf("showVersion() legacy = %q, want %q", got, tt.expected)
}
// Test new internal/version path (preferred for 2.0+ builds)
origV := ver.Version
origC := ver.Commit
ver.Version = tt.version
ver.Commit = ""
defer func() { ver.Version = origV; ver.Commit = origC }()
origStdout = os.Stdout
r, w, _ = os.Pipe()
os.Stdout = w
showVersion()
w.Close()
os.Stdout = origStdout
buf.Reset()
_, _ = buf.ReadFrom(r)
got = buf.String()
if got != tt.expected {
t.Errorf("showVersion() package = %q, want %q", got, tt.expected)
} }
}) })
} }