name: Desktop Release
on:
push:
tags: ["v*"]
workflow_dispatch:
concurrency:
group: desktop-${{ github.ref }}
cancel-in-progress: true
jobs:
build-desktop:
name: Build desktop (${{ matrix.name }})
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
name: macos
targets: aarch64-apple-darwin,x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: ui/package-lock.json
- name: Setup Go
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
GO_MINOR=$(awk '/^go /{print $2}' go.mod)
GO_VERSION=$(curl -fsSL 'https://go.dev/dl/?mode=json&include=all' \
| jq -r --arg m "go$GO_MINOR" '[.[] | select(.stable) | .version | select(. == $m or startswith($m + "."))][0]')
case "$(uname -s)/$(uname -m)" in
Darwin/arm64) PLATFORM=darwin-arm64 ;;
Darwin/x86_64) PLATFORM=darwin-amd64 ;;
Linux/x86_64) PLATFORM=linux-amd64 ;;
Linux/aarch64) PLATFORM=linux-arm64 ;;
esac
GOROOT_PARENT="$RUNNER_TEMP/go-$GO_VERSION"
if [[ ! -x "$GOROOT_PARENT/go/bin/go" ]]; then
mkdir -p "$GOROOT_PARENT"
curl -fsSL "https://go.dev/dl/$GO_VERSION.$PLATFORM.tar.gz" | tar -xz -C "$GOROOT_PARENT"
fi
echo "$GOROOT_PARENT/go/bin" >> "$GITHUB_PATH"
"$GOROOT_PARENT/go/bin/go" version
- name: Setup Go (Windows)
if: runner.os == 'Windows'
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Setup Rust
uses: https://github.com/dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.targets }}
- name: Cache Rust build
uses: https://github.com/Swatinem/rust-cache@v2
with:
workspaces: desktop/src-tauri
- name: Setup Sparkle
if: runner.os == 'macOS'
shell: bash
run: |
bash desktop/scripts/download-sparkle.sh desktop/src-tauri
echo "SPARKLE_FRAMEWORK_PATH=$PWD/desktop/src-tauri" >> "$GITHUB_ENV"
- name: Setup Windows CGO (mingw)
if: runner.os == 'Windows'
uses: https://github.com/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
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
export CGO_ENABLED=1
mkdir -p desktop/src-tauri/binaries
for TARGET in $(echo "${{ matrix.targets }}" | tr , " "); do
case "$TARGET" in
aarch64-apple-*) export GOARCH=arm64 CC="clang -arch arm64" ;;
x86_64-apple-*) export GOARCH=amd64 CC="clang -arch x86_64" ;;
esac
if [[ "$RUNNER_OS" == "Windows" ]]; then
OUTPUT="desktop/src-tauri/binaries/navidrome-$TARGET.exe"
else
OUTPUT="desktop/src-tauri/binaries/navidrome-$TARGET"
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"
done
- name: Build Tauri app
shell: bash
run: |
cd desktop
CONFIG=()
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
CONFIG=(--config "{\"version\":\"${GITHUB_REF_NAME#v}\",\"bundle\":{\"macOS\":{\"bundleVersion\":\"$GITHUB_RUN_NUMBER\"}}}")
fi
for TARGET in $(echo "${{ matrix.targets }}" | tr , " "); do
rustup target add "$TARGET"
npx --yes @tauri-apps/cli@2 build --target "$TARGET" "${CONFIG[@]}"
done
- name: Collect artifacts (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
mkdir -p dist
for TARGET in $(echo "${{ matrix.targets }}" | tr , " "); do
BUNDLE=desktop/src-tauri/target/$TARGET/release/bundle
ditto -c -k --keepParent "$BUNDLE/macos/Quintodrome.app" "dist/Quintodrome-$TARGET.app.zip"
cp "$BUNDLE"/dmg/*.dmg dist/ 2>/dev/null || true
done
ls -la dist
- name: Sign updates (macOS)
if: runner.os == 'macOS'
shell: bash
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
set -euo pipefail
if [[ -z "$SPARKLE_PRIVATE_KEY" ]]; then
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
echo "::error::SPARKLE_PRIVATE_KEY secret is not set (this repo's Settings > Actions > Secrets)"
exit 1
fi
echo "No SPARKLE_PRIVATE_KEY; not signing (not a release)"
exit 0
fi
KEY_FILE="$RUNNER_TEMP/sparkle_key"
trap 'rm -f "$KEY_FILE"' EXIT
(umask 077; printf '%s\n' "$SPARKLE_PRIVATE_KEY" > "$KEY_FILE")
mkdir -p sparkle
for TARGET in $(echo "${{ matrix.targets }}" | tr , " "); do
ARCH="${TARGET%%-*}"
desktop/src-tauri/sparkle-bin/sign_update "dist/Quintodrome-$TARGET.app.zip" -f "$KEY_FILE" > "sparkle/$ARCH.txt"
cat "sparkle/$ARCH.txt"
done
- name: Collect artifacts (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
mkdir -p dist
for TARGET in $(echo "${{ matrix.targets }}" | tr , " "); do
BUNDLE=desktop/src-tauri/target/$TARGET/release/bundle
cp "$BUNDLE"/nsis/*.exe dist/ 2>/dev/null || true
cp "$BUNDLE"/msi/*.msi dist/ 2>/dev/null || true
done
ls -la dist
- name: Upload artifacts
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: desktop-${{ matrix.name }}
path: dist/*
retention-days: 7
- name: Publish release (macOS)
if: runner.os == 'macOS' && startsWith(github.ref, 'refs/tags/')
shell: bash
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
NOTES=$(curl -fsS "$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/tags/$GITHUB_REF_NAME" \
-H "Authorization: token $TOKEN" | jq -r '.message // ""')
printf '%s\n' "$NOTES" > notes.txt
API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
RELEASE=$(curl -sS "$API/releases/tags/$GITHUB_REF_NAME" -H "Authorization: token $TOKEN")
RELEASE_ID=$(jq -r '.id // empty' <<<"$RELEASE")
if [[ -z "$RELEASE_ID" ]]; then
RELEASE=$(jq -n --arg tag "$GITHUB_REF_NAME" --arg name "${GITHUB_REF_NAME#v}" --arg body "$NOTES" \
'{tag_name: $tag, name: $name, body: $body, draft: false}' \
| curl -fsS -X POST "$API/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @-)
RELEASE_ID=$(jq -r '.id' <<<"$RELEASE")
fi
for f in dist/*; do
NAME=$(basename "$f")
OLD=$(jq -r --arg n "$NAME" '.assets[]? | select(.name == $n) | .id' <<<"$RELEASE")
for id in $OLD; do
curl -fsS -X DELETE "$API/releases/$RELEASE_ID/assets/$id" -H "Authorization: token $TOKEN"
done
curl -fsS -o /dev/null -X POST "$API/releases/$RELEASE_ID/assets?name=$NAME" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$f"
echo "uploaded $NAME"
done
- name: Write cask and appcasts (macOS)
if: runner.os == 'macOS' && startsWith(github.ref, 'refs/tags/')
shell: bash
env:
FORGE_URL: https://forge.in.quineglobal.com
GIT_URL: https://git.quineglobal.com
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
ARM_SHA=$(shasum -a 256 dist/Quintodrome-aarch64-apple-darwin.app.zip | cut -d' ' -f1)
INTEL_SHA=$(shasum -a 256 dist/Quintodrome-x86_64-apple-darwin.app.zip | cut -d' ' -f1)
mkdir -p tap/Casks tap/appcasts
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 "$FORGE_URL/$GITHUB_REPOSITORY/releases/download/v#{version}/Quintodrome-#{arch}-apple-darwin.app.zip"
name "Quintodrome"
desc "Desktop app wrapping the Navidrome music server"
homepage "$GIT_URL/$GITHUB_REPOSITORY.git"
# The app updates itself (Sparkle), so \`brew upgrade\` leaves it
# be unless asked with --greedy.
auto_updates true
depends_on :macos
app "Quintodrome.app"
postflight_steps do
run "/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
# Sparkle shows the notes in its update window: the tag's message,
# as HTML (escaped, one line per line).
NOTES_HTML=$(sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g' -e 's/$/<br>/' notes.txt)
# RFC 822, in English whatever the Mac's locale (BSD date has no -R).
PUB_DATE=$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')
for ARCH in aarch64 x86_64; do
SIGNATURE=$(cat "sparkle/$ARCH.txt")
cat > "tap/appcasts/quintodrome-$ARCH.xml" <<APPCAST
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Quintodrome</title>
<item>
<title>Quintodrome $VERSION</title>
<sparkle:version>$GITHUB_RUN_NUMBER</sparkle:version>
<sparkle:shortVersionString>$VERSION</sparkle:shortVersionString>
<pubDate>$PUB_DATE</pubDate>
<description><![CDATA[$NOTES_HTML]]></description>
<enclosure url="$FORGE_URL/$GITHUB_REPOSITORY/releases/download/$GITHUB_REF_NAME/Quintodrome-$ARCH-apple-darwin.app.zip" type="application/octet-stream" $SIGNATURE/>
</item>
</channel>
</rss>
APPCAST
cat "tap/appcasts/quintodrome-$ARCH.xml"
done
- name: Push to tap (macOS)
if: runner.os == 'macOS' && startsWith(github.ref, 'refs/tags/')
shell: bash
env:
SSH_KEY: ${{ secrets.HOMEBREW_TAP_SSH_KEY }}
TAP_REPO: ssh://git@forge.in.quineglobal.com:2200/quine-global/homebrew-tap.git
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
KEY_FILE="$RUNNER_TEMP/tap_key"
KNOWN_HOSTS="$RUNNER_TEMP/tap_known_hosts"
trap 'rm -f "$KEY_FILE" "$KNOWN_HOSTS"' EXIT
(umask 077; printf '%s\n' "$SSH_KEY" > "$KEY_FILE")
echo "[forge.in.quineglobal.com]:2200 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHECXyh0SwRlXikgu919nC9OTmAU7AEcrlbJ/J/H9DTU" > "$KNOWN_HOSTS"
export GIT_SSH_COMMAND="ssh -i $KEY_FILE -o IdentitiesOnly=yes -o UserKnownHostsFile=$KNOWN_HOSTS -o StrictHostKeyChecking=yes"
rm -rf repo
git clone -q "$TAP_REPO" repo
mkdir -p repo/Casks repo/appcasts
cp tap/Casks/quintodrome.rb repo/Casks/
cp tap/appcasts/quintodrome-*.xml repo/appcasts/
cd repo
git config user.name "Quintodrome release"
git config user.email "noreply@forge.in.quineglobal.com"
git add Casks/quintodrome.rb appcasts/quintodrome-*.xml
git diff --cached --quiet && { echo "Cask and appcasts unchanged"; exit 0; }
git commit -q -m "quintodrome ${GITHUB_REF_NAME#v}"
git push -q