a Jellyfin & Subsonic client for the terminal — powered by mpv, Chromecast and UPnP MediaRenderer
mpv
chromecast
mpris
navidrome
jellyfin
upnp
tui
1name: Release
2
3on:
4 push:
5 tags:
6 - "v*"
7 workflow_dispatch:
8 inputs:
9 tag:
10 description: "Tag to release (e.g. v0.1.0)"
11 required: true
12 type: string
13
14permissions:
15 contents: write
16
17jobs:
18 build:
19 name: ${{ matrix.label }}
20 runs-on: ${{ matrix.os }}
21 strategy:
22 fail-fast: false
23 matrix:
24 include:
25 - label: macos-amd64
26 os: macos-latest
27 target: x86_64-apple-darwin
28 - label: macos-aarch64
29 os: macos-latest
30 target: aarch64-apple-darwin
31 - label: linux-amd64
32 os: ubuntu-latest
33 target: x86_64-unknown-linux-gnu
34 # Native ARM Linux runner — no `cross`, no QEMU. Faster and
35 # matches production glibc more closely.
36 - label: linux-aarch64
37 os: ubuntu-24.04-arm
38 target: aarch64-unknown-linux-gnu
39
40 steps:
41 - name: Checkout
42 uses: actions/checkout@v4
43 with:
44 # On manual dispatch, build the tag's code rather than the branch
45 # the workflow was dispatched from.
46 ref: ${{ inputs.tag || github.ref }}
47
48 - name: Install Rust toolchain
49 uses: dtolnay/rust-toolchain@stable
50 with:
51 targets: ${{ matrix.target }}
52
53 - name: Cache cargo
54 uses: Swatinem/rust-cache@v2
55 with:
56 key: ${{ matrix.target }}
57
58 # cpal links against ALSA on Linux (macOS uses CoreAudio, no dev pkg needed).
59 - name: Install ALSA dev headers
60 if: startsWith(matrix.os, 'ubuntu')
61 run: |
62 sudo apt-get update
63 sudo apt-get install -y libasound2-dev pkg-config
64
65 - name: Build
66 run: cargo build --release --locked --target ${{ matrix.target }} --bin fin
67
68 - name: Package tarball
69 shell: bash
70 run: |
71 set -euo pipefail
72 if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
73 version="${{ inputs.tag }}"
74 else
75 version="${GITHUB_REF_NAME}"
76 fi
77 name="fin-${version}-${{ matrix.label }}"
78 src="target/${{ matrix.target }}/release/fin"
79 mkdir -p dist/staging
80 cp "$src" dist/staging/fin
81 cp README.md LICENSE dist/staging/
82 tar -C dist/staging -czf "dist/${name}.tar.gz" .
83 (cd dist && shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256")
84 ls -lh dist
85
86 - name: Install dpkg-deb and rpmbuild
87 if: startsWith(matrix.os, 'ubuntu')
88 run: |
89 sudo apt-get update
90 sudo apt-get install -y dpkg-dev rpm
91
92 - name: Build .deb
93 if: startsWith(matrix.os, 'ubuntu')
94 shell: bash
95 run: |
96 set -euo pipefail
97 if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
98 version="${{ inputs.tag }}"
99 else
100 version="${GITHUB_REF_NAME}"
101 fi
102 version_no_v="${version#v}"
103 case "${{ matrix.label }}" in
104 linux-amd64) deb_arch=amd64 ;;
105 linux-aarch64) deb_arch=arm64 ;;
106 esac
107 pkg_root="dist/debian/${deb_arch}"
108 mkdir -p "$pkg_root/usr/local/bin"
109 install -m 0755 "target/${{ matrix.target }}/release/fin" "$pkg_root/usr/local/bin/fin"
110 sed -i "s/^Version: .*/Version: ${version_no_v}/" "$pkg_root/DEBIAN/control"
111 dpkg-deb --build --root-owner-group "$pkg_root" "dist/fin_${version_no_v}_${deb_arch}.deb"
112
113 - name: Build .rpm
114 if: matrix.label == 'linux-amd64'
115 shell: bash
116 run: |
117 set -euo pipefail
118 if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
119 version="${{ inputs.tag }}"
120 else
121 version="${GITHUB_REF_NAME}"
122 fi
123 version_no_v="${version#v}"
124 rpm_arch=amd64
125 rpm_target=x86_64
126 src_root="dist/rpm/${rpm_arch}"
127 mkdir -p "${src_root}/usr/local/bin"
128 install -m 0755 "target/${{ matrix.target }}/release/fin" "${src_root}/usr/local/bin/fin"
129 sed -i "s/^Version: .*/Version: ${version_no_v}/" "${src_root}/fin.spec"
130
131 rpm_top="$(pwd)/dist/rpmbuild"
132 mkdir -p "${rpm_top}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
133 cp -r "${src_root}" "${rpm_top}/SOURCES/${rpm_arch}"
134 rpmbuild --define "_topdir ${rpm_top}" \
135 --define "_sourcedir ${rpm_top}/SOURCES" \
136 --target "${rpm_target}" \
137 -bb "${src_root}/fin.spec"
138 find "${rpm_top}/RPMS" -name '*.rpm' -exec mv {} dist/ \;
139
140 - name: Checksum packages
141 if: startsWith(matrix.os, 'ubuntu')
142 shell: bash
143 run: |
144 set -euo pipefail
145 cd dist
146 for f in *.deb *.rpm; do
147 [ -e "$f" ] || continue
148 shasum -a 256 "$f" > "$f.sha256"
149 done
150 ls -lh
151
152 - name: Upload artifact
153 uses: actions/upload-artifact@v4
154 with:
155 name: ${{ matrix.label }}
156 path: |
157 dist/*.tar.gz*
158 dist/*.deb*
159 dist/*.rpm*
160 if-no-files-found: error
161
162 build-bsd:
163 name: ${{ matrix.label }}
164 runs-on: ubuntu-latest
165 strategy:
166 fail-fast: false
167 matrix:
168 include:
169 # cpal uses its ALSA backend on FreeBSD/NetBSD, so those need
170 # alsa-lib + pkgconf. On OpenBSD cpal falls back to the Null host
171 # (no sound-card output at runtime; Chromecast/DLNA still work),
172 # so no audio packages there. cmake is for aws-lc-sys (rustls
173 # crypto provider), which uses its CMake builder and the bundled
174 # "universal" bindings on the BSDs — no bindgen needed. mpv is a
175 # runtime-only external process, nothing to install at build time.
176 #
177 # aarch64 BSD builds run under emulation and take hours, so they
178 # live in release-bsd-aarch64.yml and attach their tarballs to the
179 # release after it's published — they never block this workflow.
180 - label: freebsd-amd64
181 os: freebsd
182 os_version: "14.3"
183 arch: x86-64
184 install_deps: sudo env ASSUME_ALWAYS_YES=YES pkg install -y rust alsa-lib pkgconf cmake
185 - label: netbsd-amd64
186 os: netbsd
187 os_version: "10.1"
188 arch: x86-64
189 install_deps: sudo pkgin -y update && sudo pkgin -y install rust alsa-lib pkgconf cmake
190 - label: openbsd-amd64
191 os: openbsd
192 os_version: "7.9"
193 arch: x86-64
194 install_deps: sudo pkg_add rust cmake
195
196 steps:
197 - name: Checkout
198 uses: actions/checkout@v4
199 with:
200 # On manual dispatch, build the tag's code rather than the branch
201 # the workflow was dispatched from.
202 ref: ${{ inputs.tag || github.ref }}
203
204 - name: Start ${{ matrix.os }} ${{ matrix.arch }} VM
205 uses: cross-platform-actions/action@v1.3.0
206 with:
207 operating_system: ${{ matrix.os }}
208 version: ${{ matrix.os_version }}
209 architecture: ${{ matrix.arch }}
210 shell: bash
211 memory: 8G
212 cpu_count: 4
213
214 - name: Build in VM
215 shell: cpa.sh {0}
216 run: |
217 set -euo pipefail
218 ${{ matrix.install_deps }}
219 case "$(uname -s)" in
220 NetBSD)
221 # pkg-config crate looks for `pkg-config`; pkgsrc ships `pkgconf`.
222 export PKG_CONFIG=/usr/pkg/bin/pkgconf
223 # NetBSD's runtime linker doesn't search /usr/pkg/lib by
224 # default, so embed an rpath for libasound.
225 export RUSTFLAGS="-C link-arg=-Wl,-rpath,/usr/pkg/lib"
226 ;;
227 OpenBSD)
228 # OpenBSD's default datasize ulimit is too low for rustc
229 ulimit -d "$(ulimit -H -d)" 2>/dev/null || true
230 ;;
231 esac
232 # Keep target/ outside the workspace so it isn't synced back
233 export CARGO_TARGET_DIR="$HOME/build"
234 cargo build --release --locked --bin fin
235 mkdir -p dist
236 cp "$CARGO_TARGET_DIR/release/fin" dist/fin
237
238 - name: Package tarball
239 shell: bash
240 run: |
241 set -euo pipefail
242 if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
243 version="${{ inputs.tag }}"
244 else
245 version="${GITHUB_REF_NAME}"
246 fi
247 name="fin-${version}-${{ matrix.label }}"
248 mkdir -p dist/staging
249 install -m 0755 dist/fin dist/staging/fin
250 cp README.md LICENSE dist/staging/
251 tar -C dist/staging -czf "dist/${name}.tar.gz" .
252 (cd dist && shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256")
253 ls -lh dist
254
255 - name: Upload artifact
256 uses: actions/upload-artifact@v4
257 with:
258 name: ${{ matrix.label }}
259 path: dist/*.tar.gz*
260 if-no-files-found: error
261
262 release:
263 name: Publish GitHub release
264 needs: [build, build-bsd]
265 runs-on: ubuntu-latest
266 steps:
267 - name: Download all artifacts
268 uses: actions/download-artifact@v4
269 with:
270 path: dist
271 merge-multiple: true
272
273 - name: Show artifacts
274 run: ls -lh dist
275
276 - name: Create release
277 uses: softprops/action-gh-release@v2
278 with:
279 tag_name: ${{ github.event.inputs.tag || github.ref_name }}
280 files: dist/*
281 generate_release_notes: true
282 draft: false
283 prerelease: false
284
285 - name: Check Gemfury secrets
286 id: gemfury
287 env:
288 FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
289 FURY_ACCOUNT: ${{ secrets.FURY_ACCOUNT }}
290 run: |
291 if [ -n "$FURY_TOKEN" ] && [ -n "$FURY_ACCOUNT" ]; then
292 echo "available=true" >> "$GITHUB_OUTPUT"
293 else
294 echo "available=false" >> "$GITHUB_OUTPUT"
295 echo "Gemfury secrets not set; skipping push"
296 fi
297
298 - name: Push .deb and .rpm to Gemfury
299 if: steps.gemfury.outputs.available == 'true'
300 env:
301 FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
302 FURY_ACCOUNT: ${{ secrets.FURY_ACCOUNT }}
303 shell: bash
304 run: |
305 set -euo pipefail
306 shopt -s nullglob
307 for pkg in dist/*.deb dist/*.rpm; do
308 echo "Pushing $pkg to Gemfury (${FURY_ACCOUNT})"
309 curl -fsS -F package=@"${pkg}" \
310 "https://${FURY_TOKEN}@push.fury.io/${FURY_ACCOUNT}/"
311 echo
312 done