Skip to content

chore(release): v0.7.31 #138

chore(release): v0.7.31

chore(release): v0.7.31 #138

Workflow file for this run

name: Desktop Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 0.1.0)'
required: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
platform: mac
arch: arm64
- os: macos-latest
platform: mac
arch: x64
- os: windows-latest
platform: win
arch: x64
- os: ubuntu-latest
platform: linux
arch: x64
- os: ubuntu-latest
platform: linux
arch: arm64
runs-on: ${{ matrix.os }}
name: Build ${{ matrix.platform }}-${{ matrix.arch }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build shared packages
run: pnpm --filter @pairux/shared-types build
# macOS code signing
- name: Import macOS certificates
if: matrix.platform == 'mac'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
if [ -n "$APPLE_CERTIFICATE" ]; then
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
rm certificate.p12
fi
# Windows code signing
- name: Setup Windows signing
if: matrix.platform == 'win'
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
run: |
if ($env:WINDOWS_CERTIFICATE) {
[IO.File]::WriteAllBytes("certificate.pfx", [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE))
}
shell: pwsh
# Linux dependencies for electron-builder
- name: Install Linux dependencies
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y libarchive-tools rpm squashfs-tools
- name: Write .env file
run: printenv ENV_FILE > apps/desktop/.env
env:
ENV_FILE: ${{ secrets.ENV_FILE }}
- name: Build desktop app
run: pnpm --filter @pairux/desktop build
env:
# TURN server configuration
TURN_SERVER_URL: ${{ secrets.TURN_SERVER_URL }}
TURN_SERVER_USERNAME: ${{ secrets.TURN_SERVER_USERNAME }}
TURN_SERVER_CREDENTIAL: ${{ secrets.TURN_SERVER_CREDENTIAL }}
TURNS_SERVER_URL: ${{ secrets.TURNS_SERVER_URL }}
- name: Package desktop app
working-directory: apps/desktop
run: npx electron-builder --${{ matrix.platform }} --${{ matrix.arch }} --publish never
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# macOS notarization (afterSign hook)
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# Windows signing
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
# Guard against corrupt AppImage builds reaching users. electron-builder
# has intermittently produced AppImages with a corrupt squashfs (valid
# superblock, but unreadable id/inode tables -> "sqfs_traverse_open error"
# / "Failed to extract AppImage" at runtime). Fully extract the squashfs
# here so a bad artifact fails the build instead of being published.
- name: Verify AppImage integrity
if: matrix.platform == 'linux'
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
imgs=(apps/desktop/out/*.AppImage)
if [ ${#imgs[@]} -eq 0 ]; then
echo "::error::No AppImage was produced by electron-builder"
exit 1
fi
for img in "${imgs[@]}"; do
echo "Verifying squashfs of $img"
# Locate the appended squashfs by scanning for its 'hsqs' magic and
# keeping the first candidate that has a VALID superblock (the magic
# bytes also occur by chance inside the ELF runtime). This is
# arch-independent — the arm64 runtime can't be executed on the x64
# runner, but unsquashfs reads the gzip data regardless of CPU arch.
off=""
while IFS=: read -r cand _; do
if unsquashfs -s -o "$cand" "$img" >/dev/null 2>&1; then
off="$cand"; break
fi
done < <(grep -abo 'hsqs' "$img")
if [ -z "${off:-}" ]; then
echo "::error::no valid squashfs superblock found in $img"
exit 1
fi
echo " squashfs offset=$off"
rm -rf /tmp/appverify
if ! unsquashfs -o "$off" -d /tmp/appverify "$img" >/tmp/unsquashfs.log 2>&1; then
echo "::error::$img has a corrupt squashfs (extraction failed) — not publishing"
tail -30 /tmp/unsquashfs.log
exit 1
fi
# The extracted tree must contain the real app payload.
if [ ! -e /tmp/appverify/AppRun ] && [ ! -d /tmp/appverify/usr ] && [ ! -d /tmp/appverify/resources ]; then
echo "::error::$img extracted but is missing expected contents"
ls -la /tmp/appverify
exit 1
fi
echo " OK: $img extracted cleanly"
done
# Bundle a CURRENT static ffmpeg. The old @ffmpeg-installer npm binary is a
# 2018 (4.x) build that crashes (SIGSEGV) transcoding VP9/WebM from
# MediaRecorder — it broke RTMP streaming for every installer user.
# Linux/Windows use BtbN's maintained static GPL builds; macOS (no BtbN
# builds) keeps the npm fallback for now.
- name: Bundle ffmpeg binary
shell: bash
env:
GH_API_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
EXT=""
if [ "${{ matrix.platform }}" = "win" ]; then EXT=".exe"; fi
OUT="apps/desktop/out/ffmpeg-${{ matrix.platform }}-${{ matrix.arch }}${EXT}"
# Resolve the asset via the API instead of the static
# ".../latest/download/ffmpeg-master-latest-..." URL: BtbN rotates
# autobuilds several times a day and some releases only carry
# versioned asset names (ffmpeg-N-<rev>-g<hash>-...), which 404s the
# static alias. Suffix-match handles both schemes; retry rides out
# the window where a fresh release has no assets yet.
fetch_btbn() {
local suffix="$1" dest="$2" url=""
for attempt in 1 2 3; do
url=$(curl -fsSL -H "Authorization: Bearer $GH_API_TOKEN" \
"https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest" \
| jq -r --arg s "$suffix" \
'[.assets[] | select(.name | endswith($s))][0].browser_download_url // empty')
if [ -n "$url" ] && curl -fsSL -o "$dest" "$url"; then
echo "Downloaded $url"
return 0
fi
echo "BtbN fetch attempt $attempt failed; retrying in 20s"
sleep 20
done
return 1
}
case "${{ matrix.platform }}-${{ matrix.arch }}" in
linux-x64)
fetch_btbn "linux64-gpl.tar.xz" ff.tar.xz
mkdir -p ffx && tar -xJf ff.tar.xz -C ffx && cp ffx/*/bin/ffmpeg "$OUT"
;;
linux-arm64)
fetch_btbn "linuxarm64-gpl.tar.xz" ff.tar.xz
mkdir -p ffx && tar -xJf ff.tar.xz -C ffx && cp ffx/*/bin/ffmpeg "$OUT"
;;
win-x64)
fetch_btbn "win64-gpl.zip" ff.zip
mkdir -p ffx && unzip -q ff.zip -d ffx && cp ffx/*/bin/ffmpeg.exe "$OUT"
;;
*)
FFMPEG_PATH=$(node -e "try { console.log(require('@ffmpeg-installer/ffmpeg').path) } catch(e) { process.exit(1) }")
cp "$FFMPEG_PATH" "$OUT"
;;
esac
chmod +x "$OUT" || true
# Smoke-test when the binary matches the runner arch (x64 builds on
# x64 runners; the cross-built linux-arm64 binary can't execute here).
if [ "${{ matrix.arch }}" = "x64" ]; then
"$OUT" -version | head -1
fi
gzip "$OUT"
echo "Bundled ffmpeg as $(basename "$OUT").gz"
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
path: |
apps/desktop/out/*.dmg
apps/desktop/out/*.zip
apps/desktop/out/*.exe
apps/desktop/out/*.AppImage
apps/desktop/out/*.deb
apps/desktop/out/*.rpm
apps/desktop/out/*.gz
retention-days: 7
# These are all already-compressed binaries. upload-artifact's
# default deflate (level 6) re-compresses them for no size win and
# has repeatedly produced corrupt artifacts at the upload boundary
# (build + download-artifact hash checks pass, but the squashfs/rpm
# payload is unreadable -> release integrity gate fails). Store them
# uncompressed to skip that code path. If corruption still recurs,
# the next step is to drop the artifact round-trip entirely and
# upload straight to the GitHub Release.
compression-level: 0
release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Get version
id: version
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
REF_NAME: ${{ github.ref_name }}
run: |
if [ -n "$INPUT_VERSION" ]; then
VERSION="$INPUT_VERSION"
else
VERSION="$REF_NAME"
fi
# Strip 'v' prefix if present
VERSION="${VERSION#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
# Only mark as prerelease if version has prerelease suffix (alpha, beta, rc)
if [[ "$VERSION" == *"-alpha"* ]] || [[ "$VERSION" == *"-beta"* ]] || [[ "$VERSION" == *"-rc"* ]]; then
echo "prerelease=true" >> $GITHUB_OUTPUT
else
echo "prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: dist
pattern: desktop-*
merge-multiple: true
- name: List artifacts
run: ls -la dist/
# Re-verify the EXACT artifacts that are about to be published. The
# build job's "Verify AppImage integrity" step runs BEFORE
# upload-artifact, so it cannot catch corruption introduced by the
# artifact upload/download round-trip. v0.7.26 shipped a linux-x64
# bundle whose AppImage, .deb AND .rpm were all corrupt (valid headers,
# unreadable compressed payloads -> "sqfs_traverse_open error" /
# "lzma error: compressed data is corrupt") even though the per-build
# check passed — the corruption happened in transit. Verifying here,
# after download and before the release is created, makes a corrupt
# artifact fail the build instead of reaching users (re-run to rebuild).
- name: Install verification tools
run: sudo apt-get update && sudo apt-get install -y squashfs-tools binutils libarchive-tools unzip
- name: Verify artifact integrity
shell: bash
run: |
set -uo pipefail
shopt -s nullglob
fail=0
for img in dist/*.AppImage; do
echo "::group::AppImage $img"
# Locate the appended squashfs by its 'hsqs' magic; keep the first
# offset with a valid superblock (the magic also occurs by chance
# inside the ELF runtime). Arch-independent: unsquashfs reads the
# gzip payload regardless of the runtime's CPU arch.
off=""
while IFS=: read -r cand _; do
if unsquashfs -s -o "$cand" "$img" >/dev/null 2>&1; then off="$cand"; break; fi
done < <(grep -abo 'hsqs' "$img")
if [ -z "$off" ]; then
echo "::error::no valid squashfs superblock in $img"; fail=1; echo "::endgroup::"; continue
fi
rm -rf /tmp/appverify
if unsquashfs -o "$off" -d /tmp/appverify "$img" >/tmp/uns.log 2>&1 \
&& { [ -e /tmp/appverify/AppRun ] || [ -d /tmp/appverify/usr ] || [ -d /tmp/appverify/resources ]; }; then
echo "OK $img"
else
echo "::error::$img has a corrupt squashfs"; tail -20 /tmp/uns.log; fail=1
fi
echo "::endgroup::"
done
for deb in dist/*.deb; do
echo "::group::deb $deb"
if dpkg-deb -x "$deb" /tmp/debverify >/tmp/deb.log 2>&1; then echo "OK $deb"; rm -rf /tmp/debverify
else echo "::error::$deb is corrupt"; tail -20 /tmp/deb.log; fail=1; fi
echo "::endgroup::"
done
# Verify rpms with bsdtar (libarchive), NOT rpm2cpio|cpio: the
# runner's rpm2cpio can't decompress zstd payloads (electron-builder
# emits zstd rpms for some arches, e.g. aarch64), so it falsely
# reported "premature end of file" on perfectly valid rpms. libarchive
# handles every rpm payload compressor (gzip/xz/zstd) uniformly.
for rpm in dist/*.rpm; do
echo "::group::rpm $rpm"
if bsdtar -tf "$rpm" >/dev/null 2>/tmp/rpm.log; then echo "OK $rpm"
else echo "::error::$rpm is corrupt"; tail -20 /tmp/rpm.log; fail=1; fi
echo "::endgroup::"
done
# macOS .zip (Electron app bundle) — cheap to validate on Linux.
# .dmg / Windows .exe aren't generically extractable here.
for z in dist/*.zip; do
echo "::group::zip $z"
if unzip -t "$z" >/tmp/zip.log 2>&1; then echo "OK $z"
else echo "::error::$z is corrupt"; tail -20 /tmp/zip.log; fail=1; fi
echo "::endgroup::"
done
if [ "$fail" -ne 0 ]; then
echo "::error::One or more release artifacts are corrupt — aborting before publish. Re-run this workflow to rebuild."
exit 1
fi
echo "All verifiable artifacts extracted cleanly."
- name: Generate checksums
run: |
cd dist
sha256sum * > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: Create Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.version.outputs.tag }}
name: PairUX ${{ steps.version.outputs.version }}
draft: false
prerelease: ${{ steps.version.outputs.prerelease }}
generate_release_notes: true
files: |
dist/*
env:
# Use PKG_SUBMIT_TOKEN (PAT) to trigger submit-packages workflow
# GITHUB_TOKEN doesn't trigger other workflows
GITHUB_TOKEN: ${{ secrets.PKG_SUBMIT_TOKEN || secrets.GITHUB_TOKEN }}