Skip to content

Finish the reskin: tray parity, friendly picker labels, guarded macOS font, docs sweep #57

Finish the reskin: tray parity, friendly picker labels, guarded macOS font, docs sweep

Finish the reskin: tray parity, friendly picker labels, guarded macOS font, docs sweep #57

Workflow file for this run

# Sendspin Player CI
#
# Every action is pinned to a full commit SHA. A tag is a mutable reference the upstream owner can
# repoint at any time, so `@v4` is an unreviewed dependency with write access to this build.
name: Build
on:
push:
branches: [master, main]
pull_request:
branches: [master, main]
workflow_dispatch:
permissions:
contents: read
env:
DOTNET_VERSION: '10.0.x'
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
# ---------------------------------------------------------------------------
# Tests run once, on Linux: Sendspin.Tests covers Sendspin.Core and
# Sendspin.Platform.Shared, which are plain net10.0 and platform-neutral by
# design, so running it three times would exercise the same code three times.
# Sendspin.Ui.Tests drives the Linux head under Avalonia's headless platform,
# which needs no display server.
#
# Both projects are named. `dotnet test` on the solution would pull in the
# macOS head, which cannot restore without the macos workload.
#
# There is deliberately no `continue-on-error` here. A gate that cannot fail is
# not a gate, and this repo previously ran `dotnet test` with it set on both
# platforms, which is how having zero tests went unnoticed.
# ---------------------------------------------------------------------------
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Test
run: |
set -euo pipefail
for project in src/Sendspin.Tests/Sendspin.Tests.csproj \
src/Sendspin.Ui.Tests/Sendspin.Ui.Tests.csproj; do
dotnet test "$project" \
--configuration Release \
--logger "trx;LogFileName=$(basename "$project" .csproj).trx" \
--results-directory ./test-results
done
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results
path: ./test-results
retention-days: 14
# ---------------------------------------------------------------------------
# Dependency audit. NU1903 already fails the build through TreatWarningsAsErrors;
# this reports the detail rather than leaving a maintainer to decode a restore
# error, and covers transitive packages explicitly.
# ---------------------------------------------------------------------------
audit:
name: Dependency audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Report vulnerable packages
run: |
set -euo pipefail
dotnet restore src/Sendspin.Tests/Sendspin.Tests.csproj
dotnet list src/Sendspin.Tests/Sendspin.Tests.csproj package \
--vulnerable --include-transitive 2>&1 | tee audit.txt
if grep -qi 'has the following vulnerable packages' audit.txt; then
echo "::error::Vulnerable packages reported above"
exit 1
fi
# ---------------------------------------------------------------------------
# Linux
# ---------------------------------------------------------------------------
build-linux:
name: Build Linux (${{ matrix.rid }})
runs-on: ubuntu-latest
needs: test
strategy:
fail-fast: false
matrix:
rid: [linux-x64, linux-arm64]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Publish
run: >
dotnet publish src/Sendspin.Player/Sendspin.Player.csproj
--configuration Release
--framework net10.0
--runtime ${{ matrix.rid }}
--self-contained
--output ./publish/${{ matrix.rid }}
- name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: sendspin-player-${{ matrix.rid }}
path: ./publish/${{ matrix.rid }}
retention-days: 14
package-flatpak:
name: Package Flatpak
runs-on: ubuntu-latest
needs: build-linux
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: sendspin-player-linux-x64
path: ./publish/linux-x64
- name: Install Flatpak tools
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder
sudo flatpak remote-add --if-not-exists flathub \
https://flathub.org/repo/flathub.flatpakrepo
sudo flatpak install -y flathub \
org.freedesktop.Platform//25.08 org.freedesktop.Sdk//25.08
# The manifest is the one committed under packaging/flatpak/. It is NOT
# regenerated inline here: this workflow used to carry its own copy, which
# silently won over the committed one, so the two drifted and only the
# workflow's version was ever built or reviewed.
- name: Build Flatpak
run: |
set -euo pipefail
flatpak-builder --user --force-clean --repo=repo build-dir \
packaging/flatpak/io.sendspin.client.yml
flatpak build-bundle repo Sendspin-Player.flatpak io.sendspin.client
- name: Upload Flatpak
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: sendspin-player-flatpak
path: Sendspin-Player.flatpak
retention-days: 30
package-appimage:
name: Package AppImage
runs-on: ubuntu-latest
needs: build-linux
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
env:
# A pinned release verified by checksum, not `continuous` fetched and executed
# unverified — that hands whoever controls the URL arbitrary code execution
# inside this workflow. The digest is the one GitHub publishes for the
# 1.9.1 x86_64 asset; on an intentional upgrade both values change together.
APPIMAGETOOL_VERSION: '1.9.1'
APPIMAGETOOL_SHA256: 'ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: sendspin-player-linux-x64
path: ./publish/linux-x64
- name: Fetch and verify appimagetool
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y libfuse2t64 || sudo apt-get install -y libfuse2
url="https://github.com/AppImage/appimagetool/releases/download/${APPIMAGETOOL_VERSION}/appimagetool-x86_64.AppImage"
curl -fsSL "$url" -o appimagetool
actual="$(sha256sum appimagetool | cut -d' ' -f1)"
if [ "$actual" != "$APPIMAGETOOL_SHA256" ]; then
echo "::error::appimagetool checksum mismatch"
echo " expected: ${APPIMAGETOOL_SHA256}"
echo " actual: ${actual}"
echo "If this is an intentional appimagetool upgrade, verify the release and"
echo "update APPIMAGETOOL_SHA256 in this workflow in the same commit."
exit 1
fi
chmod +x appimagetool
- name: Create AppImage
run: |
set -euo pipefail
mkdir -p AppDir/usr/bin \
AppDir/usr/share/applications \
AppDir/usr/share/icons/hicolor/256x256/apps
cp -r ./publish/linux-x64/* AppDir/usr/bin/
chmod +x AppDir/usr/bin/Sendspin.Player
# The checked-in AppRun, not a third inline copy of one.
cp packaging/appimage/AppRun AppDir/AppRun
chmod +x AppDir/AppRun
cp packaging/io.sendspin.client.desktop AppDir/
cp packaging/io.sendspin.client.desktop AppDir/usr/share/applications/
cp packaging/icons/io.sendspin.client.png AppDir/
cp packaging/icons/io.sendspin.client.png \
AppDir/usr/share/icons/hicolor/256x256/apps/
ARCH=x86_64 ./appimagetool AppDir Sendspin-Player-x86_64.AppImage
- name: Upload AppImage
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: sendspin-player-appimage
path: Sendspin-Player-x86_64.AppImage
retention-days: 30
# ---------------------------------------------------------------------------
# Windows — framework-dependent only.
#
# Self-contained buys less than it looks like it does here: the WindowsAppRuntime
# and the VC++ redistributable are installer prerequisites either way, and a
# self-contained unpackaged publish is exactly the configuration in which
# AppNotificationManager.Register() throws (WindowsAppSDK#6071). See
# docs/COMPLIANCE.md.
# ---------------------------------------------------------------------------
build-windows:
name: Build Windows (${{ matrix.rid }})
runs-on: windows-latest
needs: test
strategy:
fail-fast: false
matrix:
rid: [win-x64, win-arm64]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Publish
run: >
dotnet publish src/Sendspin.Player/Sendspin.Player.csproj
--configuration Release
--framework net10.0-windows10.0.19041.0
--runtime ${{ matrix.rid }}
--no-self-contained
--output ./publish/${{ matrix.rid }}
- name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: sendspin-player-${{ matrix.rid }}
path: ./publish/${{ matrix.rid }}
retention-days: 14
# ---------------------------------------------------------------------------
# macOS — arm64 only. .NET cannot emit a universal binary and the bundled
# libopenal.dylib is thin arm64 already.
#
# Signing and notarization are NOT done here, and ad-hoc signing is deliberately
# not used as a stand-in: TCC keys the local-network grant to the code signature,
# so an ad-hoc hash that changes on every rebuild produces a grant that silently
# stops applying — which makes discovery testing give false results rather than
# merely unsigned ones. The unsigned bundle is published so the build is
# verifiable. docs/COMPLIANCE.md records what stays blocked on a Developer ID
# certificate.
# ---------------------------------------------------------------------------
build-macos:
name: Build macOS (osx-arm64)
runs-on: macos-latest
needs: test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install the macOS workload
run: dotnet workload install macos
- name: Publish
run: >
dotnet publish src/Sendspin.Player/Sendspin.Player.csproj
--configuration Release
--framework net10.0-macos
--runtime osx-arm64
--output ./publish/osx-arm64
# The macOS SDK's publish artifact is the .pkg installer, so CreatePackage=false
# (see Sendspin.Player.csproj) leaves --output empty rather than filling it: the
# .app bundle is built into the project's own output directory and stays there.
# Stage it into the publish tree, which is what the steps below and the uploaded
# artifact expect.
- name: Stage the bundle
run: |
set -euo pipefail
bin=src/Sendspin.Player/bin/Release/net10.0-macos
built="$(find "$bin" -maxdepth 2 -name '*.app' -print -quit)"
if [ -z "$built" ]; then
echo "::error::No .app bundle was built"
echo "Build output was:"
find "$bin" -maxdepth 2
exit 1
fi
echo "Built bundle: $built"
mkdir -p ./publish/osx-arm64
cp -R "$built" ./publish/osx-arm64/
# Three things that are silently wrong rather than loudly wrong, so they are
# checked rather than assumed: a symlinked executable breaks
# UNUserNotificationCenter, and a missing local-network key means discovery
# fails on macOS 15+ with no diagnostic.
- name: Check the bundle
run: |
set -euo pipefail
app="$(find ./publish/osx-arm64 -maxdepth 2 -name '*.app' -print -quit)"
if [ -z "$app" ]; then
echo "::error::No .app bundle was produced"
echo "Publish output was:"
find ./publish/osx-arm64 -maxdepth 2
exit 1
fi
echo "Bundle: $app"
binary="$app/Contents/MacOS/Sendspin.Player"
if [ -L "$binary" ]; then
echo "::error::${binary} is a symlink; UNUserNotificationCenter requires a real file"
exit 1
fi
if [ ! -f "$binary" ]; then
echo "::error::${binary} is missing"
exit 1
fi
plist="$app/Contents/Info.plist"
for key in NSLocalNetworkUsageDescription NSBonjourServices; do
if ! /usr/libexec/PlistBuddy -c "Print :${key}" "$plist" >/dev/null 2>&1; then
echo "::error::Info.plist is missing ${key}; macOS 15+ gates all Bonjour and *.local resolution on it"
exit 1
fi
done
- name: Create dmg
run: |
set -euo pipefail
app="$(find ./publish/osx-arm64 -maxdepth 2 -name '*.app' -print -quit)"
mkdir -p dmg-root
cp -R "$app" dmg-root/
hdiutil create -volname "Sendspin Player" -srcfolder dmg-root \
-ov -format UDZO Sendspin-Player-arm64.dmg
- name: Upload artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: sendspin-player-osx-arm64
path: |
./publish/osx-arm64
Sendspin-Player-arm64.dmg
retention-days: 14