name: Release on: release: types: [published] workflow_dispatch: inputs: tag: description: "Tag to release (e.g., v1.0.0)" required: true default: "v1.0.0" permissions: contents: write actions: read packages: write env: GO_VERSION_FILE: "go.mod" jobs: validate: 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 }} steps: - 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: | # 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" # Validate semantic versioning format if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then echo "❌ Invalid tag format: $TAG_NAME" echo "Expected format: v1.2.3 or v1.2.3-beta.1" 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 # Check if prerelease (contains hyphen) if [[ "$TAG_NAME" =~ - ]]; then echo "is_prerelease=true" >> $GITHUB_OUTPUT echo "📦 Prerelease detected: $TAG_NAME" else echo "is_prerelease=false" >> $GITHUB_OUTPUT echo "🚀 Stable release detected: $TAG_NAME" fi - name: Set up Go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version-file: ${{ env.GO_VERSION_FILE }} - name: Install libpcap run: sudo apt-get install -y libpcap-dev - name: Run tests before release run: | echo "Running final tests before release..." go test -v ./... echo "✅ All tests passed" build: name: Build Release Binaries runs-on: ubuntu-latest needs: validate strategy: matrix: include: - goos: linux goarch: amd64 - goos: linux goarch: arm64 - goos: linux goarch: arm goarm: 7 - goos: darwin goarch: amd64 - goos: darwin goarch: arm64 - goos: windows goarch: amd64 - goos: freebsd goarch: amd64 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@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version-file: ${{ env.GO_VERSION_FILE }} - name: Cache Go modules uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.mod') }}-${{ hashFiles('**/go.sum') }} - name: Build binaries env: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} GOARM: ${{ matrix.goarm }} 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 }}" fi # Function to build a binary build_binary() { local BINARY_NAME=$1 local CMD_PATH=$2 local OUTPUT_NAME # Ensure build directory exists mkdir -p build if [[ "${{ matrix.goos }}" == "windows" ]]; then OUTPUT_NAME="build/${BINARY_NAME}-v${{ needs.validate.outputs.version }}-${ARCH_SUFFIX}.exe" else OUTPUT_NAME="build/${BINARY_NAME}-v${{ needs.validate.outputs.version }}-${ARCH_SUFFIX}" fi echo "Building $BINARY_NAME: $OUTPUT_NAME" # 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 -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" exit 1 fi # Verify binary was created ls -la "$OUTPUT_NAME" echo "$BINARY_NAME=$OUTPUT_NAME" >> $GITHUB_OUTPUT } # Build CLI build_binary "soundtouch-cli" "./cmd/soundtouch-cli" # Build Service build_binary "soundtouch-service" "./cmd/soundtouch-service" # Build Player (formerly soundtouch-web) build_binary "soundtouch-player" "./cmd/soundtouch-player" # Build Backup build_binary "soundtouch-backup" "./cmd/soundtouch-backup" id: build - name: Generate individual checksums run: | CLI_NAME="${{ steps.build.outputs.soundtouch-cli }}" SVC_NAME="${{ steps.build.outputs.soundtouch-service }}" PLAYER_NAME="${{ steps.build.outputs.soundtouch-player }}" BCK_NAME="${{ steps.build.outputs.soundtouch-backup }}" # Use atomic operations to avoid conflicts TEMP_DIR=$(mktemp -d) generate_checksums() { local FILE=$1 echo "Building checksums for: $FILE" sha256sum "$FILE" > "${TEMP_DIR}/$(basename "$FILE").sha256" sha512sum "$FILE" > "${TEMP_DIR}/$(basename "$FILE").sha512" mv "${TEMP_DIR}/$(basename "$FILE").sha256" "$FILE.sha256" mv "${TEMP_DIR}/$(basename "$FILE").sha512" "$FILE.sha512" } generate_checksums "$CLI_NAME" generate_checksums "$SVC_NAME" generate_checksums "$PLAYER_NAME" generate_checksums "$BCK_NAME" # Cleanup rm -rf "$TEMP_DIR" echo "✅ Checksums generated successfully" - name: Upload build artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.goarm }} path: | build/soundtouch-cli-v* build/soundtouch-service-v* build/soundtouch-player-v* build/soundtouch-backup-v* retention-days: 1 checksums: name: Generate Checksums runs-on: ubuntu-latest needs: [validate, build] steps: - name: Download binary artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: binaries-* path: ./binaries - name: Generate checksums run: | cd binaries # Debug: Show the downloaded structure echo "📁 Downloaded artifact structure:" ls -R # Create a collection directory to avoid naming conflicts mkdir -p release-files # Move all files from subdirectories to the collection directory find . -mindepth 2 -type f \( -name "soundtouch-cli-*" -o -name "soundtouch-service-*" -o -name "soundtouch-player-*" -o -name "soundtouch-backup-*" \) -exec mv {} release-files/ \; # Remove empty directories find . -type d -empty -delete # Move to the collection directory for the rest of the processing cd release-files # Debug: Show flattened structure echo "📁 Flattened structure:" ls -la soundtouch-* || echo "No files found matching pattern" # Generate combined checksums (exclude individual .sha256/.sha512 files) if ls soundtouch-* 1> /dev/null 2>&1; then # Only checksum the actual binaries, not the .sha256/.sha512 files ls soundtouch-cli-* soundtouch-service-* soundtouch-player-* soundtouch-backup-* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha256sum > checksums.sha256 ls soundtouch-cli-* soundtouch-service-* soundtouch-player-* soundtouch-backup-* | grep -v '\.sha256$' | grep -v '\.sha512$' | xargs sha512sum > checksums.sha512 echo "📋 Generated combined checksums:" cat checksums.sha256 # Verify all expected files are present (binaries only, not checksum files) EXPECTED_COUNT=28 # 7 platforms * 4 binaries ACTUAL_COUNT=$(ls soundtouch-* | grep -v '\.sha256$' | grep -v '\.sha512$' | wc -l) if [[ $ACTUAL_COUNT -ne $EXPECTED_COUNT ]]; then echo "❌ Expected $EXPECTED_COUNT binaries, found $ACTUAL_COUNT" ls -la exit 1 fi echo "✅ All $ACTUAL_COUNT binaries present" else echo "❌ No binary files found" echo "Directory contents:" ls -la exit 1 fi - name: Upload checksums uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: checksums path: | binaries/release-files/checksums.sha256 binaries/release-files/checksums.sha512 binaries/release-files/*.sha256 binaries/release-files/*.sha512 retention-days: 1 - name: Upload all release assets uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: release-assets path: binaries/release-files/ retention-days: 1 create_release: name: Create GitHub Release runs-on: ubuntu-latest needs: [validate, checksums] if: github.event_name == 'workflow_dispatch' steps: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ needs.validate.outputs.tag }} fetch-depth: 0 - name: Download release assets uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-assets path: ./release-assets - name: Generate release notes id: release_notes run: | TAG_NAME="${{ needs.validate.outputs.tag }}" VERSION="${TAG_NAME#v}" # Short, accurate header. GitHub's auto-generated "What's Changed" # + "Full Changelog" are appended after this (generate_release_notes). cat > release_notes.md << EOF # AfterTouch $TAG_NAME **Bose SoundTouch Toolkit.** Keep your Bose SoundTouch speakers alive after the Bose cloud shutdown. No Bose infrastructure required. ## What's included Pre-built binaries for Linux (amd64, arm64, armv7), macOS (Intel & Apple Silicon), Windows (amd64), and FreeBSD (amd64): - **soundtouch-service**: local server that replaces the Bose cloud. Point your speaker at it and you keep full control; the built-in web UI on port 8000 handles setup. - **soundtouch-player**: standalone LAN web UI for device control: play/pause, volume, presets, live status. (Formerly \`soundtouch-web\`.) - **soundtouch-cli**: command-line control of any device: playback, presets, sources, multiroom zones, discovery, and migration. Good for scripting and home automation. - **soundtouch-backup**: back up your Bose cloud account and each speaker's local state. \`soundtouch-backup all\` captures everything in one step. Not sure which file to grab? The [Downloads page](https://gesellix.github.io/Bose-SoundTouch/docs/downloads/) explains which tool you need and which \`-\` build matches your computer. ## Documentation Full guides, setup walkthroughs, and troubleshooting: https://gesellix.github.io/Bose-SoundTouch/ ## Use as a Go library The core client is also importable: \`\`\`bash go get github.com/gesellix/bose-soundtouch@$TAG_NAME \`\`\` ## Verifying downloads Each binary has its own \`.sha256\`/\`.sha512\`, and combined \`checksums.sha256\` / \`checksums.sha512\` cover all of them: \`\`\`bash sha256sum -c checksums.sha256 --ignore-missing \`\`\` EOF echo "release_notes_file=release_notes.md" >> $GITHUB_OUTPUT - name: Create GitHub Release uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: 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 prerelease: ${{ needs.validate.outputs.is_prerelease == 'true' }} files: | release-assets/soundtouch-cli-v* release-assets/soundtouch-service-v* release-assets/soundtouch-player-v* release-assets/soundtouch-backup-v* release-assets/checksums.sha256 release-assets/checksums.sha512 fail_on_unmatched_files: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} update_release: name: Update Existing Release runs-on: ubuntu-latest needs: [validate, checksums] if: github.event_name == 'release' && github.event.action == 'published' steps: - name: Download release assets uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: release-assets path: ./release-assets - name: Upload additional assets to existing release uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: tag_name: ${{ needs.validate.outputs.tag }} files: | release-assets/soundtouch-cli-v* release-assets/soundtouch-service-v* release-assets/soundtouch-player-v* release-assets/soundtouch-backup-v* release-assets/checksums.sha256 release-assets/checksums.sha512 fail_on_unmatched_files: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} docker: name: Build and Push Docker Image runs-on: ubuntu-latest needs: validate steps: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ needs.validate.outputs.tag }} - name: Set build metadata id: build_date 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 - name: Log in to GitHub Container Registry uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for soundtouch-service id: meta-service uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ghcr.io/${{ github.repository }} tags: | type=semver,pattern={{version}},value=v${{ needs.validate.outputs.version }} type=semver,pattern={{major}}.{{minor}},value=v${{ needs.validate.outputs.version }} type=raw,value=latest,enable=${{ needs.validate.outputs.is_prerelease == 'false' }} - name: Build and push soundtouch-service Docker image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . target: soundtouch-service platforms: linux/amd64,linux/arm64,linux/arm64/v8,linux/arm/v7 push: true tags: ${{ steps.meta-service.outputs.tags }} labels: ${{ steps.meta-service.outputs.labels }} build-args: | 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 - name: Extract metadata (tags, labels) for soundtouch-player id: meta-player uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ghcr.io/${{ github.repository }}-player tags: | type=semver,pattern={{version}},value=v${{ needs.validate.outputs.version }} type=semver,pattern={{major}}.{{minor}},value=v${{ needs.validate.outputs.version }} type=raw,value=latest,enable=${{ needs.validate.outputs.is_prerelease == 'false' }} - name: Build and push soundtouch-player Docker image uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . target: soundtouch-player platforms: linux/amd64,linux/arm64,linux/arm64/v8,linux/arm/v7 push: true tags: ${{ steps.meta-player.outputs.tags }} labels: ${{ steps.meta-player.outputs.labels }} build-args: | 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 notify: name: Post-Release Notifications runs-on: ubuntu-latest needs: [validate, create_release, update_release, docker] if: always() && (needs.create_release.result == 'success' || needs.update_release.result == 'success' || needs.docker.result == 'success') steps: - name: Notify success run: | echo "🎉 Release ${{ needs.validate.outputs.version }} completed successfully!" echo "📦 Binaries built for 7 platforms (CLI, Service, Player, and Backup)" echo "🐳 Docker image published to ghcr.io" echo "🔐 Checksums generated and verified" echo "📋 Release notes automatically generated" echo "" TAG_NAME="${{ needs.validate.outputs.tag }}" echo "🔗 Release URL: https://github.com/${{ github.repository }}/releases/tag/${TAG_NAME}" echo "" echo "Next steps:" echo "- Monitor download metrics" echo "- Update documentation if needed" echo "- Announce to community (see scripts/post-release.md)"