release: 2.2.45 — soften unrecognized-PKG-magic warning (jailbroken-P… #246
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: release | |
| # Fires on version tags (preferred) or manual dispatch with a tag arg. | |
| # Required token scope: contents:write so the final job can create | |
| # the GitHub release and upload assets. | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Release tag (e.g. v2.1.0). Must match the VERSION file content prefixed with 'v'." | |
| required: true | |
| default: "v2.1.0" | |
| permissions: | |
| contents: write | |
| # A second push to the same tag (e.g., someone re-tags after fixing a | |
| # typo in release notes) would otherwise spawn a concurrent workflow | |
| # and the two would race to create-release, usually producing a | |
| # half-duplicated release page. Keep one release in flight per tag; | |
| # cancel-in-progress means the retagged push wins. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| # Tauri release builds are slow (full Rust release build + webview | |
| # bundling). Surface live progress so stuck jobs are obvious. | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| # ───────────────────────────────────────────────────────────────────── | |
| # Build the PS5 ELF once on Linux. The client bundles this file as a | |
| # Tauri resource (see `resources` in client/src-tauri/tauri.conf.json), | |
| # so every platform-build job downloads it before running tauri build. | |
| # Doing it once beats installing the PS5 SDK on every matrix leg. | |
| # ───────────────────────────────────────────────────────────────────── | |
| build-payload: | |
| name: build-payload | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # Sync downstream version files from VERSION before anything | |
| # compiles. engine-ci's version-sync job already enforces this on | |
| # every PR, but we re-run here as belt-and-braces: if a tag ever | |
| # gets pushed against a branch where someone forgot to bump, the | |
| # tagged build is still consistent (payload's PS5UPLOAD2_VERSION | |
| # macro matches the Tauri bundle's version). | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| - name: Sync version from VERSION | |
| run: node scripts/update-version.js | |
| - name: Install payload toolchain | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y clang lld unzip | |
| # Pin to a specific SDK tag rather than `latest` so an upstream | |
| # SDK release that bumps offsets (or breaks a symbol) can't | |
| # silently take out our release pipeline. v0.38 was the release | |
| # that added 11.x + 12.x kernel offsets — we want at least that. | |
| # Bump PS5_SDK_TAG when a new firmware needs support. | |
| # | |
| # `curl --retry` handles the occasional GitHub CDN hiccup so one | |
| # flaky network moment doesn't fail an entire release run. | |
| - name: Download PS5 Payload SDK | |
| env: | |
| PS5_SDK_TAG: v0.38 | |
| run: | | |
| curl --retry 3 --retry-delay 5 --retry-connrefused \ | |
| --fail --location --silent --show-error \ | |
| -o /tmp/ps5-payload-sdk.zip \ | |
| "https://github.com/ps5-payload-dev/sdk/releases/download/${PS5_SDK_TAG}/ps5-payload-sdk.zip" | |
| sudo unzip -q /tmp/ps5-payload-sdk.zip -d /opt | |
| - name: Build payload ELF | |
| env: | |
| PS5_PAYLOAD_SDK: /opt/ps5-payload-sdk | |
| run: make payload | |
| - name: Sanity-check ELF shape | |
| run: | | |
| file payload/ps5upload.elf | |
| test -s payload/ps5upload.elf | |
| # Gzip the payload before bundling. On Linux, Tauri's AppImage | |
| # bundler invokes `linuxdeploy` which walks every ELF in the | |
| # AppDir via `patchelf --print-needed`. The PS5 payload is a | |
| # FreeBSD ELF depending on PS5 sprx modules (libkernel_web.sprx | |
| # etc.) that can never exist on Linux — so linuxdeploy aborts the | |
| # whole build. A gzipped file shows `\x1f\x8b` magic instead of | |
| # `\x7fELF`, linuxdeploy skips it, and Tauri ships the .gz as a | |
| # regular resource. The host decompresses on first use (see | |
| # `probes.rs::find_bundled_payload`). We keep the bundle | |
| # resource `.elf.gz` cross-platform for consistency — macOS/ | |
| # Windows bundlers don't walk ELFs but there's no reason to have | |
| # platform-specific resource wiring. | |
| - name: Gzip payload for bundling | |
| run: | | |
| gzip -kf payload/ps5upload.elf | |
| ls -la payload/ps5upload.elf payload/ps5upload.elf.gz | |
| - name: Upload payload artifact | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: ps5upload-payload | |
| path: | | |
| payload/ps5upload.elf | |
| payload/ps5upload.elf.gz | |
| if-no-files-found: error | |
| # ───────────────────────────────────────────────────────────────────── | |
| # Build the Tauri desktop client for every supported platform/arch. | |
| # Matrix legs are fully independent — one leg failing doesn't take | |
| # out the rest, so a partial release (e.g. macOS only) is still | |
| # possible while we debug a per-platform issue. | |
| # ───────────────────────────────────────────────────────────────────── | |
| build-client: | |
| name: build-client-${{ matrix.platform }}-${{ matrix.arch }} | |
| needs: build-payload | |
| runs-on: ${{ matrix.os }} | |
| # Every matrix leg is a supported release target. Keep | |
| # fail-fast:false so one platform's failure doesn't hide the | |
| # others' diagnostics, but do not allow partial releases to | |
| # publish silently. | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-15 | |
| platform: macos | |
| arch: arm64 | |
| rust_target: aarch64-apple-darwin | |
| - os: macos-15-intel | |
| platform: macos | |
| arch: x64 | |
| rust_target: x86_64-apple-darwin | |
| - os: ubuntu-24.04 | |
| platform: linux | |
| arch: x64 | |
| rust_target: x86_64-unknown-linux-gnu | |
| - os: ubuntu-24.04-arm | |
| platform: linux | |
| arch: arm64 | |
| rust_target: aarch64-unknown-linux-gnu | |
| - os: windows-2022 | |
| platform: windows | |
| arch: x64 | |
| rust_target: x86_64-pc-windows-msvc | |
| # Windows arm64: GitHub only has native arm64 runners from the | |
| # windows-11-arm image family. Tauri supports cross-compilation | |
| # too — if that image is ever unavailable we can fall back to | |
| # `windows-2022` with `--target aarch64-pc-windows-msvc` and | |
| # just install the aarch64 Windows SDK components. | |
| - os: windows-11-arm | |
| platform: windows | |
| arch: arm64 | |
| rust_target: aarch64-pc-windows-msvc | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Setup Node | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: client/package-lock.json | |
| # Must run before `npm ci` (which would lock the client to | |
| # whatever version is currently in package-lock.json) and before | |
| # cargo/Tauri read their manifests, so every per-matrix leg | |
| # builds from the same VERSION-driven view. | |
| - name: Sync version from VERSION | |
| run: node scripts/update-version.js | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.rust_target }} | |
| # Tauri release builds pull in a few hundred crates; without | |
| # caching, cold builds take 15+ minutes per leg. Key by the | |
| # lockfile so a dep bump invalidates cleanly. | |
| - name: Cache cargo target | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| client/src-tauri/target | |
| key: ${{ runner.os }}-${{ matrix.arch }}-tauri-${{ hashFiles('client/src-tauri/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-${{ matrix.arch }}-tauri- | |
| # Linux needs the GTK/webkit toolchain for Tauri's webview to | |
| # link. The exact package names track Ubuntu releases; these | |
| # are the 24.04 set. If we bump to 26.04 revisit this list. | |
| - name: Install Linux Tauri prerequisites | |
| if: matrix.platform == 'linux' | |
| # libfuse2: Tauri's AppImage bundler invokes `linuxdeploy` which | |
| # is itself packaged as an AppImage and self-mounts via FUSE2 at | |
| # startup. Ubuntu 24.04 dropped libfuse2 from the default runner | |
| # image; without it, linuxdeploy dies immediately with a | |
| # swallowed "fuse: device not found" and Tauri surfaces the | |
| # generic "failed to run linuxdeploy" message (ubuntu-24.04-arm | |
| # happens to have it, which is why arm64 passes without this). | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libgtk-3-dev \ | |
| libwebkit2gtk-4.1-dev \ | |
| libappindicator3-dev \ | |
| librsvg2-dev \ | |
| libsoup-3.0-dev \ | |
| libjavascriptcoregtk-4.1-dev \ | |
| libfuse2 \ | |
| patchelf \ | |
| build-essential \ | |
| curl \ | |
| wget \ | |
| file | |
| - name: Install client deps | |
| working-directory: client | |
| run: npm ci --no-audit --no-fund | |
| # The client bundles the payload ELF as a Tauri resource. Without | |
| # it, `tauri build` fails with "resource not found" before ever | |
| # invoking cargo. Materialise it at the path tauri.conf.json | |
| # expects (../../payload/ps5upload.elf relative to src-tauri). | |
| - name: Restore payload ELF | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: ps5upload-payload | |
| path: payload | |
| - name: Verify payload resource in place | |
| shell: bash | |
| run: | | |
| test -s payload/ps5upload.elf.gz | |
| ls -la payload/ps5upload.elf.gz | |
| # Build the sidecar engine binary for the exact Tauri target. | |
| # The desktop build script embeds this target-specific binary | |
| # into the final app, which prevents accidentally shipping a | |
| # host-arch sidecar in cross-target builds. | |
| - name: Build engine sidecar binary | |
| working-directory: engine | |
| shell: bash | |
| run: cargo build --release -p ps5upload-engine --target "${{ matrix.rust_target }}" | |
| - name: Verify engine binary in place | |
| shell: bash | |
| env: | |
| RUST_TARGET: ${{ matrix.rust_target }} | |
| run: | | |
| target_root="engine/target/${RUST_TARGET}/release" | |
| if [ "$RUNNER_OS" = "Windows" ]; then | |
| test -s "$target_root/ps5upload-engine.exe" | |
| ls -la "$target_root/ps5upload-engine.exe" | |
| else | |
| test -s "$target_root/ps5upload-engine" | |
| ls -la "$target_root/ps5upload-engine" | |
| fi | |
| # Diagnostic: on Linux, confirm linuxdeploy can actually start | |
| # under APPIMAGE_EXTRACT_AND_RUN before handing off to tauri | |
| # build. Tauri's bundler swallows linuxdeploy's stderr, so when | |
| # something breaks we only see "failed to run linuxdeploy" — | |
| # no hint of the real cause. Running it directly here surfaces | |
| # the actual error in the CI log (exit code + stderr), which is | |
| # the difference between a 5-minute fix and hours of guessing. | |
| # Always runs — if linuxdeploy IS fine, `--version` exits 0 | |
| # with a single line and we continue. | |
| - name: Diagnose linuxdeploy (Linux only) | |
| if: matrix.platform == 'linux' | |
| env: | |
| APPIMAGE_EXTRACT_AND_RUN: "1" | |
| run: | | |
| set +e | |
| cd "$RUNNER_TEMP" | |
| url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous" | |
| case "${{ matrix.arch }}" in | |
| x64) asset=linuxdeploy-x86_64.AppImage ;; | |
| arm64) asset=linuxdeploy-aarch64.AppImage ;; | |
| esac | |
| curl --retry 3 --fail -L -o linuxdeploy "$url/$asset" | |
| chmod +x linuxdeploy | |
| echo "=== linuxdeploy --version (direct) ===" | |
| ./linuxdeploy --version | |
| rc=$? | |
| echo "=== linuxdeploy exit code: $rc ===" | |
| if [ $rc -ne 0 ]; then | |
| echo "=== retry with extract-and-run explicit ===" | |
| ./linuxdeploy --appimage-extract-and-run --version | |
| fi | |
| exit 0 | |
| - name: Build Tauri bundle | |
| working-directory: client | |
| shell: bash | |
| # No code-signing configured. The update flow is "download + | |
| # user replaces manually", which doesn't need signed bundles — | |
| # users trust the GitHub release URL (HTTPS + the repo they | |
| # downloaded the app from in the first place) and verify by | |
| # filename. On Windows a fresh .exe still trips SmartScreen | |
| # until the binary accumulates reputation; on macOS first-run | |
| # requires right-click → Open. Both are acceptable. | |
| # | |
| # Windows builds pass `--no-bundle`: we ship the raw | |
| # `target/release/PS5Upload.exe` inside a .zip (see the | |
| # "Collect bundle artefacts" step). No NSIS installer step, | |
| # which cuts ~1 minute from each Windows leg. | |
| env: | |
| APPLE_SIGNING_IDENTITY: "-" | |
| CSC_IDENTITY_AUTO_DISCOVERY: "false" | |
| RUST_TARGET: ${{ matrix.rust_target }} | |
| PLATFORM: ${{ matrix.platform }} | |
| # GitHub Actions Linux runners don't expose `/dev/fuse`, so | |
| # linuxdeploy (an AppImage) can't FUSE-mount itself even with | |
| # libfuse2 installed. Setting APPIMAGE_EXTRACT_AND_RUN=1 makes | |
| # every AppImage (linuxdeploy + its plugins + the bundle's own | |
| # runtime) self-extract to /tmp and exec from there instead. | |
| # Harmless on macOS/Windows — the env var is only read by the | |
| # AppImage runtime, which doesn't run on those platforms. | |
| APPIMAGE_EXTRACT_AND_RUN: "1" | |
| # Tauri/linuxdeploy's default strip pass can corrupt Rust-built | |
| # binaries on newer toolchains (.note.gnu.property sections). | |
| # NO_STRIP preserves them — file size goes up ~2 MB, which is | |
| # a rounding error against the webkit runtime anyway. | |
| NO_STRIP: "true" | |
| # --verbose on linuxdeploy so its stdout/stderr bleed through | |
| # Tauri's bundler instead of being swallowed into "failed to | |
| # run linuxdeploy". Harmless on other platforms — env var is | |
| # only consumed by linuxdeploy. | |
| LINUXDEPLOY_VERBOSE: "1" | |
| run: | | |
| if [ "$PLATFORM" = "windows" ]; then | |
| npx tauri build --target "$RUST_TARGET" --ci --no-bundle | |
| else | |
| # `--verbose` on Linux + macOS so the bundler prints the | |
| # actual linuxdeploy / bundle_dmg.sh invocation + captured | |
| # stderr. Without this, every bundler failure collapses | |
| # to an opaque "failed to run X" message, and we lose | |
| # ~30 min per CI round to retries with no new signal. | |
| # Windows --no-bundle doesn't invoke any post-compile | |
| # bundler so verbose there is noise-only. | |
| npx tauri build --target "$RUST_TARGET" --ci --verbose | |
| fi | |
| - name: Collect bundle artefacts | |
| shell: bash | |
| env: | |
| RUST_TARGET: ${{ matrix.rust_target }} | |
| PLATFORM: ${{ matrix.platform }} | |
| run: | | |
| set -euo pipefail | |
| dist_abs="$PWD/dist" | |
| mkdir -p "$dist_abs" | |
| target_root="client/src-tauri/target/${RUST_TARGET}/release" | |
| bundle="$target_root/bundle" | |
| echo "Scanning $bundle" | |
| ls -R "$bundle" || true | |
| case "$PLATFORM" in | |
| macos) | |
| # .dmg ships as-is — macOS users drag-to-Applications. | |
| find "$bundle" -type f -name "*.dmg" -exec cp -v {} "$dist_abs/" \; | |
| ;; | |
| linux) | |
| # .AppImage goes inside a .zip so the user downloads one | |
| # file (and browsers + mail clients play nicer with zips | |
| # than raw executables). Name it `PS5Upload.AppImage` | |
| # inside the zip so extraction yields a stable filename | |
| # regardless of tauri's full bundle filename. | |
| app="$(find "$bundle" -type f -name "*.AppImage" | head -n1)" | |
| if [ -z "$app" ]; then | |
| echo "::error::no .AppImage produced by tauri" | |
| exit 1 | |
| fi | |
| staging="$(mktemp -d)" | |
| cp -v "$app" "$staging/PS5Upload.AppImage" | |
| chmod +x "$staging/PS5Upload.AppImage" | |
| (cd "$staging" && zip -9 "$dist_abs/PS5Upload.zip" PS5Upload.AppImage) | |
| rm -rf "$staging" | |
| ;; | |
| windows) | |
| # Single-file portable .exe. Everything it needs — the | |
| # engine sidecar, the PS5 payload .gz, FAQ, CHANGELOG — | |
| # is embedded at compile time via `include_bytes!` | |
| # (see build.rs). The runtime extracts the engine + | |
| # payload into %LOCALAPPDATA%\PS5Upload\ on first use. | |
| # Users drop one .exe anywhere and it works. | |
| # | |
| # With `--no-bundle`, cargo produces the binary named | |
| # after the Cargo [package] name (ps5upload-desktop.exe), | |
| # not the Tauri productName. Rename on copy so the user- | |
| # visible filename inside the zip stays `PS5Upload.exe`. | |
| raw_exe="$target_root/ps5upload-desktop.exe" | |
| if [ ! -f "$raw_exe" ]; then | |
| echo "::error::raw .exe not found at $raw_exe" | |
| ls -la "$target_root" || true | |
| exit 1 | |
| fi | |
| staging="$(mktemp -d)" | |
| cp -v "$raw_exe" "$staging/PS5Upload.exe" | |
| # `tar -a -c -f out.zip in` uses tar's "auto-format from | |
| # extension" mode (bsdtar flag) to produce a zip. Ships | |
| # with Windows 10+ bundled tar and works in Git Bash. | |
| # Replaces 7z, which was only "soft-preinstalled" on | |
| # windows-2022 and not guaranteed on windows-11-arm. | |
| (cd "$staging" && tar -a -c -f "$dist_abs/PS5Upload.zip" PS5Upload.exe) | |
| rm -rf "$staging" | |
| ;; | |
| esac | |
| ls -la "$dist_abs" | |
| - name: Upload platform bundle | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: ps5upload-${{ matrix.platform }}-${{ matrix.arch }} | |
| path: dist/* | |
| if-no-files-found: error | |
| # ───────────────────────────────────────────────────────────────────── | |
| # Collect every artifact, normalise filenames to include the version | |
| # number, extract release notes from CHANGELOG.md, and publish the | |
| # GitHub release. Runs once, after all builds succeed. | |
| # ───────────────────────────────────────────────────────────────────── | |
| create-release: | |
| name: create-release | |
| needs: | |
| - build-payload | |
| - build-client | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # Important: resolve the tag inside a shell block that reads the | |
| # user-controlled `inputs.tag` via an env var, not direct | |
| # interpolation. Direct `${{ github.event.inputs.tag }}` inside | |
| # `run:` is a shell-injection hole — a tag like | |
| # `v1.0.0"; rm -rf $HOME; echo "` would execute arbitrary code. | |
| # See: | |
| # https://securitylab.github.com/research/github-actions-untrusted-input/ | |
| - name: Resolve tag | |
| id: tag | |
| env: | |
| INPUT_TAG: ${{ github.event.inputs.tag }} | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| if [ -n "$INPUT_TAG" ]; then | |
| raw_tag="$INPUT_TAG" | |
| else | |
| raw_tag="$REF_NAME" | |
| fi | |
| # Strictly constrain what we accept: leading 'v' + semver-ish. | |
| # Anything else we refuse with a hard failure — this is the | |
| # last opportunity to reject malicious tag text before we | |
| # start using it in filenames and shell contexts. | |
| if ! echo "$raw_tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-+._A-Za-z0-9]*)?$'; then | |
| echo "::error::Refusing to proceed: '$raw_tag' does not look like a version tag" | |
| exit 1 | |
| fi | |
| version="${raw_tag#v}" | |
| echo "tag=$raw_tag" >> "$GITHUB_OUTPUT" | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "Resolved tag=$raw_tag version=$version" | |
| - name: Sanity-check tag vs VERSION file | |
| env: | |
| TAG_VERSION: ${{ steps.tag.outputs.version }} | |
| run: | | |
| expected="$(tr -d '[:space:]' < VERSION)" | |
| if [ "$expected" != "$TAG_VERSION" ]; then | |
| echo "::warning::VERSION file says '$expected' but tag says '$TAG_VERSION'. Proceeding anyway, but update VERSION in a follow-up commit." | |
| fi | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| path: artifacts | |
| - name: Normalise release assets | |
| id: assets | |
| env: | |
| TAG_VERSION: ${{ steps.tag.outputs.version }} | |
| run: | | |
| mkdir -p release | |
| # Rename each artifact to include the version + arch so the | |
| # release page is self-describing. Three shapes total: | |
| # macOS : <name>-mac-<arch>.dmg (native installer) | |
| # Windows : <name>-win-<arch>.zip (contains portable .exe) | |
| # Linux : <name>-linux-<arch>.zip (contains AppImage) | |
| shopt -s nullglob | |
| for f in artifacts/ps5upload-macos-*/*.dmg; do | |
| arch="$(basename "$(dirname "$f")" | sed 's/^ps5upload-macos-//')" | |
| cp -v "$f" "release/PS5Upload-${TAG_VERSION}-mac-${arch}.dmg" | |
| done | |
| for f in artifacts/ps5upload-windows-*/PS5Upload.zip; do | |
| arch="$(basename "$(dirname "$f")" | sed 's/^ps5upload-windows-//')" | |
| cp -v "$f" "release/PS5Upload-${TAG_VERSION}-win-${arch}.zip" | |
| done | |
| for f in artifacts/ps5upload-linux-*/PS5Upload.zip; do | |
| arch="$(basename "$(dirname "$f")" | sed 's/^ps5upload-linux-//')" | |
| cp -v "$f" "release/PS5Upload-${TAG_VERSION}-linux-${arch}.zip" | |
| done | |
| # Single payload ELF. Does transfer / mount / browse / hardware | |
| # / FS ops — nothing else to ship alongside it. | |
| if [ -f artifacts/ps5upload-payload/ps5upload.elf ]; then | |
| cp -v artifacts/ps5upload-payload/ps5upload.elf \ | |
| "release/ps5upload-${TAG_VERSION}.elf" | |
| fi | |
| echo "---release/ contents---" | |
| ls -la release | |
| # `find` is robust against filenames containing newlines (SC2012); | |
| # `-maxdepth 1` scopes to direct children + `-mindepth 1` | |
| # excludes the release dir itself from the count. | |
| count="$(find release -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" | |
| echo "count=$count" >> "$GITHUB_OUTPUT" | |
| - name: Extract release notes from CHANGELOG | |
| env: | |
| TAG_VERSION: ${{ steps.tag.outputs.version }} | |
| run: | | |
| notes="release/RELEASE_NOTES.md" | |
| # CHANGELOG uses `## 2.2.0` headings (human-readable — no | |
| # Keep-a-Changelog brackets). Copy lines from the matching | |
| # version heading up to the next `## ` heading or EOF. | |
| awk -v ver="$TAG_VERSION" ' | |
| $0 == "## " ver { found=1; next } | |
| found && $0 ~ "^## " { exit } | |
| found && $0 ~ "^---$" { exit } | |
| found { print } | |
| ' CHANGELOG.md > "$notes" | |
| if [ ! -s "$notes" ]; then | |
| printf "Release %s.\n\nSee CHANGELOG.md for details.\n" "$TAG_VERSION" > "$notes" | |
| fi | |
| echo "---RELEASE_NOTES.md---" | |
| cat "$notes" | |
| - name: Generate latest.json updater manifest | |
| env: | |
| TAG_VERSION: ${{ steps.tag.outputs.version }} | |
| TAG_NAME: ${{ steps.tag.outputs.tag }} | |
| run: | | |
| # Publishes a single `latest.json` asset alongside the | |
| # installer files. The in-app updater fetches it from | |
| # github.com/.../releases/latest/download/latest.json and | |
| # picks the platform-appropriate bundle URL based on the | |
| # running OS + arch. | |
| # | |
| # Platform keys are the canonical ones tauri-plugin-updater | |
| # expects: darwin-aarch64, darwin-x86_64, linux-x86_64, | |
| # linux-aarch64, windows-x86_64, windows-aarch64. Missing | |
| # keys = "no update for this platform in this release" and | |
| # the plugin will report "up to date" to that user. | |
| python3 scripts/gen-updater-manifest.py \ | |
| --release-dir release \ | |
| --version "$TAG_VERSION" \ | |
| --tag "$TAG_NAME" \ | |
| --repo phantomptr/ps5upload \ | |
| --notes release/RELEASE_NOTES.md \ | |
| --out release/latest.json | |
| echo "---latest.json---" | |
| cat release/latest.json | |
| # `RELEASE_NOTES.md` is read by body_path to populate the | |
| # release body; it shouldn't also appear in the downloadable | |
| # assets. Move it out of release/ before the upload globs pick | |
| # it up. | |
| - name: Move release notes out of assets dir | |
| run: mv release/RELEASE_NOTES.md "$RUNNER_TEMP/RELEASE_NOTES.md" | |
| - name: Create GitHub release | |
| # v3 (released 2026-04-12) ports the action runtime from Node 20 | |
| # to Node 24. v2.x is still maintained but emits the GHA Node 20 | |
| # deprecation warning ("forced to Node 24 starting 2026-06-02, | |
| # removed 2026-09-16"); bumping ahead of those dates keeps the | |
| # release workflow's run page warning-free. | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.tag }} | |
| name: ${{ steps.tag.outputs.tag }} | |
| body_path: ${{ runner.temp }}/RELEASE_NOTES.md | |
| draft: false | |
| prerelease: ${{ contains(steps.tag.outputs.tag, '-') }} | |
| files: release/* |