chore(release): v0.7.31 #138
Workflow file for this run
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
| 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 }} |