name: Continuous Deployment permissions: contents: read on: push: tags: - "v*.*.*" workflow_dispatch: env: CARGO_TERM_COLOR: always BINARY_NAME: spotatui jobs: build: name: Build ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: # Multi-source features (local-files, subsonic, internet-radio, youtube, qobuz) # ship on all three platforms: they share the rodio output engine, which # is now opened on macOS too (the #9/#20 crashes were librespot's own # older rodio backend, not this one — librespot still uses portaudio # there, which is why `portaudio-backend` stays on the macOS rows). # The youtube source needs a yt-dlp binary at runtime (ffmpeg # recommended); without one it shows an actionable error. - os: ubuntu-latest target: x86_64-unknown-linux-gnu artifact_prefix: linux-x86_64 # most Linux distros binary_postfix: "" audio_features: "audio-viz,mpris,local-files,subsonic,internet-radio,youtube,qobuz" - os: ubuntu-24.04-arm # free native ARM64 runner for public repos target: aarch64-unknown-linux-gnu artifact_prefix: linux-aarch64 # Raspberry Pi 4/5, ARM servers, Asahi Linux binary_postfix: "" audio_features: "audio-viz,mpris,local-files,subsonic,internet-radio,youtube,qobuz" - os: macos-15-intel # Intel runner for x86_64 (available until Aug 2027) target: x86_64-apple-darwin artifact_prefix: macos-x86_64 binary_postfix: "" audio_features: "audio-viz-cpal,portaudio-backend,macos-media,local-files,subsonic,internet-radio,youtube,qobuz,apple-music" - os: macos-latest # Apple Silicon runner for aarch64 target: aarch64-apple-darwin artifact_prefix: macos-aarch64 binary_postfix: "" audio_features: "audio-viz-cpal,portaudio-backend,macos-media,local-files,subsonic,internet-radio,youtube,qobuz,apple-music" - os: windows-latest target: x86_64-pc-windows-msvc artifact_prefix: windows-x86_64 binary_postfix: ".exe" audio_features: "audio-viz-cpal,windows-media,local-files,subsonic,internet-radio,youtube,qobuz" steps: - name: Checkout sources uses: actions/checkout@v7 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Install Linux dependencies if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libpipewire-0.3-dev libspa-0.2-dev libasound2-dev - name: Install cargo-deb if: runner.os == 'Linux' run: cargo install cargo-deb --locked - name: Install macOS dependencies if: runner.os == 'macOS' run: brew install openssl@3 portaudio - name: Build release binary run: | if [ -z "${{ matrix.audio_features }}" ]; then cargo build --release --target ${{ matrix.target }} --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj else cargo build --release --target ${{ matrix.target }} --no-default-features --features telemetry,tui,streaming,discord-rpc,cover-art,self-update,scripting,mcp-server,ai-dj,${{ matrix.audio_features }} fi shell: bash - name: Package binary (Unix) if: runner.os != 'Windows' shell: bash run: | cd target/${{ matrix.target }}/release BINARY=${{ env.BINARY_NAME }}${{ matrix.binary_postfix }} strip $BINARY || true RELEASE_NAME=${{ env.BINARY_NAME }}-${{ matrix.artifact_prefix }} tar czvf ../../../$RELEASE_NAME.tar.gz $BINARY cd ../../.. # Self-update up to v0.45 refuses to update without this exact file. Never remove it. shasum -a 256 $RELEASE_NAME.tar.gz > $RELEASE_NAME.tar.gz.sha256 - name: Package Debian (.deb) if: runner.os == 'Linux' shell: bash run: | cargo deb --no-build --target ${{ matrix.target }} DEB_PATH=$(ls target/${{ matrix.target }}/debian/*.deb) cp "$DEB_PATH" . - name: Package binary (Windows) if: runner.os == 'Windows' shell: bash run: | cd target/${{ matrix.target }}/release BINARY=${{ env.BINARY_NAME }}${{ matrix.binary_postfix }} RELEASE_NAME=${{ env.BINARY_NAME }}-${{ matrix.artifact_prefix }} 7z a ../../../$RELEASE_NAME.zip $BINARY cd ../../.. # Self-update up to v0.45 refuses to update without this exact file. Never remove it. certutil -hashfile $RELEASE_NAME.zip sha256 | grep -E [A-Fa-f0-9]{64} > $RELEASE_NAME.zip.sha256 - name: Upload artifact (Unix) if: runner.os != 'Windows' uses: actions/upload-artifact@v7 with: name: ${{ env.BINARY_NAME }}-${{ matrix.artifact_prefix }} path: | ${{ env.BINARY_NAME }}-${{ matrix.artifact_prefix }}.tar.gz ${{ env.BINARY_NAME }}-${{ matrix.artifact_prefix }}.tar.gz.sha256 - name: Upload artifact (Linux .deb) if: runner.os == 'Linux' uses: actions/upload-artifact@v7 with: name: ${{ env.BINARY_NAME }}-${{ matrix.artifact_prefix }}-deb path: "*.deb" - name: Upload artifact (Windows) if: runner.os == 'Windows' uses: actions/upload-artifact@v7 with: name: ${{ env.BINARY_NAME }}-${{ matrix.artifact_prefix }} path: | ${{ env.BINARY_NAME }}-${{ matrix.artifact_prefix }}.zip ${{ env.BINARY_NAME }}-${{ matrix.artifact_prefix }}.zip.sha256 # Creates the release as a draft. The `publish` job makes it public after the # desktop installers and checksums.txt are on it, so every link works from the # start, and installed self-updaters never see a half-built release. release: name: Create GitHub Release needs: build runs-on: ubuntu-latest permissions: contents: write env: BASE: ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }} steps: - name: Checkout sources uses: actions/checkout@v7 - name: Download all artifacts uses: actions/download-artifact@v8 with: path: artifacts pattern: spotatui-* merge-multiple: true - name: Resolve .deb file names id: deb # cargo-deb names the package from Cargo.toml, not from the tag, so the # download links must use the file names that were built. run: | for arch in amd64 arm64; do f=$(find artifacts -maxdepth 1 -type f -name "spotatui_*_${arch}.deb" -print -quit) if [ -z "$f" ]; then echo "No ${arch} .deb found in artifacts/" >&2 exit 1 fi echo "${arch}=$(basename "$f")" >> "$GITHUB_OUTPUT" done # Tauri names the installers from desktop/Cargo.toml, which the release commit keeps equal to Cargo.toml. - name: Read the desktop version id: desktop run: echo "version=$(grep -m1 '^version' desktop/Cargo.toml | cut -d'"' -f2)" >> "$GITHUB_OUTPUT" # Pins the generated change list to the last public release, so a release # never repeats the one before it (v0.45.0 repeated v0.44.0). - name: Find the previous release id: prev env: GH_TOKEN: ${{ github.token }} run: echo "tag=$(gh release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --exclude-pre-releases --limit 1 --json tagName --jq '.[0].tagName // ""')" >> "$GITHUB_OUTPUT" - name: Create Release uses: softprops/action-gh-release@v3 with: files: artifacts/* draft: true generate_release_notes: true previous_tag: ${{ steps.prev.outputs.tag }} prerelease: ${{ contains(github.ref_name, '-') }} body: | **Install:** `curl -fsSL https://spotatui.com/install.sh | bash` on macOS and Linux, or `irm https://spotatui.com/install.ps1 | iex` on Windows. Already installed? Run `spotatui update --install`, or update with your package manager. ## Terminal app | | Download | |---|---| | **Windows** 10/11 | [64-bit .zip](${{ env.BASE }}/spotatui-windows-x86_64.zip) | | **macOS** | [Apple Silicon](${{ env.BASE }}/spotatui-macos-aarch64.tar.gz) · [Intel](${{ env.BASE }}/spotatui-macos-x86_64.tar.gz) | | **Linux** | [x86_64](${{ env.BASE }}/spotatui-linux-x86_64.tar.gz) · [ARM64](${{ env.BASE }}/spotatui-linux-aarch64.tar.gz) (Raspberry Pi 4/5) | | **Debian, Ubuntu** | [.deb amd64](${{ env.BASE }}/${{ steps.deb.outputs.amd64 }}) · [.deb arm64](${{ env.BASE }}/${{ steps.deb.outputs.arm64 }}) | ## Desktop app (preview) | | Download | |---|---| | **Windows** 10/11 | [Installer .exe](${{ env.BASE }}/spotatui-desktop_${{ steps.desktop.outputs.version }}_x64-setup.exe) | | **macOS** | [Apple Silicon .dmg](${{ env.BASE }}/spotatui_${{ steps.desktop.outputs.version }}_aarch64.dmg) · [Intel .dmg](${{ env.BASE }}/spotatui_${{ steps.desktop.outputs.version }}_x64.dmg) | | **Linux x86_64** | [AppImage](${{ env.BASE }}/spotatui-desktop_${{ steps.desktop.outputs.version }}_amd64.AppImage) · [.deb](${{ env.BASE }}/spotatui-desktop_${{ steps.desktop.outputs.version }}_amd64.deb) · [.rpm](${{ env.BASE }}/spotatui-desktop-${{ steps.desktop.outputs.version }}-1.x86_64.rpm) | | **Linux ARM64** | [AppImage](${{ env.BASE }}/spotatui-desktop_${{ steps.desktop.outputs.version }}_aarch64.AppImage) · [.deb](${{ env.BASE }}/spotatui-desktop_${{ steps.desktop.outputs.version }}_arm64.deb) · [.rpm](${{ env.BASE }}/spotatui-desktop-${{ steps.desktop.outputs.version }}-1.aarch64.rpm) | The Windows installer, the macOS app and the AppImage update themselves. A .deb or .rpm install updates with a new download from this page. Linux needs glibc 2.39 and WebKitGTK 4.1 (Ubuntu 24.04+, Debian 13, Fedora 40+, Arch).
First launch on Windows and macOS - **Windows:** the installer is not signed yet, so SmartScreen warns. Click **More info**, then **Run anyway**. - **macOS:** the app is not notarized, so macOS blocks the first launch. Close the dialog, open **System Settings > Privacy & Security**, click **Open Anyway** next to the spotatui message, and confirm.
Both apps play Spotify (Premium), Local Files, Subsonic, Internet Radio, YouTube and Qobuz, plus Apple Music on macOS. YouTube needs [yt-dlp](https://github.com/yt-dlp/yt-dlp) (ffmpeg recommended). **Verify a download:** [checksums.txt](${{ env.BASE }}/checksums.txt) has the SHA-256 of every file: `sha256sum -c --ignore-missing checksums.txt`. [Full changelog](${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/CHANGELOG.md) · [Code signing policy](${{ github.server_url }}/${{ github.repository }}#code-signing-policy) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upate Nix Version Reference id: nix_version run: | OLD_VERSION=$(grep -oP 'version\s*=\s*"\K[^"]+' flake.nix) echo "Version: $OLD_VERSION" NEW_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') echo "Version: $NEW_VERSION" sed -i "s/version *= *\"$OLD_VERSION\"/version = \"$NEW_VERSION\"/" flake.nix # The Tauri desktop app (desktop/), attached to the draft the job above # created. A failed row can be re-run alone; `publish` waits for every row. # The installer names carry no OS word, so self-update's OS+arch fallback and # the .deb, winget and Homebrew lookups never pick one up. # The updater bundles (.AppImage, -setup.exe, .app.tar.gz) are signed with the # TAURI_SIGNING_PRIVATE_KEY secret; only this job turns them on. desktop: name: Desktop ${{ matrix.os }} needs: release if: ${{ startsWith(github.ref, 'refs/tags/') }} runs-on: ${{ matrix.os }} permissions: contents: write env: GH_TOKEN: ${{ github.token }} defaults: run: shell: bash strategy: fail-fast: false matrix: include: - os: ubuntu-24.04 bundles: deb,rpm,appimage - os: ubuntu-24.04-arm # AppImage on arm64 is unverified bundles: deb,rpm,appimage - os: macos-15-intel bundles: app,dmg arch: x64 - os: macos-latest bundles: app,dmg arch: aarch64 - os: windows-latest bundles: nsis steps: # The build runs dependency code; `gh` reads GH_TOKEN, so no token stays in .git/config. - name: Checkout sources uses: actions/checkout@v7 with: persist-credentials: false - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install Node uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 cache: npm cache-dependency-path: gui/package-lock.json # desktop/build.rs refuses a release build without gui/dist. - name: Build the page working-directory: gui run: npm ci && npm run build # libfuse2t64 is for the AppImage tooling. - name: Install Linux dependencies if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libpipewire-0.3-dev libspa-0.2-dev libasound2-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev patchelf libfuse2t64 # Static portaudio, so the app runs without Homebrew (checked below). - name: Install macOS dependencies if: runner.os == 'macOS' run: | brew install portaudio echo "PKG_CONFIG_ALL_STATIC=1" >> "$GITHUB_ENV" # No --locked: desktop/Cargo.lock is refreshed in the release commit. # The prebuilt CLI from npm: `cargo install tauri-cli` compiled for up to 19 minutes per row. # The updater bundles are on here only, so local and PR builds need no key. - name: Build the desktop bundles working-directory: desktop env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: npx --yes @tauri-apps/cli@2.12.2 build --ci --bundles ${{ matrix.bundles }} --config '{"bundle":{"createUpdaterArtifacts":true}}' - name: Warn when desktop/Cargo.lock is stale run: git diff --quiet -- desktop/Cargo.lock || echo "::warning file=desktop/Cargo.lock::desktop/Cargo.lock was stale for this release; refresh it in the next release commit." - name: Check the macOS app links only system libraries if: runner.os == 'macOS' working-directory: desktop/target/release/bundle/macos run: | status=0 for bin in *.app/Contents/MacOS/*; do otool -L "$bin" foreign=$(otool -L "$bin" | tail -n +2 | awk '{print $1}' | grep -v -e '^/System/' -e '^/usr/lib/' || true) if [ -n "$foreign" ]; then echo "::error::$bin links libraries outside /System and /usr/lib: $foreign" status=1 fi done # The oldest macOS the portaudio bottle supports, for minimumSystemVersion. otool -l "$(brew --prefix portaudio)/lib/libportaudio.dylib" | grep -A4 LC_BUILD_VERSION || true exit $status - name: Show the Linux binary's shared libraries if: runner.os == 'Linux' run: ldd desktop/target/release/spotatui-desktop || echo "::warning::ldd failed on desktop/target/release/spotatui-desktop" # Both macOS rows build .app.tar.gz; the arch keeps them apart. - name: Name the macOS update bundle by arch if: runner.os == 'macOS' working-directory: desktop/target/release/bundle/macos run: | shopt -s nullglob src=(*.app.tar.gz) if [ ${#src[@]} -ne 1 ] || [ ! -f "${src[0]}.sig" ]; then echo "::error::Expected one signed .app.tar.gz, found: ${src[*]}" exit 1 fi name="spotatui_${GITHUB_REF_NAME#v}_${{ matrix.arch }}.app.tar.gz" mv "${src[0]}" "$name" mv "${src[0]}.sig" "$name.sig" - name: Upload installers to the release working-directory: desktop/target/release/bundle run: | shopt -s nullglob files=(deb/*.deb rpm/*.rpm appimage/*.AppImage dmg/*.dmg nsis/*-setup.exe) if [ ${#files[@]} -eq 0 ]; then echo "::error::No installers found under desktop/target/release/bundle" exit 1 fi # desktop-manifest copies each .sig into latest.json; publish then deletes them from the release. upload=("${files[@]}" appimage/*.AppImage.sig nsis/*-setup.exe.sig macos/*.app.tar.gz macos/*.app.tar.gz.sig) gh release upload "$GITHUB_REF_NAME" "${upload[@]}" --clobber --repo "$GITHUB_REPOSITORY" # latest.json, the file the desktop updater reads from the latest release. # Stable tags only, so a prerelease never reaches users. One failed desktop # row skips this job, so no manifest ships without a platform; re-running that # row runs it again. desktop-manifest: name: Desktop update manifest needs: desktop if: ${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-') }} runs-on: ubuntu-latest permissions: contents: write env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout sources uses: actions/checkout@v7 - name: Download the update signatures run: gh release download "$GITHUB_REF_NAME" --pattern '*.sig' --dir sigs --repo "$GITHUB_REPOSITORY" - name: Build and upload latest.json env: TAG: ${{ github.ref_name }} BASE_URL: ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }} CHANGELOG_URL: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/CHANGELOG.md run: | heading=$(grep -m1 "^## \[$TAG\]" CHANGELOG.md || true) if [ -n "$heading" ]; then anchor=$(printf '%s' "${heading#\#\# }" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9 -' | tr ' ' '-') CHANGELOG_URL="$CHANGELOG_URL#$anchor" fi platforms='{}' add() { local key=$1 pattern f name shift for pattern in "$@"; do f=$(find sigs -maxdepth 1 -type f -name "$pattern" -print -quit) if [ -n "$f" ]; then name=$(basename "$f" .sig) platforms=$(jq --arg k "$key" --arg sig "$(cat "$f")" --arg url "$BASE_URL/$name" '. + {($k): {signature: $sig, url: $url}}' <<<"$platforms") return fi done echo "::warning::No $key update bundle in $TAG" } add windows-x86_64 '*_x64-setup.exe.sig' add darwin-x86_64 '*_x64.app.tar.gz.sig' add darwin-aarch64 '*_aarch64.app.tar.gz.sig' add linux-x86_64 '*_amd64.AppImage.sig' '*_x86_64.AppImage.sig' add linux-aarch64 '*_aarch64.AppImage.sig' '*_arm64.AppImage.sig' for key in windows-x86_64 linux-x86_64; do if ! jq -e --arg k "$key" 'has($k)' <<<"$platforms" >/dev/null; then echo "::error::latest.json needs $key" exit 1 fi done jq -n --arg version "${TAG#v}" --arg notes "What changed: $CHANGELOG_URL" \ --arg pub_date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --argjson platforms "$platforms" \ '{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms}' > latest.json cat latest.json gh release upload "$TAG" latest.json --clobber --repo "$GITHUB_REPOSITORY" # Writes checksums.txt, checks every link in the notes, then makes a stable # release public. A release candidate stays a draft: installed self-updaters # up to v0.45 install a public prerelease without checking it. publish: name: Checksums and publish needs: [release, desktop, desktop-manifest] if: ${{ !cancelled() && startsWith(github.ref, 'refs/tags/') && needs.release.result == 'success' && needs.desktop.result == 'success' && needs.desktop-manifest.result != 'failure' }} runs-on: ubuntu-latest permissions: contents: write env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} TAG: ${{ github.ref_name }} steps: - name: Write checksums.txt run: | patterns=(--pattern 'spotatui*') if [[ "$TAG" != *-* ]]; then patterns+=(--pattern 'latest.json') fi gh release download "$TAG" "${patterns[@]}" --dir dl rm -f dl/*.sha256 dl/*.sig (cd dl && sha256sum * | sort -k2) > checksums.txt cat checksums.txt gh release upload "$TAG" checksums.txt --clobber - name: Check every download link run: | assets=$(gh release view "$TAG" --json assets --jq '.assets[].name') missing=0 for name in $(gh release view "$TAG" --json body --jq .body | grep -oE "releases/download/$TAG/[^)]+" | sed "s|.*/||" | sort -u); do if ! grep -qxF "$name" <<<"$assets"; then echo "::error::The notes link $name, which is not on the release" missing=1 fi done exit $missing # latest.json carries the signatures, so the .sig files leave the page just # before it goes public. Here, so desktop-manifest can still be re-run. - name: Publish the release run: | if [[ "$TAG" == *-* ]]; then echo "::notice::$TAG is a release candidate, so it stays a draft." exit 0 fi for name in $(gh release view "$TAG" --json assets --jq '.assets[].name | select(endswith(".sig"))'); do gh release delete-asset "$TAG" "$name" --yes || echo "::warning::Could not delete $name" done gh release edit "$TAG" --draft=false --latest announce-discord: name: Announce on Discord needs: publish runs-on: ubuntu-latest if: ${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, '-') }} steps: - name: Checkout sources uses: actions/checkout@v7 - name: Post the release and ping Release Pings env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} TAG: ${{ github.ref_name }} URL: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }} CHANGELOG_URL: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/CHANGELOG.md run: | heading=$(grep -m1 "^## \[$TAG\]" CHANGELOG.md || true) if [ -n "$heading" ]; then anchor=$(printf '%s' "${heading#\#\# }" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9 -' | tr ' ' '-') CHANGELOG_URL="$CHANGELOG_URL#$anchor" fi jq -n --arg tag "$TAG" --arg url "$URL" --arg changelog "$CHANGELOG_URL" '{ content: "<@&1534823673851150377> spotatui \($tag) is out: \($url)\nWhat changed: <\($changelog)>", allowed_mentions: {roles: ["1534823673851150377"]} }' | curl -fsS -H "Content-Type: application/json" -d @- "$DISCORD_WEBHOOK" publish-cargo: name: Publish to crates.io needs: publish runs-on: ubuntu-latest if: ${{ !contains(github.ref_name, '-') }} steps: - name: Checkout sources uses: actions/checkout@v7 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Install Linux dependencies run: | sudo apt-get update sudo apt-get install -y -qq pkg-config libssl-dev libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev - name: Publish to crates.io run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} --allow-dirty publish-aur: name: Publish to AUR needs: publish runs-on: ubuntu-latest if: ${{ !contains(github.ref_name, '-') }} steps: - name: Checkout sources uses: actions/checkout@v7 - name: Extract version id: version run: | VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Version: $VERSION" - name: Calculate SHA256 id: sha256 run: | TARBALL_URL="https://github.com/${{ github.repository }}/archive/refs/tags/v${{ steps.version.outputs.version }}.tar.gz" SHA256=$(curl -sL "$TARBALL_URL" | sha256sum | awk '{print $1}') echo "sha256=$SHA256" >> $GITHUB_OUTPUT echo "SHA256: $SHA256" - name: Setup SSH for AUR run: | mkdir -p ~/.ssh echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur chmod 600 ~/.ssh/aur ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts git config --global user.name "${{ secrets.AUR_USERNAME }}" git config --global user.email "${{ secrets.AUR_EMAIL }}" - name: Clone AUR repository run: | GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes' git clone ssh://aur@aur.archlinux.org/spotatui.git aur - name: Update PKGBUILD working-directory: aur run: | sed -i "s/^pkgver=.*/pkgver=${{ steps.version.outputs.version }}/" PKGBUILD sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD sed -i "s/^sha256sums=.*/sha256sums=('${{ steps.sha256.outputs.sha256 }}')/" PKGBUILD # Ensure AUR builds work with default system linker (avoids LLVM bitcode/LTO linker errors). if ! grep -q "CARGO_PROFILE_RELEASE_LTO=false" PKGBUILD; then sed -i "/^[[:space:]]*build() {[[:space:]]*$/a\\ export CARGO_PROFILE_RELEASE_LTO=false" PKGBUILD fi - name: Generate .SRCINFO working-directory: aur run: | docker run --rm -v $PWD:/pkg archlinux:latest /bin/bash -c " pacman -Syu --noconfirm --needed base-devel && useradd -m builder && chown -R builder:builder /pkg && cd /pkg && su builder -c 'makepkg --printsrcinfo' > .SRCINFO " sudo chown -R $(id -u):$(id -g) . - name: Commit and push to AUR working-directory: aur run: | git add PKGBUILD .SRCINFO git commit -m "Update to ${{ steps.version.outputs.version }}" GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes' git push publish-aur-bin: name: Publish to AUR (spotatui-bin) needs: publish runs-on: ubuntu-latest if: ${{ !contains(github.ref_name, '-') }} steps: - name: Checkout sources uses: actions/checkout@v7 - name: Extract version id: version run: | VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Version: $VERSION" - name: Calculate SHA256 checksums id: sha256 run: | VERSION="${{ steps.version.outputs.version }}" # SHA256 for the binary tarball BINARY_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/spotatui-linux-x86_64.tar.gz" SHA256_BINARY=$(curl -sL "$BINARY_URL" | sha256sum | awk '{print $1}') echo "sha256_binary=$SHA256_BINARY" >> $GITHUB_OUTPUT echo "Binary SHA256: $SHA256_BINARY" # SHA256 for LICENSE LICENSE_URL="https://raw.githubusercontent.com/${{ github.repository }}/v${VERSION}/LICENSE" SHA256_LICENSE=$(curl -sL "$LICENSE_URL" | sha256sum | awk '{print $1}') echo "sha256_license=$SHA256_LICENSE" >> $GITHUB_OUTPUT echo "LICENSE SHA256: $SHA256_LICENSE" # SHA256 for README.md README_URL="https://raw.githubusercontent.com/${{ github.repository }}/v${VERSION}/README.md" SHA256_README=$(curl -sL "$README_URL" | sha256sum | awk '{print $1}') echo "sha256_readme=$SHA256_README" >> $GITHUB_OUTPUT echo "README SHA256: $SHA256_README" - name: Setup SSH for AUR run: | mkdir -p ~/.ssh echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur chmod 600 ~/.ssh/aur ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts git config --global user.name "${{ secrets.AUR_USERNAME }}" git config --global user.email "${{ secrets.AUR_EMAIL }}" - name: Clone AUR repository run: | GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes' git clone ssh://aur@aur.archlinux.org/spotatui-bin.git aur-bin - name: Update PKGBUILD working-directory: aur-bin run: | # Update version and release sed -i "s/^pkgver=.*/pkgver=${{ steps.version.outputs.version }}/" PKGBUILD sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD # Remove existing checksum lines (handles both single and multi-line) sed -i '/^sha256sums=/d' PKGBUILD sed -i '/^sha256sums_x86_64=/d' PKGBUILD sed -i "/^[[:space:]]*'[a-f0-9]\{64\}')/d" PKGBUILD # Insert new single-line checksums before package() SHA_L="${{ steps.sha256.outputs.sha256_license }}" SHA_R="${{ steps.sha256.outputs.sha256_readme }}" SHA_B="${{ steps.sha256.outputs.sha256_binary }}" sed -i "/^package()/i sha256sums=('${SHA_L}' '${SHA_R}')\nsha256sums_x86_64=('${SHA_B}')" PKGBUILD # Validation echo "=== Validating PKGBUILD ===" grep "^pkgver=" PKGBUILD grep "^sha256sums=" PKGBUILD || { echo "ERROR: sha256sums not found"; exit 1; } grep "^sha256sums_x86_64=" PKGBUILD || { echo "ERROR: sha256sums_x86_64 not found"; exit 1; } grep -q "package()" PKGBUILD || { echo "ERROR: package() function missing"; exit 1; } echo "=== PKGBUILD validation successful ===" - name: Generate .SRCINFO working-directory: aur-bin run: | docker run --rm -v $PWD:/pkg archlinux:latest /bin/bash -c " pacman -Syu --noconfirm --needed base-devel && useradd -m builder && chown -R builder:builder /pkg && cd /pkg && su builder -c 'makepkg --printsrcinfo' > .SRCINFO " sudo chown -R $(id -u):$(id -g) . - name: Commit and push to AUR working-directory: aur-bin run: | git add PKGBUILD .SRCINFO git commit -m "Update to ${{ steps.version.outputs.version }}" GIT_SSH_COMMAND='ssh -i ~/.ssh/aur -o IdentitiesOnly=yes' git push publish-winget: name: Publish to winget needs: publish runs-on: windows-latest if: ${{ !contains(github.ref_name, '-') }} steps: - name: Install cargo-binstall uses: cargo-bins/cargo-binstall@main env: GITHUB_TOKEN: ${{ github.token }} - name: Install komac shell: pwsh run: cargo binstall komac@2.15.0 -y env: GITHUB_TOKEN: ${{ github.token }} - name: Submit to winget-pkgs shell: pwsh run: | $ReleaseInfo = Invoke-RestMethod ` -Uri "https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ github.ref_name }}" ` -Headers @{ Authorization = "token $env:GITHUB_TOKEN" } $Version = "${{ github.ref_name }}" -replace '^v' $Urls = $ReleaseInfo.assets.Where({ $_.name -match '-windows-x86_64\.zip$' }).browser_download_url -join ' ' if ([string]::IsNullOrWhiteSpace($Urls)) { Write-Error "No Windows x86_64 zip release asset found" exit 1 } komac update 'LargeModGames.spotatui' --version "$Version" --submit --urls $Urls env: GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} KOMAC_FORK_OWNER: LargeModGames KOMAC_CREATED_WITH: spotatui CD workflow KOMAC_CREATED_WITH_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} publish-homebrew: name: Publish to Homebrew needs: publish runs-on: ubuntu-latest if: ${{ !contains(github.ref_name, '-') }} steps: - name: Checkout sources uses: actions/checkout@v7 - name: Extract version id: version run: | VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Version: $VERSION" - name: Calculate SHA256 checksums id: sha256 run: | VERSION="${{ steps.version.outputs.version }}" # macOS aarch64 AARCH64_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/spotatui-macos-aarch64.tar.gz" SHA256_AARCH64=$(curl -sL "$AARCH64_URL" | sha256sum | awk '{print $1}') echo "sha256_aarch64=$SHA256_AARCH64" >> $GITHUB_OUTPUT echo "macOS aarch64 SHA256: $SHA256_AARCH64" # macOS x86_64 X86_64_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/spotatui-macos-x86_64.tar.gz" SHA256_X86_64=$(curl -sL "$X86_64_URL" | sha256sum | awk '{print $1}') echo "sha256_x86_64=$SHA256_X86_64" >> $GITHUB_OUTPUT echo "macOS x86_64 SHA256: $SHA256_X86_64" - name: Setup SSH for Homebrew tap run: | mkdir -p ~/.ssh echo "${{ secrets.HOMEBREW_TAP_SSH_KEY }}" > ~/.ssh/homebrew_tap chmod 600 ~/.ssh/homebrew_tap ssh-keyscan github.com >> ~/.ssh/known_hosts git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" - name: Clone Homebrew tap repository run: | GIT_SSH_COMMAND='ssh -i ~/.ssh/homebrew_tap -o IdentitiesOnly=yes' \ git clone git@github.com:LargeModGames/homebrew-spotatui.git homebrew-tap - name: Update formula working-directory: homebrew-tap run: | cat > Formula/spotatui.rb << 'FORMULA_EOF' # typed: false # frozen_string_literal: true # This formula is automatically updated by the CD workflow on each release. # Manual changes will be overwritten. class Spotatui < Formula desc "Terminal music player for Spotify, local files, Subsonic, radio, YouTube, Qobuz" homepage "https://spotatui.com" version "${{ steps.version.outputs.version }}" license "MIT" depends_on :macos depends_on "portaudio" depends_on "openssl@3" on_arm do url "https://github.com/LargeModGames/spotatui/releases/download/v#{version}/spotatui-macos-aarch64.tar.gz" sha256 "${{ steps.sha256.outputs.sha256_aarch64 }}" end on_intel do url "https://github.com/LargeModGames/spotatui/releases/download/v#{version}/spotatui-macos-x86_64.tar.gz" sha256 "${{ steps.sha256.outputs.sha256_x86_64 }}" end def install bin.install "spotatui" end def caveats <<~EOS Spotify in spotatui needs a Spotify Premium account. Run `spotatui`, pick Spotify, and confirm the code it shows on Spotify's pairing page. Note: Native streaming on macOS requires 'portaudio' (installed as a dependency). More info: https://github.com/LargeModGames/spotatui EOS end test do assert_match "spotatui", shell_output("#{bin}/spotatui --version") end end FORMULA_EOF - name: Commit and push to Homebrew tap working-directory: homebrew-tap run: | git add Formula/spotatui.rb git diff --cached --quiet && echo "No changes to commit" && exit 0 git commit -m "Update spotatui to ${{ steps.version.outputs.version }}" GIT_SSH_COMMAND='ssh -i ~/.ssh/homebrew_tap -o IdentitiesOnly=yes' git push