name: Release on: push: tags: - 'v*' workflow_dispatch: jobs: create-release: permissions: contents: write runs-on: ubuntu-latest outputs: release_id: ${{ steps.create-release.outputs.result }} package_version: ${{ steps.get-version.outputs.version }} steps: - uses: actions/checkout@v5 - name: setup node uses: actions/setup-node@v5 with: node-version: lts/* cache: 'npm' - name: get version id: get-version run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT - name: extract changelog id: changelog run: | VERSION="${{ steps.get-version.outputs.version }}" # Extract the block between ## [VERSION] and the next ## heading BODY=$(awk "/^## \[$VERSION\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md) # Store multiline output EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) echo "body<<$EOF" >> $GITHUB_OUTPUT echo "$BODY" >> $GITHUB_OUTPUT echo "$EOF" >> $GITHUB_OUTPUT - name: create release id: create-release uses: actions/github-script@v7 env: PACKAGE_VERSION: ${{ steps.get-version.outputs.version }} CHANGELOG_BODY: ${{ steps.changelog.outputs.body }} with: script: | const tag = `app-v${process.env.PACKAGE_VERSION}`; const body = process.env.CHANGELOG_BODY || 'See the assets to download this version and install.'; try { const { data } = await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag, }); await github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, release_id: data.id, body, }); return data.id; } catch (e) { if (e.status !== 404) throw e; } const { data } = await github.rest.repos.createRelease({ owner: context.repo.owner, repo: context.repo.repo, tag_name: tag, name: `Psysonic v${process.env.PACKAGE_VERSION}`, body, draft: true, prerelease: false }); return data.id; build-macos-windows: needs: create-release permissions: contents: write strategy: fail-fast: false matrix: settings: - platform: 'macos-latest' args: '--target aarch64-apple-darwin' - platform: 'macos-latest' args: '--target x86_64-apple-darwin' - platform: 'windows-latest' args: '--bundles nsis' runs-on: ${{ matrix.settings.platform }} steps: - uses: actions/checkout@v5 - name: setup node uses: actions/setup-node@v5 with: node-version: lts/* cache: 'npm' - name: install Rust stable uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: cache cargo uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - name: install npm dependencies run: npm install - name: write Apple API key (macOS only) if: runner.os == 'macOS' run: | mkdir -p ~/private_keys echo "${{ secrets.APPLE_API_KEY_B64 }}" | base64 --decode > ~/private_keys/AuthKey.p8 echo "APPLE_API_KEY_PATH=$HOME/private_keys/AuthKey.p8" >> $GITHUB_ENV - uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VITE_LASTFM_API_KEY: ${{ secrets.VITE_LASTFM_API_KEY }} VITE_LASTFM_API_SECRET: ${{ secrets.VITE_LASTFM_API_SECRET }} # Apple signing + notarization (macOS runner only — ignored on Windows) APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} # APPLE_API_KEY_PATH comes from the previous step via $GITHUB_ENV # Tauri Updater signing — produces .sig files alongside the update bundles TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} with: releaseId: ${{ needs.create-release.outputs.release_id }} args: ${{ matrix.settings.args }} - name: re-sign updater bundle + upload .sig (macOS only) # tauri-action re-packs the .app into .app.tar.gz after tauri CLI is # done, which invalidates the .sig tauri CLI created (different hash). # We can't stop the repack (it's tied to includeUpdaterJson), so we # sign the final repacked .tar.gz ourselves and upload the fresh .sig. if: runner.os == 'macOS' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | set -e VERSION=${{ needs.create-release.outputs.package_version }} TARGET_ARG='${{ matrix.settings.args }}' if echo "$TARGET_ARG" | grep -q 'aarch64'; then TARGET="aarch64-apple-darwin" ARCH="aarch64" else TARGET="x86_64-apple-darwin" ARCH="x64" fi TARBALL="src-tauri/target/${TARGET}/release/bundle/macos/Psysonic.app.tar.gz" if [ ! -f "$TARBALL" ]; then echo "::error::Expected tarball missing: $TARBALL" ls -la "$(dirname "$TARBALL")" || true exit 1 fi npx @tauri-apps/cli signer sign "$TARBALL" cp "${TARBALL}.sig" "Psysonic_${ARCH}.app.tar.gz.sig" gh release upload "app-v${VERSION}" \ "Psysonic_${ARCH}.app.tar.gz.sig" \ --clobber generate-manifest: needs: [create-release, build-macos-windows] runs-on: ubuntu-24.04 permissions: contents: write steps: - uses: actions/checkout@v5 - name: generate latest.json env: VERSION: ${{ needs.create-release.outputs.package_version }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: node scripts/generate-update-manifest.js - name: upload latest.json to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION=${{ needs.create-release.outputs.package_version }} gh release upload "app-v${VERSION}" latest.json --clobber build-linux: needs: create-release permissions: contents: write runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - name: install dependencies run: | sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf \ libasound2-dev squashfs-tools cmake - name: setup node uses: actions/setup-node@v5 with: node-version: lts/* cache: 'npm' - name: install Rust stable uses: dtolnay/rust-toolchain@stable - name: cache cargo uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - name: install npm dependencies run: npm install - name: build env: VITE_LASTFM_API_KEY: ${{ secrets.VITE_LASTFM_API_KEY }} VITE_LASTFM_API_SECRET: ${{ secrets.VITE_LASTFM_API_SECRET }} APPIMAGE_EXTRACT_AND_RUN: 1 run: npm run tauri:build -- --bundles deb,rpm,appimage - name: upload Linux artifacts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION=${{ needs.create-release.outputs.package_version }} find src-tauri/target/release/bundle \ \( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \) \ | xargs gh release upload "app-v${VERSION}" --clobber # Verifies that `nix build .#psysonic` still works against the current source, # refreshes `nix/upstream-sources.json` (npmDepsHash) + `flake.lock` # (nixpkgs pin), and pushes the resulting store paths to the public Cachix # binary cache so end users can `nix profile install github:Psychotoxical/psysonic` # without having to compile locally. # # The refreshed lock/hash files are committed back to `main` when they change. verify-nix: needs: create-release runs-on: ubuntu-24.04 permissions: contents: write steps: - uses: actions/checkout@v5 with: # Full history so we can push the auto-commit back to the default branch. fetch-depth: 0 # Checkout main, not the tag — we want to push lock/hash refreshes to # the moving branch, not the immutable tag ref. ref: main - name: install Nix uses: DeterminateSystems/nix-installer-action@v15 # cachix-action with no signingKey = Cachix-managed signing (Cachix signs # server-side). The action watches the nix store during subsequent build # steps and uploads new paths automatically. - name: configure Cachix (managed signing) uses: cachix/cachix-action@v15 with: name: psysonic authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: compute npmDepsHash from package-lock.json id: npm-hash run: | set -euo pipefail HASH="$(nix run nixpkgs/nixos-unstable#prefetch-npm-deps -- package-lock.json)" echo "hash=$HASH" >> "$GITHUB_OUTPUT" echo "Computed npmDepsHash: $HASH" - name: write npmDepsHash into nix/upstream-sources.json run: | set -euo pipefail HASH='${{ steps.npm-hash.outputs.hash }}' jq --arg h "$HASH" '.npmDepsHash = $h' nix/upstream-sources.json > nix/upstream-sources.json.new mv nix/upstream-sources.json.new nix/upstream-sources.json cat nix/upstream-sources.json - name: refresh flake.lock (nixpkgs pin) run: nix flake update --accept-flake-config - name: verify nix build + push to Cachix run: | set -euo pipefail nix build .#psysonic --accept-flake-config --no-link --print-build-logs # The cachix-action daemon writes a post-build-hook into the user # nix.conf, but the Determinate Nix daemon that runs the builds reads # the system nix.conf — so the hook never fires and only a couple of # early prep paths get uploaded. Force an explicit closure push here; # cachix dedupes against anything already in the cache. nix path-info --recursive .#psysonic | cachix push psysonic - name: commit + push refreshed lock and hash (if changed) run: | set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add flake.lock nix/upstream-sources.json if git diff --cached --quiet; then echo "flake.lock / nix/upstream-sources.json unchanged — nothing to commit." exit 0 fi VERSION="${{ needs.create-release.outputs.package_version }}" git commit -m "chore(nix): refresh lock + npmDepsHash for v${VERSION}" git push origin HEAD:main