🏡 index : ironmagma/quintodrome.git

name: Desktop Release

# Builds the Quintodrome desktop app (Tauri) for macOS (.app/.dmg), bundling
# the Navidrome server as a sidecar, attaches the artifacts to the Forgejo
# release on `v*` tags, and updates the Homebrew tap. Windows isn't built here
# (no Windows runner); the Windows steps below are kept for when one exists.
#
# Note: artifacts are not signed with Apple/Windows certs (macOS builds are only
# ad-hoc signed), so users will hit Gatekeeper/SmartScreen prompts. The
# Homebrew cask works around Gatekeeper by stripping the quarantine flag.

on:
  push:
    tags: ["v*"]
  workflow_dispatch:

concurrency:
  group: desktop-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-desktop:
    name: Build desktop (${{ matrix.target }})
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-latest
            target: aarch64-apple-darwin
          # Cross-compiled on the same Mac runner; there is no Intel runner.
          - os: macos-latest
            target: x86_64-apple-darwin
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v6

      - name: Setup Node
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: npm
          cache-dependency-path: ui/package-lock.json

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version-file: go.mod

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Cache Rust build
        uses: Swatinem/rust-cache@v2
        with:
          workspaces: desktop/src-tauri

      # Navidrome's sqlite_fts5 tag needs CGO, so on Windows we need a gcc.
      - name: Setup Windows CGO (mingw)
        if: runner.os == 'Windows'
        uses: msys2/setup-msys2@v2
        with:
          msystem: MINGW64
          install: mingw-w64-x86_64-gcc
          update: false

      - name: Add mingw to PATH
        if: runner.os == 'Windows'
        shell: bash
        run: echo "C:/msys64/mingw64/bin" >> $GITHUB_PATH

      - name: Build Navidrome web UI
        shell: bash
        run: |
          cd ui
          # --ignore-scripts skips the workbox postinstall (bin/update-workbox.sh),
          # a bash script that fails under cmd.exe on Windows. It's only needed
          # for the PWA service worker, which the desktop app doesn't use.
          npm ci --ignore-scripts
          npm run build

      - name: Build Navidrome server binary
        shell: bash
        run: |
          GIT_SHA=$(git rev-parse --short HEAD)
          if [[ "$GITHUB_REF" == refs/tags/* ]]; then
            GIT_TAG="${GITHUB_REF_NAME#v}"
          else
            GIT_TAG="dev"
          fi

          if [[ "$RUNNER_OS" == "macOS" ]]; then
            # Build for the matrix target, not the runner's own arch.
            case "${{ matrix.target }}" in
              aarch64-*) export GOARCH=arm64 CC="clang -arch arm64" ;;
              x86_64-*) export GOARCH=amd64 CC="clang -arch x86_64" ;;
            esac
            export CGO_ENABLED=1
          fi

          if [[ "$RUNNER_OS" == "Windows" ]]; then
            export CGO_ENABLED=1
            OUTPUT="navidrome.exe"
          else
            OUTPUT="navidrome"
          fi

          go build \
            -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$GIT_SHA -X github.com/navidrome/navidrome/consts.gitTag=$GIT_TAG" \
            -tags=netgo,sqlite_fts5 \
            -o "$OUTPUT"

          # Stage the sidecar under the target-triple name tauri expects.
          mkdir -p desktop/src-tauri/binaries
          if [[ "$RUNNER_OS" == "Windows" ]]; then
            cp navidrome.exe "desktop/src-tauri/binaries/navidrome-${{ matrix.target }}.exe"
          else
            cp navidrome "desktop/src-tauri/binaries/navidrome-${{ matrix.target }}"
            chmod +x "desktop/src-tauri/binaries/navidrome-${{ matrix.target }}"
          fi

      - name: Build Tauri app
        shell: bash
        run: |
          cd desktop
          npx --yes @tauri-apps/cli@2 build --target ${{ matrix.target }}

      - name: Collect artifacts (macOS)
        if: runner.os == 'macOS'
        shell: bash
        run: |
          mkdir -p dist
          BUNDLE=desktop/src-tauri/target/${{ matrix.target }}/release/bundle
          ditto -c -k --keepParent "$BUNDLE/macos/Quintodrome.app" "dist/Quintodrome-${{ matrix.target }}.app.zip"
          cp "$BUNDLE"/dmg/*.dmg dist/ 2>/dev/null || true
          ls -la dist

      - name: Collect artifacts (Windows)
        if: runner.os == 'Windows'
        shell: bash
        run: |
          mkdir -p dist
          BUNDLE=desktop/src-tauri/target/${{ matrix.target }}/release/bundle
          cp "$BUNDLE"/nsis/*.exe dist/ 2>/dev/null || true
          cp "$BUNDLE"/msi/*.msi dist/ 2>/dev/null || true
          ls -la dist

      - name: Upload artifacts
        uses: actions/upload-artifact@v7
        with:
          name: desktop-${{ matrix.target }}
          path: dist/*
          retention-days: 7

  release:
    name: Publish Gitea release
    needs: build-desktop
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v8
        with:
          path: dist
          pattern: desktop-*
          merge-multiple: true

      - run: ls -R dist

      - name: Create release and upload assets
        env:
          TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          # tag_name must be the pushed tag itself (with its "v"), or Forgejo
          # creates a second tag without it.
          RELEASE_ID=$(curl -fsS -X POST "$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases" \
            -H "Authorization: token $TOKEN" \
            -H "Content-Type: application/json" \
            -d "{\"tag_name\":\"$GITHUB_REF_NAME\",\"name\":\"${GITHUB_REF_NAME#v}\",\"draft\":false}" \
            | jq -r '.id')
          for f in dist/*; do
            curl -fsS -X POST "$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=$(basename "$f")" \
              -H "Authorization: token $TOKEN" \
              -H "Content-Type: application/octet-stream" \
              --data-binary @"$f"
          done

  homebrew-tap:
    name: Update Homebrew tap
    needs: release
    runs-on: ubuntu-latest
    env:
      TAP_REPO: git@forge.quinefoundation.com:Quine/homebrew-quintodrome.git
    steps:
      - uses: actions/download-artifact@v8
        with:
          path: dist
          pattern: desktop-*-apple-darwin
          merge-multiple: true

      # The app is only ad-hoc signed, so the cask strips the quarantine flag
      # that Homebrew adds; otherwise Gatekeeper refuses to open it.
      - name: Write cask
        run: |
          set -euo pipefail
          VERSION="${GITHUB_REF_NAME#v}"
          ARM_SHA=$(sha256sum dist/Quintodrome-aarch64-apple-darwin.app.zip | cut -d' ' -f1)
          INTEL_SHA=$(sha256sum dist/Quintodrome-x86_64-apple-darwin.app.zip | cut -d' ' -f1)
          mkdir -p tap/Casks
          cat > tap/Casks/quintodrome.rb <<CASK
          cask "quintodrome" do
            arch arm: "aarch64", intel: "x86_64"

            version "$VERSION"
            sha256 arm:   "$ARM_SHA",
                   intel: "$INTEL_SHA"

            url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/v#{version}/Quintodrome-#{arch}-apple-darwin.app.zip"
            name "Quintodrome"
            desc "Desktop app wrapping the Navidrome music server"
            homepage "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"

            app "Quintodrome.app"

            postflight do
              system_command "/usr/bin/xattr",
                             args: ["-dr", "com.apple.quarantine", "#{appdir}/Quintodrome.app"]
            end

            zap trash: [
              "~/Library/Application Support/com.quineglobal.drome",
              "~/Library/Application Support/com.quintodrome.desktop",
            ]
          end
          CASK
          cat tap/Casks/quintodrome.rb

      - name: Push to tap
        env:
          SSH_KEY: ${{ secrets.HOMEBREW_TAP_SSH_KEY }}
        run: |
          set -euo pipefail
          if [[ -z "$SSH_KEY" ]]; then
            echo "::error::HOMEBREW_TAP_SSH_KEY secret is not set (add it under this repo's Settings > Actions > Secrets; its public key needs write access on the tap repo)"
            exit 1
          fi
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/tap_key
          chmod 600 ~/.ssh/tap_key
          echo "forge.quinefoundation.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID2WBpBywsGATL7IMkvjlYguBfeYOxPstTcsPuH0Xdp3" > ~/.ssh/known_hosts
          export GIT_SSH_COMMAND="ssh -i ~/.ssh/tap_key -o IdentitiesOnly=yes"
          git clone "$TAP_REPO" repo
          mkdir -p repo/Casks
          cp tap/Casks/quintodrome.rb repo/Casks/
          cd repo
          git config user.name "Quintodrome release"
          git config user.email "noreply@forge.quinefoundation.com"
          git add Casks/quintodrome.rb
          git diff --cached --quiet && { echo "Cask unchanged"; exit 0; }
          git commit -m "quintodrome ${GITHUB_REF_NAME#v}"
          git push