Skip to content

v4.1.2: silent auto-update (manual update banner removed); on-disk ca… #31

v4.1.2: silent auto-update (manual update banner removed); on-disk ca…

v4.1.2: silent auto-update (manual update banner removed); on-disk ca… #31

Workflow file for this run

name: Build PS5 Game Browser
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 'Release version tag (e.g. v4.0.0)'
required: true
default: 'v4.0.0'
# contents:write needed to create the release and upload assets.
permissions:
contents: write
jobs:
# ── Stage 1: Create (or reuse) a draft release ──────────────────────────────
create-release:
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create.outputs.result }}
tag: ${{ steps.tag.outputs.value }}
steps:
- uses: actions/checkout@v4
- name: Determine version tag
id: tag
shell: bash
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "value=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "value=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
fi
- name: Verify versions are consistent across all sources
shell: bash
env:
TAG: ${{ steps.tag.outputs.value }}
run: |
set -euo pipefail
CARGO_VERSION=$(grep -m1 '^version' src-tauri/Cargo.toml | cut -d'"' -f2)
CONF_VERSION=$(grep -m1 '"version"' src-tauri/tauri.conf.json | cut -d'"' -f4)
EXPECTED_TAG="v$CARGO_VERSION"
if [ "$CARGO_VERSION" != "$CONF_VERSION" ]; then
echo "::error::Cargo.toml ($CARGO_VERSION) and tauri.conf.json ($CONF_VERSION) disagree."
echo "::error::Fix: edit both files to the same version, commit, retag, re-push."
exit 1
fi
if [ "$TAG" != "$EXPECTED_TAG" ]; then
echo "::error::Tag $TAG doesn't match the source version (Cargo.toml=$CARGO_VERSION)."
echo "::error::Fix: bump Cargo.toml + tauri.conf.json to match the tag (or retag to v$CARGO_VERSION)."
exit 1
fi
echo "✓ Cargo.toml = tauri.conf.json = $CARGO_VERSION = $TAG"
- name: Create or reuse draft release
id: create
uses: actions/github-script@v7
with:
# Idempotent: reuse an existing release for this tag instead of
# failing on duplicate. Useful when re-running the workflow.
result-encoding: string
script: |
const tag = '${{ steps.tag.outputs.value }}';
try {
const { data: existing } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag,
});
core.info(`Reusing existing release ${existing.id} for tag ${tag}`);
return existing.id.toString();
} catch (e) {
if (e.status !== 404) throw e;
}
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tag,
name: `PS5 Game Browser ${tag}`,
draft: true,
prerelease: false,
generate_release_notes: true,
});
core.info(`Created draft release ${data.id} for tag ${tag}`);
return data.id.toString();
# ── Stage 2: Build per-platform and upload renamed assets ───────────────────
build:
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- platform: windows-latest
label: windows
target: x86_64-pc-windows-msvc
args: ''
- platform: macos-14 # Apple Silicon — builds ARM natively
label: mac-arm
target: aarch64-apple-darwin
args: '--target aarch64-apple-darwin'
- platform: macos-14 # Apple Silicon — cross-compiles to Intel
label: mac-intel # macos-13 is deprecated; macos-14 cross-builds x86_64 fine
target: x86_64-apple-darwin
args: '--target x86_64-apple-darwin'
- platform: ubuntu-22.04
label: linux
target: x86_64-unknown-linux-gnu
args: ''
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
# On macos-14 the dtolnay action occasionally installs the toolchain
# without setting it as the rustup default — leaving `cargo` as a
# rustup-init shim that fails with "unexpected argument 'metadata' found".
# Setting the default explicitly fixes it; the version print verifies.
- name: Set rustup default + verify
shell: bash
run: |
rustup default stable
rustup show
cargo --version
rustc --version
- name: Install Linux dependencies (Tauri v2)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libssl-dev \
libxdo-dev \
patchelf \
file \
build-essential
- name: Cache Rust artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
key: ${{ matrix.label }}
# Build only — NO releaseId, so tauri-action just produces bundles in
# src-tauri/target/<...>/release/bundle/. We rename + upload them ourselves
# in the next step using the user's preferred naming scheme.
# CATALOG GATING IS RUST-SIDE ONLY. The catalog JSON is fetched through the
# tokened Worker by the fetch_catalog command (compiled with CATALOG_URL /
# CATALOG_TOKEN in the build step below), which runs OUTSIDE the webview.
# Cover images + ui_patch.* stay on the public pub-*.r2.dev bucket and are
# NEVER routed through the Worker. The webview's `const CDN` (used for images
# AND the JS catalog fallback) must therefore ALWAYS stay a public r2.dev
# host: rewriting it to the Worker is exactly what white-screened v4.1.0 —
# the JS fallback can't send X-App-Token, so it 403'd and blanked the boot.
# This step rewrites NOTHING; it asserts that invariant and fails the build
# if a future change ever re-introduces the coupling.
- name: Assert public image/fallback CDN is intact (anti-white-screen guard)
shell: bash
run: |
set -euo pipefail
if ! grep -Eq "const CDN[[:space:]]*=[[:space:]]*'https://pub-[0-9a-f]+\.r2\.dev'" ui/index.html; then
echo "::error::ui/index.html const CDN is not a public pub-*.r2.dev host. Catalog gating is Rust-side only (fetch_catalog); the webview CDN const — images + the JS catalog fallback — must never be masked to the Worker. Masking it 403'd the tokenless JS fallback and white-screened v4.1.0."
exit 1
fi
echo "OK: webview CDN const is public r2.dev — catalog gating is Rust-side only (no JS-fallback coupling)"
- name: Build with tauri-action
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Token-gated catalog config, compiled into the binary via option_env!
# in main.rs. Empty when the secrets aren't set, so the app falls back
# to the public CDN — a safe no-op until you add these two repo secrets.
CATALOG_URL: ${{ secrets.CATALOG_URL }}
CATALOG_TOKEN: ${{ secrets.CATALOG_TOKEN }}
with:
projectPath: src-tauri
args: ${{ matrix.args }}
- name: Rename and upload artifacts
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.create-release.outputs.tag }}
LABEL: ${{ matrix.label }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# `find` is authoritative — tauri-action's artifactPaths is sometimes
# incomplete (drops sub-bundles).
#
# PORTABLE OUTPUTS:
# Windows: src-tauri/target/release/<binary>.exe (raw exe — built by cargo)
# macOS: src-tauri/target/<arch>/release/bundle/macos/<Product>.app (we zip it)
# Linux: src-tauri/target/release/bundle/appimage/<binary>_<ver>_amd64.AppImage
#
# Bundle dir only relevant for macOS and Linux. Windows builds no bundle.
if [ "$LABEL" != "windows" ]; then
BUNDLE_DIR=$(find src-tauri/target -type d -path '*/release/bundle' | head -1)
if [ -z "$BUNDLE_DIR" ] || [ ! -d "$BUNDLE_DIR" ]; then
echo "::error::no bundle directory found under src-tauri/target"
exit 1
fi
echo "Bundle directory: $BUNDLE_DIR"
find "$BUNDLE_DIR" -type f -o -type d -name '*.app' | sort
fi
upload() {
local SRC="$1"; local NEW_NAME="$2"
if [ -z "$SRC" ] || [ ! -e "$SRC" ]; then
echo " skip (not found): $NEW_NAME"
return 1
fi
local STAGED
STAGED="$(dirname "$SRC")/$NEW_NAME"
# If the source is already named correctly (mac .app.zip case), skip cp.
# cp errors with "are identical" rather than silently no-op'ing.
if [ "$SRC" != "$STAGED" ]; then
cp -f "$SRC" "$STAGED"
fi
echo " uploading: $NEW_NAME"
gh release upload "$TAG" "$STAGED" --clobber --repo "$REPO"
}
# Filenames include the tag for discoverability outside the app:
# users who download "PS5 Game Browser v4.0.0.exe" can tell at a
# glance which version they have. The in-app updater matches by
# file extension + arch substring, so it doesn't care about the
# version part of the name.
case "$LABEL" in
windows)
# Raw portable exe — Tauri 2 always builds this at target/release/<binary>.exe
# regardless of bundle.targets. Drop NSIS entirely.
SRC=$(find src-tauri/target/release -maxdepth 1 -name 'ps5-game-browser.exe' -type f | head -1)
upload "$SRC" "PS5 Game Browser $TAG.exe"
;;
mac-arm)
# Zip the .app bundle. `ditto -ck --sequesterRsrc --keepParent`
# is the macOS-native way to zip preserving HFS+ metadata and
# the .app top-level directory (so unzip recreates "PS5 Game Browser.app").
# Build the zip with a FILENAME-only target inside the cd, then
# reference it from the parent shell with the full path. Using
# a relative path with embedded slashes inside cd creates a
# bizarre nested directory and breaks the upload.
APP=$(find "$BUNDLE_DIR/macos" -maxdepth 1 -name '*.app' -type d | head -1)
if [ -n "$APP" ]; then
APP_DIR="$(dirname "$APP")"
APP_NAME="$(basename "$APP")"
ZIP_NAME="PS5 Game Browser ${TAG}_aarch64.app.zip"
( cd "$APP_DIR" && ditto -ck --sequesterRsrc --keepParent "$APP_NAME" "$ZIP_NAME" )
upload "$APP_DIR/$ZIP_NAME" "$ZIP_NAME"
else
echo "::error::no .app found at $BUNDLE_DIR/macos"
exit 1
fi
;;
mac-intel)
APP=$(find "$BUNDLE_DIR/macos" -maxdepth 1 -name '*.app' -type d | head -1)
if [ -n "$APP" ]; then
APP_DIR="$(dirname "$APP")"
APP_NAME="$(basename "$APP")"
ZIP_NAME="PS5 Game Browser ${TAG}_x64.app.zip"
( cd "$APP_DIR" && ditto -ck --sequesterRsrc --keepParent "$APP_NAME" "$ZIP_NAME" )
upload "$APP_DIR/$ZIP_NAME" "$ZIP_NAME"
else
echo "::error::no .app found at $BUNDLE_DIR/macos"
exit 1
fi
;;
linux)
APPIMAGE=$(find "$BUNDLE_DIR" -type f -name '*.AppImage' | head -1)
upload "$APPIMAGE" "PS5 Game Browser ${TAG}_amd64.AppImage"
;;
*)
echo "::error::unknown label: $LABEL"
exit 1
;;
esac
# ── Stage 3: Flip draft to published ────────────────────────────────────────
publish-release:
needs: [create-release, build]
runs-on: ubuntu-latest
if: always() && needs.create-release.result == 'success'
steps:
- name: Publish draft as latest release
uses: actions/github-script@v7
with:
script: |
const release_id = parseInt('${{ needs.create-release.outputs.release_id }}', 10);
const { data: release } = await github.rest.repos.getRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id,
});
const asset_count = (release.assets || []).length;
core.info(`Release ${release_id} has ${asset_count} uploaded asset(s)`);
if (asset_count === 0) {
core.warning('No assets uploaded — leaving release as draft.');
return;
}
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id,
draft: false,
make_latest: 'true',
});
core.info(`Published release ${release_id} with ${asset_count} asset(s)`);