Parse resilient SACD images, decode DST, scan ISO releases into the local library, and keep explicit opens ephemeral. Ship decoder notices across distribution formats and align DSD album filtering and version selection. cx
354 lines
16 KiB
YAML
354 lines
16 KiB
YAML
# Linux aarch64 release — Qt frontend (crates/ workspace, package `qbz-qt`,
|
|
# binary `qbz`).
|
|
#
|
|
# Rewritten 2026-08-25 for the Slint -> Qt frontend switch. Pattern is still
|
|
# build once, package many — but the build is the shared build-qt-linux.yml
|
|
# (workflow_call) and it yields TWO payloads:
|
|
# * the bare binary -> qbz_${VERSION}_arm64.tar.gz (AUR / Gentoo / Flathub
|
|
# repack: ecosystems that provide Qt >= 6.8 themselves)
|
|
# * the AppDir (Qt bundled, self-contained) -> deb / rpm / AppImage, so
|
|
# distros without a usable Qt (Ubuntu 24.04 = Qt 6.4) keep working.
|
|
#
|
|
# CONTRACT (do not break): the release must keep publishing
|
|
# qbz_${VERSION}_arm64.tar.gz (dir qbz_${VERSION}_arm64/{qbz, qbzd, LICENSE, licenses/, qbz.desktop,
|
|
# com.blitzfc.qbz.metainfo.xml, qbzd.service,
|
|
# completions/, icons/hicolor/...})
|
|
# QBZ_${VERSION}_arm64.deb / QBZ-${VERSION}-1.aarch64.rpm / QBZ_${VERSION}_arm64.AppImage
|
|
# qbzd_${VERSION}_arm64.deb / qbzd-${VERSION}-1.aarch64.rpm / qbzd-${VERSION}-linux-arm64.tar.gz
|
|
# — release-aur.yml, release-gentoo.yml, qbz-apt, qbz-rpm, the updater
|
|
# manifest and the website consume these names.
|
|
#
|
|
# release-linux.yml is this file with the four ARCH_* values changed.
|
|
# Keep them structurally identical.
|
|
#
|
|
# Triggers: version tags (cut from main — enforced by the ancestry guard)
|
|
# + manual dispatch (guard skipped, for testing on any branch).
|
|
|
|
name: "Linux ARM64 (aarch64) Build"
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Optional release tag to build (e.g. v2.1.0). Empty = build HEAD with the Cargo.toml version, no Release publish."
|
|
required: false
|
|
type: string
|
|
push:
|
|
tags:
|
|
- "v*"
|
|
|
|
concurrency:
|
|
group: release-linux-aarch64-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
ARCH: aarch64 # build-qt-linux input + artifact suffix
|
|
DEB_ARCH: arm64 # QBZ_${V}_arm64.deb, qbz_${V}_arm64.tar.gz
|
|
RPM_ARCH: aarch64 # QBZ-${V}-1.aarch64.rpm
|
|
APPIMAGE_ARCH: aarch64 # QBZ_${V}_aarch64.AppImage
|
|
|
|
jobs:
|
|
# Release bookkeeping first so a bad tag fails in seconds, not after a build.
|
|
version:
|
|
runs-on: ubuntu-24.04-arm
|
|
outputs:
|
|
version: ${{ steps.release.outputs.version }}
|
|
prerelease: ${{ steps.release.outputs.prerelease }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.tag || github.ref }}
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve release version
|
|
id: release
|
|
run: |
|
|
VERSION=""
|
|
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
|
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
elif [[ -n "${{ inputs.tag }}" ]]; then
|
|
VERSION="${{ inputs.tag }}"
|
|
VERSION="${VERSION#v}"
|
|
else
|
|
# Manual dispatch without a tag: test build at the Cargo.toml version.
|
|
VERSION="$(grep -m1 '^version = ' crates/Cargo.toml | sed 's/version = \"\(.*\)\"/\1/')"
|
|
fi
|
|
PRERELEASE=false
|
|
if [[ "$VERSION" =~ -(rc|beta|alpha|pre) ]]; then
|
|
PRERELEASE=true
|
|
fi
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
|
|
echo "Building version $VERSION (prerelease=$PRERELEASE)"
|
|
|
|
# Release tags must be cut from main. A tag on any other commit fires
|
|
# this workflow too (tags are not branch-scoped in Actions), so verify
|
|
# ancestry instead of trusting convention. Manual dispatch skips this.
|
|
- name: Verify tag is on main
|
|
if: startsWith(github.ref, 'refs/tags/')
|
|
run: |
|
|
git fetch origin main
|
|
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
|
echo "::error::Tag ${GITHUB_REF#refs/tags/} points to a commit that is NOT on main. Releases are cut from main only."
|
|
exit 1
|
|
fi
|
|
|
|
# The binary embeds the workspace version (About, diagnostics); a
|
|
# mismatched tag would ship a binary that reports the wrong version.
|
|
- name: Assert Cargo.toml version matches tag
|
|
if: startsWith(github.ref, 'refs/tags/') || inputs.tag != ''
|
|
env:
|
|
VERSION: ${{ steps.release.outputs.version }}
|
|
run: |
|
|
CARGO_VERSION="$(grep -m1 '^version = ' crates/Cargo.toml | sed 's/version = \"\(.*\)\"/\1/')"
|
|
if [[ "$CARGO_VERSION" != "$VERSION" ]]; then
|
|
echo "::error::crates/Cargo.toml workspace version ($CARGO_VERSION) != tag version ($VERSION). Bump it before tagging (see RELEASE_CHECKLIST)."
|
|
exit 1
|
|
fi
|
|
|
|
# The media-widget icon invariant (DesktopEntry == installed .desktop
|
|
# basename, Icon= installed) — regressed more than once, so it is a
|
|
# gate at the recipe level here and on the built packages below.
|
|
- name: Gate — desktop entry / icon invariant (recipes)
|
|
run: scripts/packaging/desktop_entry_check.sh
|
|
|
|
# No workflow may build the frozen Slint binary: same bin name, same
|
|
# target dir, last build wins the file — a smoke against "the wrong
|
|
# qbz" is silent.
|
|
- name: Gate — no workflow builds `-p qbz`
|
|
run: |
|
|
set -euo pipefail
|
|
hits=$(grep -rnE 'cargo (build|check|test|run|install).* -p qbz([^-a-z]|$)' .github/workflows/ || true)
|
|
[ -z "$hits" ] || { echo "::error::a workflow still builds the Slint crate:"; echo "$hits"; exit 1; }
|
|
|
|
build:
|
|
needs: version
|
|
uses: ./.github/workflows/build-qt-linux.yml
|
|
with:
|
|
arch: aarch64
|
|
source_ref: ${{ inputs.tag || github.ref }}
|
|
|
|
# qbzd is minutes-class and Slint-free. Runs on ubuntu-22.04 so the shipped
|
|
# binary's glibc floor stays <= Pi OS bookworm's 2.36 (the honest-Pi-story
|
|
# gate); the objdump step verifies that empirically.
|
|
build-qbzd:
|
|
runs-on: ubuntu-22.04-arm
|
|
timeout-minutes: 30
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.tag || github.ref }}
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
- uses: Swatinem/rust-cache@v2
|
|
with:
|
|
workspaces: crates -> target
|
|
shared-key: qbzd-linux-aarch64
|
|
cache-on-failure: true
|
|
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/pre-release' }}
|
|
- name: system deps
|
|
run: sudo apt-get update && sudo apt-get install -y libasound2-dev libjack-jackd2-dev libdbus-1-dev libssl-dev
|
|
- name: slint-free dep-graph gate
|
|
run: |
|
|
set -euo pipefail
|
|
hits=$(cargo tree --manifest-path crates/Cargo.toml -p qbzd -e normal \
|
|
| grep -E '\b(slint|qbz-ui|qbz-slint-common|qbz-dac-wizard) v' || true)
|
|
[ -z "$hits" ] || { echo "::error::qbzd graph resolves Slint:"; echo "$hits"; exit 1; }
|
|
- name: build
|
|
run: cargo build --release --manifest-path crates/Cargo.toml -p qbzd
|
|
- name: glibc floor gate (objdump — the empirical-verification rule)
|
|
run: |
|
|
set -euo pipefail
|
|
floor=$(objdump -T crates/target/release/qbzd \
|
|
| grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -Vu | tail -1)
|
|
echo "glibc floor: $floor"
|
|
highest=$(printf '%s\n' "$floor" "GLIBC_2.35" | sort -V | tail -1)
|
|
[ "$highest" = "GLIBC_2.35" ] || { echo "::error::floor $floor exceeds 2.35"; exit 1; }
|
|
- uses: actions/upload-artifact@v7
|
|
with:
|
|
name: qbzd-linux-aarch64
|
|
path: crates/target/release/qbzd
|
|
retention-days: 7
|
|
|
|
package:
|
|
needs: [version, build, build-qbzd]
|
|
runs-on: ubuntu-24.04-arm
|
|
permissions:
|
|
contents: write
|
|
env:
|
|
VERSION: ${{ needs.version.outputs.version }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.tag || github.ref }}
|
|
|
|
- name: Download bare binary
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: qbz-linux-${{ env.ARCH }}
|
|
|
|
- name: Download AppDir
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: qbz-appdir-${{ env.ARCH }}
|
|
|
|
- name: Download qbzd binary
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: qbzd-linux-${{ env.ARCH }}
|
|
|
|
- name: Unpack AppDir
|
|
run: |
|
|
tar -xf appdir.tar
|
|
chmod +x qbz qbzd
|
|
ls AppDir; ls AppDir/usr/plugins/platforms
|
|
|
|
- name: Install nfpm
|
|
run: |
|
|
echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | \
|
|
sudo tee /etc/apt/sources.list.d/goreleaser.list
|
|
sudo apt-get update
|
|
sudo apt-get install -y nfpm
|
|
nfpm --version
|
|
|
|
# deb/rpm carry the AppDir tree under /opt/qbz (bin, lib, plugins, qml,
|
|
# qt.conf) and a /usr/bin/qbz symlink. RPATH ($ORIGIN/../lib, patched by
|
|
# linuxdeploy) resolves through /proc/self/exe, so the symlink needs no
|
|
# wrapper — and a wrapper exporting LD_LIBRARY_PATH would leak our Qt
|
|
# into every process qbz spawns (the login browser, xdg-open).
|
|
- name: Build deb + rpm (nfpm)
|
|
env:
|
|
QBZ_VERSION: ${{ env.VERSION }}
|
|
QBZ_ARCH: ${{ env.DEB_ARCH }}
|
|
run: |
|
|
mkdir -p dist
|
|
./qbzd completions bash > qbzd.bash
|
|
./qbzd completions zsh > qbzd.zsh
|
|
./qbzd completions fish > qbzd.fish
|
|
nfpm package -f packaging/nfpm/nfpm.yaml -p deb \
|
|
-t "dist/QBZ_${VERSION}_${DEB_ARCH}.deb"
|
|
nfpm package -f packaging/nfpm/nfpm.yaml -p rpm \
|
|
-t "dist/QBZ-${VERSION}-1.${RPM_ARCH}.rpm"
|
|
# Standalone qbzd packages (daemon-only), same staged ./qbzd.
|
|
nfpm package -f packaging/nfpm/nfpm-qbzd.yaml -p deb \
|
|
-t "dist/qbzd_${VERSION}_${DEB_ARCH}.deb"
|
|
nfpm package -f packaging/nfpm/nfpm-qbzd.yaml -p rpm \
|
|
-t "dist/qbzd-${VERSION}-1.${RPM_ARCH}.rpm"
|
|
ls -la dist
|
|
|
|
# The same invariant on the ARTIFACTS: the deb/rpm must install
|
|
# /usr/share/applications/<DesktopEntry>.desktop and its Icon=.
|
|
- name: Gate — desktop entry / icon invariant (packages)
|
|
run: |
|
|
sudo apt-get install -y --no-install-recommends rpm
|
|
scripts/packaging/desktop_entry_check.sh \
|
|
--deb "dist/QBZ_${VERSION}_${DEB_ARCH}.deb" \
|
|
--rpm "dist/QBZ-${VERSION}-1.${RPM_ARCH}.rpm" \
|
|
--appdir AppDir
|
|
|
|
- name: Build AppImage
|
|
run: |
|
|
curl -fsSL -o appimagetool \
|
|
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${ARCH}.AppImage"
|
|
chmod +x appimagetool
|
|
ARCH="${ARCH}" ./appimagetool --appimage-extract-and-run AppDir \
|
|
"dist/QBZ_${VERSION}_${APPIMAGE_ARCH}.AppImage"
|
|
ls -la dist
|
|
|
|
# Tauri-updater bridge: v1.2.x AppImage installs poll latest.json and
|
|
# verify artifacts with the Tauri minisign pubkey baked into the old
|
|
# binaries. Signing with the SAME private key lets them auto-update
|
|
# straight into this AppImage. release-updater-manifest.yml collects
|
|
# the .sig. The app itself has no auto-updater.
|
|
- name: Sign AppImage (Tauri-updater bridge)
|
|
if: startsWith(github.ref, 'refs/tags/')
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
run: |
|
|
npx --yes @tauri-apps/cli@^2 signer sign \
|
|
--private-key "$TAURI_SIGNING_PRIVATE_KEY" \
|
|
--password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
|
|
"dist/QBZ_${VERSION}_${APPIMAGE_ARCH}.AppImage"
|
|
ls -la dist
|
|
|
|
# The BARE binary. Its consumers (AUR, Gentoo, Flathub) provide Qt.
|
|
- name: Build AUR/Gentoo tarball
|
|
run: |
|
|
# CONTRACT: release-aur.yml, the Gentoo binary ebuild, qbz-apt and
|
|
# the website hard-code this name and internal layout.
|
|
TARBALL_DIR="qbz_${VERSION}_${DEB_ARCH}"
|
|
mkdir -p "tarball-staging/${TARBALL_DIR}/icons/hicolor"/{32x32,48x48,64x64,128x128,256x256,512x512}/apps
|
|
cp qbz "tarball-staging/${TARBALL_DIR}/qbz"
|
|
chmod +x "tarball-staging/${TARBALL_DIR}/qbz"
|
|
cp packaging/icons/32x32.png "tarball-staging/${TARBALL_DIR}/icons/hicolor/32x32/apps/qbz.png"
|
|
cp packaging/icons/48x48.png "tarball-staging/${TARBALL_DIR}/icons/hicolor/48x48/apps/qbz.png"
|
|
cp packaging/icons/64x64.png "tarball-staging/${TARBALL_DIR}/icons/hicolor/64x64/apps/qbz.png"
|
|
cp packaging/icons/128x128.png "tarball-staging/${TARBALL_DIR}/icons/hicolor/128x128/apps/qbz.png"
|
|
cp packaging/icons/256x256.png "tarball-staging/${TARBALL_DIR}/icons/hicolor/256x256/apps/qbz.png"
|
|
cp packaging/icons/512x512.png "tarball-staging/${TARBALL_DIR}/icons/hicolor/512x512/apps/qbz.png"
|
|
cp packaging/linux/qbz.desktop "tarball-staging/${TARBALL_DIR}/qbz.desktop"
|
|
cp packaging/flatpak/com.blitzfc.qbz.metainfo.xml "tarball-staging/${TARBALL_DIR}/com.blitzfc.qbz.metainfo.xml"
|
|
cp LICENSE "tarball-staging/${TARBALL_DIR}/LICENSE"
|
|
cp -r licenses "tarball-staging/${TARBALL_DIR}/licenses"
|
|
cp qbzd "tarball-staging/${TARBALL_DIR}/qbzd"
|
|
chmod +x "tarball-staging/${TARBALL_DIR}/qbzd"
|
|
cp crates/qbzd/service/qbzd.service "tarball-staging/${TARBALL_DIR}/qbzd.service"
|
|
mkdir -p "tarball-staging/${TARBALL_DIR}/completions"
|
|
cp qbzd.bash "tarball-staging/${TARBALL_DIR}/completions/qbzd.bash"
|
|
cp qbzd.zsh "tarball-staging/${TARBALL_DIR}/completions/qbzd.zsh"
|
|
cp qbzd.fish "tarball-staging/${TARBALL_DIR}/completions/qbzd.fish"
|
|
tar -czvf "dist/qbz_${VERSION}_${DEB_ARCH}.tar.gz" -C tarball-staging "${TARBALL_DIR}"
|
|
ls -la dist
|
|
|
|
- name: qbzd-tarball
|
|
run: |
|
|
QBZD_DIR="qbzd-${VERSION}-linux-${DEB_ARCH}"
|
|
mkdir -p "qbzd-tarball-staging/${QBZD_DIR}/completions"
|
|
cp qbzd "qbzd-tarball-staging/${QBZD_DIR}/qbzd"
|
|
chmod +x "qbzd-tarball-staging/${QBZD_DIR}/qbzd"
|
|
cp crates/qbzd/service/qbzd.service "qbzd-tarball-staging/${QBZD_DIR}/qbzd.service"
|
|
cp qbzd.bash "qbzd-tarball-staging/${QBZD_DIR}/completions/qbzd.bash"
|
|
cp qbzd.zsh "qbzd-tarball-staging/${QBZD_DIR}/completions/qbzd.zsh"
|
|
cp qbzd.fish "qbzd-tarball-staging/${QBZD_DIR}/completions/qbzd.fish"
|
|
cp packaging/linux/qbzd-standalone-README.md "qbzd-tarball-staging/${QBZD_DIR}/README.md"
|
|
cp LICENSE "qbzd-tarball-staging/${QBZD_DIR}/LICENSE"
|
|
cp -r licenses "qbzd-tarball-staging/${QBZD_DIR}/licenses"
|
|
tar -czvf "dist/qbzd-${VERSION}-linux-${DEB_ARCH}.tar.gz" -C qbzd-tarball-staging "${QBZD_DIR}"
|
|
ls -la dist
|
|
|
|
- name: Upload artifacts
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: qbz-linux-packages-${{ env.ARCH }}
|
|
path: dist/*
|
|
retention-days: ${{ startsWith(github.ref, 'refs/tags/v') && 90 || 7 }}
|
|
|
|
- name: Publish GitHub Release Assets
|
|
if: startsWith(github.ref, 'refs/tags/')
|
|
uses: softprops/action-gh-release@v3
|
|
with:
|
|
files: dist/*
|
|
prerelease: ${{ needs.version.outputs.prerelease }}
|
|
make_latest: ${{ needs.version.outputs.prerelease == 'true' && 'false' || 'true' }}
|
|
|
|
- name: Trigger APT repo update
|
|
if: startsWith(github.ref, 'refs/tags/')
|
|
uses: peter-evans/repository-dispatch@v4
|
|
with:
|
|
token: ${{ secrets.APT_REPO_TOKEN }}
|
|
repository: vicrodh/qbz-apt
|
|
event-type: new-release
|
|
client-payload: '{"tag": "${{ github.ref_name }}"}'
|
|
|
|
# Our own signed RPM repo. This is intentionally required: a bad token
|
|
# must not leave the stable RPM channel stale without failing release CI.
|
|
- name: Trigger RPM repo update
|
|
if: startsWith(github.ref, 'refs/tags/')
|
|
uses: peter-evans/repository-dispatch@v4
|
|
with:
|
|
token: ${{ secrets.APT_REPO_TOKEN }}
|
|
repository: vicrodh/qbz-rpm
|
|
event-type: new-release
|
|
client-payload: '{"tag": "${{ github.ref_name }}"}'
|