Merge pull request #24 from chrisuthe/chrisuthe/task/centralize-the-a… #66
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 | |
| # --------------------------------------------------------------------------- | |
| # The icon set under packaging/icons/ is generated from one SVG master and committed, | |
| # because a plain `dotnet build` needs the .ico and no build machine should have to own | |
| # a rasterizer. This checks the two cannot drift. | |
| # | |
| # It hashes the INPUTS rather than regenerating and diffing the outputs. Regenerating | |
| # would be checking that two builds of librsvg agree on edge antialiasing, which they | |
| # do not have to; the job would eventually fail whenever the runner image moved, for a | |
| # reason no contributor could act on. What actually goes wrong is a master edited | |
| # without the set being regenerated, and that is exactly what this catches. | |
| # --------------------------------------------------------------------------- | |
| icons: | |
| name: Icon set is current | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Check packaging/icons/.source-hash | |
| run: | | |
| set -euo pipefail | |
| if ! sha256sum -c packaging/icons/.source-hash; then | |
| echo "::error::An icon master or the generator changed without the icon set being regenerated" | |
| echo "Run ./scripts/generate-icons.sh and commit everything under packaging/icons/." | |
| exit 1 | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # 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 | |
| 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/ | |
| # Every size the theme carries, not just 256x256: a panel and a switcher ask for | |
| # different ones, and a desktop given only 256 downscales it itself. | |
| for dir in packaging/icons/hicolor/*/apps; do | |
| size_dir="$(basename "$(dirname "$dir")")" | |
| install -Dm644 "$dir"/io.sendspin.client.* \ | |
| -t "AppDir/usr/share/icons/hicolor/$size_dir/apps" | |
| done | |
| # appimagetool reads the Icon= key's file from the AppDir root too. | |
| cp packaging/icons/hicolor/256x256/apps/io.sendspin.client.png AppDir/ | |
| 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 | |
| # The icon is the third thing that is silently wrong rather than loudly wrong: with | |
| # either half of this missing the bundle just shows the generic application icon in | |
| # Finder, the Dock and alt-tab, and nothing reports it. | |
| icon="$(/usr/libexec/PlistBuddy -c "Print :CFBundleIconFile" "$plist" 2>/dev/null || true)" | |
| if [ -z "$icon" ]; then | |
| echo "::error::Info.plist is missing CFBundleIconFile; the bundle would show the generic icon" | |
| exit 1 | |
| fi | |
| if [ ! -f "$app/Contents/Resources/${icon%.icns}.icns" ]; then | |
| echo "::error::CFBundleIconFile names ${icon}, but Contents/Resources/${icon%.icns}.icns is missing" | |
| ls -la "$app/Contents/Resources" || true | |
| exit 1 | |
| fi | |
| - 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 |