Files
2026-09-24 16:07:47 -04:00

1247 lines
51 KiB
YAML

name: Release
# Builds the macOS, Windows, and Linux desktop apps, an Android APK, an iOS
# IPA, a Nintendo Switch SD-ready zip (experimental), Xbox UWP, the Anbernic
# RG34XXSP (Stock OS 64-bit MOD / PortMaster) and Linux ARM SBC PortMaster
# handheld ports on the self-hosted Mac runner, and publishes them as a
# GitHub Release.
#
# Versioning:
# - First ever release is 0.1.0.
# - Every push to main auto-increments the patch: 0.1.0 -> 0.1.1 -> ... -> 0.1.99,
# then rolls over to 0.2.0 and keeps going.
# - To force a specific version, either:
# * run this workflow manually (Actions tab) and type it into "version", or
# * put "[release X.Y.Z]" anywhere in the commit message.
#
# Branch model: day-to-day work merges to `dev`. Releases stay on `main` only
# so promoting `dev` -> `main` is the ship gate that cuts a build.
on:
push:
branches: [main]
# CI/workflow and docs-only changes don't ship anything to users, so they
# don't earn a release. A push touching these *and* real source still
# releases; only pushes confined entirely to these paths are skipped.
paths-ignore:
- '.github/**'
- '**.md'
- 'mobile/ios/app-repo.json'
workflow_dispatch:
inputs:
version:
description: "Exact version to release (e.g. 0.2.0). Leave blank to auto-increment."
required: false
default: ""
permissions:
contents: write
actions: write
issues: read
pull-requests: read
concurrency:
group: release
cancel-in-progress: false
jobs:
version:
name: determine release version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
tag: ${{ steps.ver.outputs.tag }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Determine version
id: ver
env:
DISPATCH_VERSION: ${{ github.event.inputs.version }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
semver_re='^[0-9]+\.[0-9]+\.[0-9]+$'
# 1) Explicit override from a manual run.
override=""
if [ -n "${DISPATCH_VERSION:-}" ]; then
override="$DISPATCH_VERSION"
else
# 2) Override from the commit message: [release X.Y.Z]
msg="$(git log -1 --pretty=%B || true)"
tag_ver="$(printf '%s' "$msg" | sed -n -E 's/.*\[release[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+)\].*/\1/p' | head -1)"
if [ -n "$tag_ver" ]; then
override="$tag_ver"
fi
fi
if [ -n "$override" ]; then
if ! printf '%s' "$override" | grep -Eq "$semver_re"; then
echo "::error::Invalid version override '$override' (expected X.Y.Z)"
exit 1
fi
version="$override"
echo "Using override version: $version"
else
# 3) Auto-increment from the highest existing vX.Y.Z tag.
latest="$(git tag -l 'v*' \
| sed -E 's/^v//' \
| grep -E "$semver_re" \
| sort -t. -k1,1n -k2,2n -k3,3n \
| tail -1 || true)"
if [ -z "$latest" ]; then
version="0.1.0"
echo "No existing release tag; starting at $version"
else
major="${latest%%.*}"
rest="${latest#*.}"
minor="${rest%%.*}"
patch="${rest##*.}"
patch=$((patch + 1))
if [ "$patch" -gt 99 ]; then
minor=$((minor + 1))
patch=0
fi
version="${major}.${minor}.${patch}"
echo "Latest was $latest; next is $version"
fi
fi
tag="v${version}"
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
echo "::error::Tag $tag already exists. Pick a different version."
exit 1
fi
if gh release view "$tag" >/dev/null 2>&1; then
echo "::error::Release $tag already exists. Pick a different version."
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
love-payload:
name: build release game.love
needs: version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Build shared payload
run: |
scripts/pack_love.sh \
--output dist/payload/game.love \
--listing dist/payload/love-listing.txt \
--version "${{ needs.version.outputs.version }}"
- name: Upload shared payload
uses: actions/upload-artifact@v7
with:
name: gen1recomp-release-love
path: dist/payload/game.love
if-no-files-found: error
retention-days: 7
linux-arm64:
name: build Linux arm64 AppImage
needs: [version, love-payload, shaderfx-bridge]
# GitHub's free arm64 runner for public repos. It has to be arm64: the
# AppImage compiles LÖVE natively inside a Debian bullseye arm64
# container, and the qemu-emulated alternative takes hours.
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- name: Download shared payload
uses: actions/download-artifact@v8
with:
name: gen1recomp-release-love
path: .bazinga/work
- name: Download the ShaderFX bridge
uses: actions/download-artifact@v8
with:
name: shaderfx-bridge-linux-arm64
path: dist/native/linux-arm64
- name: Restore compiled arm64 dependencies
uses: actions/cache@v4
with:
path: .bazinga/cache/linux-arm64
key: linux-arm64-deps-${{ hashFiles('scripts/linux-arm64/**', 'scripts/build_linux_arm64.sh') }}
- name: Build Linux arm64 AppImage
env:
SHADERFX_BRIDGE_REQUIRED: "1"
run: |
set -euo pipefail
scripts/build_linux_arm64.sh \
--version "${{ needs.version.outputs.version }}" \
--game-love .bazinga/work/game.love
- name: Verify the AppImage is self-contained and bullseye-compatible
env:
SHADERFX_BRIDGE_REQUIRED: "1"
run: bash scripts/linux-arm64/verify_appimage.sh "dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage"
- name: Upload Linux arm64 release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-linux-arm64-release
path: |
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage.sha256
if-no-files-found: error
retention-days: 7
linux-flatpak:
name: build Linux Flatpak
needs: [version, love-payload, shaderfx-bridge]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Install flatpak-builder deps
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder elfutils imagemagick
- name: Download shared payload
uses: actions/download-artifact@v8
with:
name: gen1recomp-release-love
path: .bazinga/work
- name: Download the ShaderFX bridge
uses: actions/download-artifact@v8
with:
name: shaderfx-bridge-linux-x64
path: dist/native/linux-x64
- name: Restore the Flatpak runtime
uses: actions/cache@v4
with:
path: ~/.local/share/flatpak
key: flatpak-runtime-${{ runner.arch }}-${{ hashFiles('flatpak/*.yml', 'scripts/build_flatpak.sh') }}
- name: Build Flatpak bundle
env:
SHADERFX_BRIDGE_REQUIRED: "1"
run: |
set -euo pipefail
scripts/build_flatpak.sh \
--version "${{ needs.version.outputs.version }}" \
--game-love .bazinga/work/game.love
- name: Upload Flatpak release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-linux-flatpak-release
path: |
dist/flatpak/gen1recomp-${{ needs.version.outputs.version }}-linux.flatpak
dist/flatpak/gen1recomp-${{ needs.version.outputs.version }}-linux.flatpak.sha256
if-no-files-found: error
retention-days: 7
xbox-uwp:
name: build Xbox UWP release
needs: [version, love-payload]
runs-on: windows-2022
steps:
- uses: actions/checkout@v7
- name: Download shared payload
uses: actions/download-artifact@v8
with:
name: gen1recomp-release-love
path: .bazinga/work
- name: Prepare signing certificate
shell: pwsh
env:
CERTIFICATE_BASE64: ${{ secrets.XBOX_UWP_SIGNING_CERTIFICATE }}
CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
run: |
if ($env:CANONICAL_REPOSITORY -eq 'true' -and
[string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
throw 'XBOX_UWP_SIGNING_CERTIFICATE is not configured.'
}
if ([string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
"UWP_PUBLISHER=CN=Gen1Recomp" | Out-File $env:GITHUB_ENV -Append
exit 0
}
$pfx = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.pfx'
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:CERTIFICATE_BASE64))
$flags = [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet
$cert = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
$pfx, $env:CERTIFICATE_PASSWORD, $flags)
$cer = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.cer'
[IO.File]::WriteAllBytes(
$cer,
$cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert))
Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople | Out-Null
"UWP_PFX=$pfx" | Out-File $env:GITHUB_ENV -Append
"UWP_CERT_THUMBPRINT=$($cert.Thumbprint)" | Out-File $env:GITHUB_ENV -Append
"UWP_PUBLISHER=$($cert.Subject)" | Out-File $env:GITHUB_ENV -Append
- name: Build Xbox UWP package
shell: bash
run: |
bash scripts/build_xbox_uwp.sh \
--release \
--version "${{ needs.version.outputs.version }}" \
--publisher "$UWP_PUBLISHER" \
--game-love .bazinga/work/game.love
- name: Sign and stage Xbox UWP release
shell: pwsh
env:
CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
run: |
if (-not $env:UWP_PFX) {
exit 0
}
scripts/xbox-uwp/stage_release.ps1 `
-Version '${{ needs.version.outputs.version }}' `
-Configuration Release `
-BuildInfo .bazinga/work/xbox-uwp-build-info.json `
-CertificatePath $env:UWP_PFX `
-CertificatePassword $env:CERTIFICATE_PASSWORD
- name: Upload Xbox UWP release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-xbox-uwp-release
path: |
dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip
dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip.sha256
if-no-files-found: error
retention-days: 7
- name: Remove signing certificate
if: always()
shell: pwsh
run: |
if ($env:UWP_CERT_THUMBPRINT) {
Remove-Item "Cert:\LocalMachine\TrustedPeople\$env:UWP_CERT_THUMBPRINT" -ErrorAction SilentlyContinue
}
if ($env:UWP_PFX) {
Remove-Item $env:UWP_PFX -Force -ErrorAction SilentlyContinue
}
# Windows Native AOT TLS dialer. The Mac release runner fuses the win64 zip
# from LÖVE's prebuilt binaries and cannot cross-compile this DLL, so build
# it here and inject it in the release job before scripts/build.sh win.
native-tls-win:
name: build Windows gen1tls.dll
needs: version
runs-on: windows-2022
steps:
- uses: actions/checkout@v7
- name: Identify TLS build environment
id: tls-env
shell: pwsh
run: |
"image=$env:ImageOS-$env:ImageVersion" >> $env:GITHUB_OUTPUT
# Exact keys only: a partial match could ship an outdated native library.
# Hash the build script too so changes to build flags invalidate the binary.
- name: Restore finished TLS library
id: tls-cache
uses: actions/cache/restore@v4
with:
path: dist/native/win-x64/gen1tls.dll
key: native-tls-v2-${{ runner.os }}-${{ runner.arch }}-${{ steps.tls-env.outputs.image }}-dotnet8-${{ hashFiles('native/tls_dial/**', 'scripts/ci/build_gen1tls.ps1', '**/Directory.Build.*', '**/Directory.Packages.props', '**/NuGet.Config', '**/nuget.config', '**/global.json') }}
- name: Setup .NET 8
if: steps.tls-cache.outputs.cache-hit != 'true'
uses: actions/setup-dotnet@v6
with:
dotnet-version: "8.0.x"
- name: Publish gen1tls (win-x64 Native AOT)
if: steps.tls-cache.outputs.cache-hit != 'true'
shell: pwsh
run: ./scripts/ci/build_gen1tls.ps1
- name: Verify TLS library
shell: pwsh
run: |
$dll = Get-Item 'dist/native/win-x64/gen1tls.dll' -ErrorAction Stop
if ($dll.Length -eq 0) { throw 'gen1tls.dll is empty' }
- name: Cache finished TLS library
if: steps.tls-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: dist/native/win-x64/gen1tls.dll
key: ${{ steps.tls-cache.outputs.cache-primary-key }}
- name: Upload gen1tls.dll
uses: actions/upload-artifact@v7
with:
name: gen1tls-win-x64
path: dist/native/win-x64/gen1tls.dll
if-no-files-found: error
retention-days: 7
shaderfx-bridge:
name: build ShaderFX bridge (${{ matrix.plat }})
needs: version
strategy:
fail-fast: false
matrix:
include:
- plat: win-x64
runs-on: windows-2022
lib: librashader_bridge.dll
- plat: mac
runs-on: macos-latest
lib: liblibrashader_bridge.dylib
- plat: linux-x64
runs-on: ubuntu-24.04
lib: liblibrashader_bridge.so
glibc_triple: x86_64-unknown-linux-gnu.2.17
- plat: linux-arm64
runs-on: ubuntu-24.04-arm
lib: liblibrashader_bridge.so
glibc_triple: aarch64-unknown-linux-gnu.2.17
# Built here rather than on the packaging runner so the APK's bridge
# does not depend on that machine's local Rust/NDK toolchain (#1932).
- plat: android
runs-on: ubuntu-24.04
lib: liblibrashader_bridge.so
- plat: ios
runs-on: macos-latest
lib: liblibrashader_bridge.a
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v7
- name: Identify bridge build environment
id: bridge-env
shell: bash
env:
PLAT: ${{ matrix.plat }}
run: bash scripts/ci/shaderfx_bridge.sh env
# Keep release versions and game sources out of this key. Only bridge
# inputs, build instructions, and the native environment affect reuse.
- name: Restore finished bridge
id: bridge-cache
uses: actions/cache/restore@v4
with:
path: dist/native/${{ matrix.plat }}
key: shaderfx-binary-v2-${{ matrix.plat }}-${{ runner.arch }}-${{ matrix.glibc_triple }}-${{ steps.bridge-env.outputs.hash }}-${{ hashFiles('tools/shaderfx-bridge/**', '.cargo/**', 'rust-toolchain*', 'scripts/ci/shaderfx_bridge.sh') }}
- name: Setup Rust
if: steps.bridge-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry and build dir
if: steps.bridge-cache.outputs.cache-hit != 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: tools/shaderfx-bridge
key: ${{ matrix.plat }}-${{ matrix.glibc_triple }}
- name: Build the bridge
if: steps.bridge-cache.outputs.cache-hit != 'true'
shell: bash
env:
PLAT: ${{ matrix.plat }}
LIB: ${{ matrix.lib }}
GLIBC_TRIPLE: ${{ matrix.glibc_triple }}
run: bash scripts/ci/shaderfx_bridge.sh build
- name: Verify bridge libraries
shell: bash
env:
PLAT: ${{ matrix.plat }}
LIB: ${{ matrix.lib }}
run: bash scripts/ci/shaderfx_bridge.sh verify
- name: Cache finished bridge
if: steps.bridge-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: dist/native/${{ matrix.plat }}
key: ${{ steps.bridge-cache.outputs.cache-primary-key }}
- name: Upload the bridge
uses: actions/upload-artifact@v7
with:
name: shaderfx-bridge-${{ matrix.plat }}
path: dist/native/${{ matrix.plat }}
if-no-files-found: error
retention-days: 7
desktop:
name: build macOS + Linux
needs: [version, love-payload, shaderfx-bridge]
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
concurrency:
group: mac-signing-keychain
cancel-in-progress: false
steps:
# The self-hosted runner lives under the machine owner's home
# directory; mask it first so absolute paths in every later step's
# output show up as *** in the public workflow logs.
- name: Mask runner paths
run: echo "::add-mask::$HOME"
- name: Checkout
uses: actions/checkout@v7
- name: Download ShaderFX bridge libraries
uses: actions/download-artifact@v8
with:
pattern: shaderfx-bridge-*
path: dist/native
- name: Flatten the ShaderFX bridge artifact layout
run: |
set -euo pipefail
shopt -s nullglob
for d in dist/native/shaderfx-bridge-*/; do
d="${d%/}"
plat="${d#dist/native/shaderfx-bridge-}"
mkdir -p "dist/native/$plat"
for f in "$d"/*; do mv "$f" "dist/native/$plat/"; done
rmdir "$d"
done
for plat in mac win-x64 linux-x64 linux-arm64 android; do
ls "dist/native/$plat" >/dev/null \
|| { echo "::error::no ShaderFX bridge staged for $plat"; exit 1; }
done
ls -lR dist/native
# The same game.love the arm64 AppImage and Xbox UWP builds fused, so
# every release asset ships one identical payload (build.sh's own pack
# would omit PATCH_NOTES.md and mobile/ios/app-repo.json).
- name: Download shared payload
uses: actions/download-artifact@v8
with:
name: gen1recomp-release-love
path: dist/payload
- name: Import signing certificate into a temporary keychain
if: github.repository == 'bryanthaboi/gen1recomp'
uses: ./.github/actions/mac-signing-keychain
- name: Build macOS + Linux
env:
SHADERFX_BRIDGE_REQUIRED: "1"
run: |
set -euo pipefail
scripts/build.sh mac --version "${{ needs.version.outputs.version }}" --no-notarize \
--game-love dist/payload/game.love
scripts/build.sh linux --version "${{ needs.version.outputs.version }}" \
--game-love dist/payload/game.love
- name: Notarize & staple macOS app
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
set -euo pipefail
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
if [ ! -f "$ci_dir/notary.env" ]; then
echo "::error::Missing $ci_dir/notary.env. Run scripts/ci-setup-signing.sh on the runner."
exit 1
fi
set -a; . "$ci_dir/notary.env"; set +a
echo "::add-mask::$APPLE_APP_PASSWORD"
app=".bazinga/work/gen1recomp.app"
zip="dist/mac/gen1recomp-macos.zip"
[ -d "$app" ] || { echo "::error::signed app not found at $app"; exit 1; }
if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_APP_PASSWORD:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ]; then
echo "::error::notary.env is missing APPLE_ID / APPLE_APP_PASSWORD / APPLE_TEAM_ID."
exit 1
fi
echo "Submitting to Apple notary service (can take a few minutes)..."
xcrun notarytool submit "$zip" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait
echo "Stapling ticket to the app..."
xcrun stapler staple "$app"
# Re-zip the now-stapled app (same format build.sh uses).
rm -f "$zip"
ditto -c -k --sequesterRsrc --keepParent "$app" "$zip"
echo "Notarized + stapled ✓"
- name: Upload gen1recomp-desktop-release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-desktop-release
path: |
dist/mac/gen1recomp-macos.zip
dist/linux/gen1recomp-linux-x86_64.AppImage
if-no-files-found: error
retention-days: 7
- name: Clean up signing keychain
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
run: security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
android:
name: build Android
needs: [version, shaderfx-bridge]
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
steps:
# The self-hosted runner lives under the machine owner's home
# directory; mask it first so absolute paths in every later step's
# output show up as *** in the public workflow logs.
- name: Mask runner paths
run: echo "::add-mask::$HOME"
- name: Checkout
uses: actions/checkout@v7
- name: Download the Android ShaderFX bridge
uses: actions/download-artifact@v8
with:
name: shaderfx-bridge-android
path: dist/native/android
- name: Materialize Android release signing key
env:
KEYSTORE_B64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_B64 }}
run: |
set -euo pipefail
[ -n "$KEYSTORE_B64" ] || {
echo "::error::ANDROID_RELEASE_KEYSTORE_B64 is required for a publishable Android update"
exit 1
}
python3 - <<'PY'
import base64, os, pathlib
encoded = os.environ["KEYSTORE_B64"]
path = pathlib.Path(os.environ["RUNNER_TEMP"]) / "gen1recomp-android-release.keystore"
path.write_bytes(base64.b64decode(encoded, validate=True))
PY
- name: Build Android
env:
SHADERFX_BRIDGE_REQUIRED: "1"
SHADERFX_BRIDGE_ANDROID_DIR: ${{ github.workspace }}/dist/native/android
GEN1RECOMP_ANDROID_KEYSTORE: ${{ runner.temp }}/gen1recomp-android-release.keystore
GEN1RECOMP_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}
GEN1RECOMP_ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }}
GEN1RECOMP_ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }}
run: |
set -euo pipefail
export GEN1_ANDROID_SHADOW_DIR="$HOME/.cache/gen1recomp-ci/android-shadow"
scripts/build_android.sh --release --version "${{ needs.version.outputs.version }}"
- name: Upload gen1recomp-android-release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-android-release
path: |
dist/android/release/**/*.apk
if-no-files-found: error
retention-days: 7
ios:
name: build iOS
needs: [version, shaderfx-bridge]
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
concurrency:
group: mac-signing-keychain
cancel-in-progress: false
steps:
# The self-hosted runner lives under the machine owner's home
# directory; mask it first so absolute paths in every later step's
# output show up as *** in the public workflow logs.
- name: Mask runner paths
run: echo "::add-mask::$HOME"
- name: Checkout
uses: actions/checkout@v7
- name: Import signing certificate into a temporary keychain
if: github.repository == 'bryanthaboi/gen1recomp'
uses: ./.github/actions/mac-signing-keychain
- name: Download the iOS ShaderFX bridge
uses: actions/download-artifact@v8
with:
name: shaderfx-bridge-ios
path: dist/native/ios
- name: Install xcbeautify
run: |
set -euo pipefail
brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
- name: Build iOS
env:
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
SHADERFX_BRIDGE_IOS: ${{ github.workspace }}/dist/native/ios/liblibrashader_bridge.a
run: |
set -euo pipefail
if [ "$CANONICAL_REPOSITORY" = true ]; then
scripts/build_ios.sh --fetch --device --release \
--version "${{ needs.version.outputs.version }}"
else
scripts/build_ios.sh --fetch --release \
--version "${{ needs.version.outputs.version }}"
fi
- name: Upload gen1recomp-ios-release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-ios-release
path: |
dist/ios/gen1recomp++.ipa
if-no-files-found: error
retention-days: 7
- name: Clean up signing keychain
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
run: security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
switch:
name: build Switch
needs: version
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
steps:
# The self-hosted runner lives under the machine owner's home
# directory; mask it first so absolute paths in every later step's
# output show up as *** in the public workflow logs.
- name: Mask runner paths
run: echo "::add-mask::$HOME"
- name: Checkout
uses: actions/checkout@v7
- name: Restore pinned love-nx binaries
uses: actions/cache@v4
with:
path: .bazinga/love-nx/11.5-nx1
key: love-nx-11.5-nx1-${{ hashFiles('scripts/switch/love-nx-11.5-nx1.sha256') }}
- name: Build Switch
run: |
set -euo pipefail
# Hard-fail gate: Switch ships with every release (never soft-fail).
# PR CI is path-gated (ubuntu selftest + canonical fused); release
# always builds Switch regardless of which files changed.
# Needs native switch-tools (nacptool/elf2nro) and/or Docker on the
# Mac self-hosted runner; see docs/switch-build.md.
scripts/build_switch.sh --fetch --fused \
--version "${{ needs.version.outputs.version }}"
- name: Upload gen1recomp-switch-release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-switch-release
path: |
dist/switch/gen1recomp-${{ needs.version.outputs.version }}-switch.zip
if-no-files-found: error
retention-days: 7
rg34xxsp:
name: build Anbernic RG34XXSP port
needs: [version, shaderfx-bridge]
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
steps:
# The self-hosted runner lives under the machine owner's home
# directory; mask it first so absolute paths in every later step's
# output show up as *** in the public workflow logs.
- name: Mask runner paths
run: echo "::add-mask::$HOME"
- name: Checkout
uses: actions/checkout@v7
- name: Download the Linux arm64 ShaderFX bridge
uses: actions/download-artifact@v8
with:
name: shaderfx-bridge-linux-arm64
path: dist/native/linux-arm64
- name: Build Anbernic RG34XXSP port
env:
SHADERFX_BRIDGE_REQUIRED: "1"
run: |
set -euo pipefail
# Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5
# runtime from PortMaster-GUI, so it needs no signing/notarization.
./build-rg34xxsp.sh --version "${{ needs.version.outputs.version }}"
- name: Upload gen1recomp-rg34xxsp-release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-rg34xxsp-release
path: |
dist/rg34xxsp/gen1recomp-rg34xxsp-stockos64-mod.zip
if-no-files-found: error
retention-days: 7
linux-arm-sbc:
name: build Linux ARM SBC PortMaster port
needs: [version, shaderfx-bridge]
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
steps:
# The self-hosted runner lives under the machine owner's home
# directory; mask it first so absolute paths in every later step's
# output show up as *** in the public workflow logs.
- name: Mask runner paths
run: echo "::add-mask::$HOME"
- name: Checkout
uses: actions/checkout@v7
- name: Download the Linux arm64 ShaderFX bridge
uses: actions/download-artifact@v8
with:
name: shaderfx-bridge-linux-arm64
path: dist/native/linux-arm64
- name: Build Linux ARM SBC PortMaster port
env:
# The release workflow must package the commit being released. The
# script defaults to the latest published release for standalone
# builds, while this explicit local override keeps CI source-aligned.
GEN1RECOMP_SOURCE_DIR: ${{ github.workspace }}
GEN1RECOMP_RELEASE_TAG: v${{ needs.version.outputs.version }}
SHADERFX_BRIDGE_REQUIRED: "1"
run: |
set -euo pipefail
# Same aarch64 PortMaster-style pack for Linux ARM SBC PortMaster. The build
# keeps its own cache because the two scripts use different staging
# layouts and runtime package paths.
./build-linux-arm-sbc.sh --version "${{ needs.version.outputs.version }}"
- name: Upload gen1recomp-linux-arm-sbc-release
uses: actions/upload-artifact@v7
with:
name: gen1recomp-linux-arm-sbc-release
path: |
dist/linux-arm-sbc/gen1recomp-sbc-portmaster.zip
if-no-files-found: error
retention-days: 7
notes:
name: write release notes
needs: version
if: github.repository == 'bryanthaboi/gen1recomp'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Write release notes
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="${{ needs.version.outputs.tag }}"
# Issues this release closes. Three sources, deduped by number:
# 1. GitHub's own "closing issues" links on every PR whose
# commits are in the range (works for squash, rebase, and
# merge commits alike) -- including PRs that were merged
# into a branch this release PR is itself merging in.
# 2. CLOSES/fixes/resolves text in those PRs' titles + bodies
# that GitHub didn't turn into a closing link (a PR into a
# non-default branch never gets one).
# 3. The same text in raw commit messages, so a direct push
# with "CLOSES #N #M" still lands in the notes.
# Scans every commit since the previous tag so a skipped release
# run doesn't drop issues on the floor.
prev_tag="$(git tag -l 'v*' --sort=-v:refname | grep -v "^${tag}$" | head -1 || true)"
range="${prev_tag:+${prev_tag}..}$GITHUB_SHA"
# every #N on a line that carries a closing keyword (handles the
# multi-issue "CLOSES #1 #2 #3" form the tracker uses)
scan_closes() {
grep -iE '\b(close[sd]?|fix(es|ed)?|resolve[sd]?)\b' \
| grep -oE '#[0-9]+' | tr -d '#' || true
}
nums=""
nums+=" $(git log --pretty=%B "$range" | scan_closes | tr '\n' ' ')"
prs="$(git log --pretty=%H "$range" \
| xargs -I{} gh api "repos/$GITHUB_REPOSITORY/commits/{}/pulls" \
--jq '.[].number' 2>/dev/null \
| sort -un || true)"
for pr in $prs; do
nums+=" $(gh api graphql \
-f owner="${GITHUB_REPOSITORY%/*}" \
-f name="${GITHUB_REPOSITORY#*/}" \
-F pr="$pr" \
-f query='
query($owner:String!, $name:String!, $pr:Int!) {
repository(owner:$owner, name:$name) {
pullRequest(number:$pr) {
closingIssuesReferences(first:50) { nodes { number } }
}
}
}' \
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number' \
2>/dev/null | grep -E '^[0-9]+$' | tr '\n' ' ' || true)"
nums+=" $(gh api "repos/$GITHUB_REPOSITORY/pulls/$pr" \
--jq '.title + " " + (.body // "")' 2>/dev/null \
| scan_closes | tr '\n' ' ' || true)"
done
# dedupe, drop anything that is a PR or does not exist, keep
# titles (gh api prints the response body to stdout on an HTTP
# error, so only a zero exit counts)
closed=""
for n in $(printf '%s' "$nums" | tr ' ' '\n' | grep -E '^[0-9]+$' | sort -un); do
if title="$(gh api "repos/$GITHUB_REPOSITORY/issues/$n" \
--jq 'if .pull_request then empty else .title end' \
2>/dev/null)"; then
[ -n "$title" ] && closed+="- #$n $title"$'\n'
fi
done
closed="$(printf '%s' "$closed" | grep . | sort -t'#' -k2 -n || true)"
# Everyone whose commits are in the range: GitHub login when the
# commit is linked to an account, the raw git author name when not;
# CI bots filtered out.
if [ -n "$prev_tag" ]; then
commits_endpoint="repos/$GITHUB_REPOSITORY/compare/${prev_tag}...${GITHUB_SHA}?per_page=100"
else
# No previous release: include all reachable commits, even the root.
commits_endpoint="repos/$GITHUB_REPOSITORY/commits?sha=${GITHUB_SHA}&per_page=100"
fi
contributors="$(gh api --paginate \
"$commits_endpoint" \
--jq '(if type == "array" then . else .commits end)[]
| if .author and .author.login
then "@" + .author.login
else .commit.author.name end' 2>/dev/null \
| grep -viE '\[bot\]$' | sort -uf | sed 's/^/- /' || true)"
# Use the dev -> main promotion PR for this release commit, not an
# individual feature PR whose commits also happen to be in the range.
promotion_body="$(gh api --paginate --slurp \
"repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls?per_page=100" 2>/dev/null \
| jq -r --arg repo "$GITHUB_REPOSITORY" --arg sha "$GITHUB_SHA" '
[.[][] | select(.merged_at != null
and .base.repo.full_name == $repo and .base.ref == "main"
and .head.repo.full_name == $repo and .head.ref == "dev"
and .merge_commit_sha == $sha)]
| sort_by(.merged_at) | last | .body // ""' || true)"
notes="Download the right version for your device below."
if [ -n "$(printf '%s' "$promotion_body" | tr -d '[:space:]')" ]; then
notes="$promotion_body"
fi
if [ -n "$closed" ]; then
notes+=$'\n\n## Issues closed\n\n'"$closed"
fi
if [ -n "$contributors" ]; then
notes+=$'\n\n## Contributors\n\n'"$contributors"
fi
printf 'Release notes:\n%s\n' "$notes"
mkdir -p dist/release-notes
printf '%s\n' "$notes" > dist/release-notes/release-notes.md
- name: Upload release notes
uses: actions/upload-artifact@v7
with:
name: gen1recomp-release-notes
path: dist/release-notes/release-notes.md
if-no-files-found: error
retention-days: 7
windows-sign:
name: build and sign Windows (separate run)
needs: [version, love-payload, native-tls-win, shaderfx-bridge]
if: github.repository == 'bryanthaboi/gen1recomp'
runs-on: ubuntu-latest
timeout-minutes: 100
steps:
- name: Start the Windows signing workflow
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
nonce="${{ github.run_id }}-${{ github.run_attempt }}"
title="windows ${{ needs.version.outputs.version }} ($nonce)"
gh workflow run windows-sign.yml -R "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME" \
-f version="${{ needs.version.outputs.version }}" \
-f sha="$GITHUB_SHA" \
-f source_run="${{ github.run_id }}" \
-f nonce="$nonce"
id=""
for _ in $(seq 1 60); do
id="$(gh run list -R "$GITHUB_REPOSITORY" --workflow windows-sign.yml --event workflow_dispatch -L 30 \
--json databaseId,displayTitle --jq ".[] | select(.displayTitle == \"$title\") | .databaseId" | head -1)"
[ -n "$id" ] && break
sleep 5
done
[ -n "$id" ] || { echo "::error::the Windows signing run never showed up"; exit 1; }
echo "Windows signing run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$id"
echo "run-id=$id" >> "$GITHUB_OUTPUT"
gh run watch "$id" -R "$GITHUB_REPOSITORY" --exit-status --interval 15
- name: Download the signed Windows build
uses: actions/download-artifact@v8
with:
name: gen1recomp-windows-signed
path: dist/win
run-id: ${{ steps.dispatch.outputs.run-id }}
github-token: ${{ github.token }}
- name: Check the signed Windows zip
run: |
set -euo pipefail
unzip -l dist/win/gen1recomp-win64.zip | grep -F gen1recomp.exe
unzip -l dist/win/gen1recomp-win64.zip | grep -F gen1tls.dll
unzip -l dist/win/gen1recomp-win64.zip | grep -F librashader_bridge.dll
- name: Upload gen1recomp-windows-signed
uses: actions/upload-artifact@v7
with:
name: gen1recomp-windows-signed
path: dist/win/gen1recomp-win64.zip
if-no-files-found: error
retention-days: 7
release:
needs: [version, notes, love-payload, xbox-uwp, linux-arm64, linux-flatpak, desktop, windows-sign, android, ios, switch, rg34xxsp, linux-arm-sbc]
if: github.repository == 'bryanthaboi/gen1recomp'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
- name: Download gen1recomp-release-notes
uses: actions/download-artifact@v8
with:
name: gen1recomp-release-notes
path: dist/release-notes
- name: Download gen1recomp-release-love
uses: actions/download-artifact@v8
with:
name: gen1recomp-release-love
path: dist/payload
- name: Download gen1recomp-desktop-release
uses: actions/download-artifact@v8
with:
name: gen1recomp-desktop-release
path: dist
- name: Download gen1recomp-windows-signed
uses: actions/download-artifact@v8
with:
name: gen1recomp-windows-signed
path: dist/win
- name: Download gen1recomp-android-release
uses: actions/download-artifact@v8
with:
name: gen1recomp-android-release
path: dist/android/release
- name: Download gen1recomp-ios-release
uses: actions/download-artifact@v8
with:
name: gen1recomp-ios-release
path: dist/ios
- name: Download gen1recomp-switch-release
uses: actions/download-artifact@v8
with:
name: gen1recomp-switch-release
path: dist/switch
- name: Download gen1recomp-rg34xxsp-release
uses: actions/download-artifact@v8
with:
name: gen1recomp-rg34xxsp-release
path: dist/rg34xxsp
- name: Download gen1recomp-linux-arm-sbc-release
uses: actions/download-artifact@v8
with:
name: gen1recomp-linux-arm-sbc-release
path: dist/linux-arm-sbc
- name: Download gen1recomp-xbox-uwp-release
uses: actions/download-artifact@v8
with:
name: gen1recomp-xbox-uwp-release
path: dist/xbox-uwp
- name: Download gen1recomp-linux-arm64-release
uses: actions/download-artifact@v8
with:
name: gen1recomp-linux-arm64-release
path: dist/linux-arm64
- name: Download gen1recomp-linux-flatpak-release
uses: actions/download-artifact@v8
with:
name: gen1recomp-linux-flatpak-release
path: dist/flatpak
- name: Stage release assets
if: github.repository == 'bryanthaboi/gen1recomp'
id: assets
run: |
set -euo pipefail
v="${{ needs.version.outputs.version }}"
outdir="dist/release"
rm -rf "$outdir"
mkdir -p "$outdir"
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
# x86_64 desktop Linux: raw AppImage (no zip wrapper).
x64_appimage="dist/linux/gen1recomp-linux-x86_64.AppImage"
[ -f "$x64_appimage" ] || { echo "::error::$x64_appimage not found (expected from scripts/build.sh linux)"; exit 1; }
cp "$x64_appimage" "$outdir/gen1recomp-${v}-linux-x86_64.AppImage"
chmod +x "$outdir/gen1recomp-${v}-linux-x86_64.AppImage"
# arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
# its own runner because LÖVE publishes no aarch64 binary and the
# AppImage has to be compiled natively; ships as a runnable
# AppImage so `chmod +x && ./it` just works.
arm64_appimage="dist/linux-arm64/gen1recomp-${v}-linux-arm64.AppImage"
[ -f "$arm64_appimage" ] || { echo "::error::$arm64_appimage not found (expected from the linux-arm64 job)"; exit 1; }
cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
flatpak_bundle="dist/flatpak/gen1recomp-${v}-linux.flatpak"
[ -f "$flatpak_bundle" ] || { echo "::error::$flatpak_bundle not found (expected from the linux-flatpak job)"; exit 1; }
cp "$flatpak_bundle" "$outdir/gen1recomp-${v}-linux.flatpak"
apk="$(find dist/android/release -name '*.apk' | head -1)"
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/release"; exit 1; }
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
ipa="dist/ios/gen1recomp++.ipa"
[ -f "$ipa" ] || { echo "::error::$ipa not found (expected from scripts/build_ios.sh --device)"; exit 1; }
cp "$ipa" "$outdir/gen1recomp++-${v}-ios.ipa"
swzip="dist/switch/gen1recomp-${v}-switch.zip"
[ -f "$swzip" ] || { echo "::error::$swzip not found (expected from scripts/build_switch.sh --fused → pack_sd_zip.sh)"; exit 1; }
cp "$swzip" "$outdir/gen1recomp-${v}-switch.zip"
# Local fused .nro stays under dist/switch/ for PR CI / debug; release
# publishes the SD-ready zip only.
uwp="dist/xbox-uwp/gen1recomp-${v}-xbox-uwp.zip"
[ -f "$uwp" ] || { echo "::error::$uwp not found (expected from the Xbox UWP job)"; exit 1; }
cp "$uwp" "$outdir/gen1recomp-${v}-xbox-uwp.zip"
# Anbernic handheld port (suffix names the CFW it targets, so a
# future RG35XX/other-CFW pack can ship alongside it).
rg34="dist/rg34xxsp/gen1recomp-rg34xxsp-stockos64-mod.zip"
[ -f "$rg34" ] || { echo "::error::$rg34 not found (expected from ./build-rg34xxsp.sh)"; exit 1; }
cp "$rg34" "$outdir/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"
# Linux ARM SBC PortMaster handheld port.
sbc="dist/linux-arm-sbc/gen1recomp-sbc-portmaster.zip"
[ -f "$sbc" ] || { echo "::error::$sbc not found (expected from ./build-linux-arm-sbc.sh)"; exit 1; }
cp "$sbc" "$outdir/gen1recomp-${v}-sbc-portmaster.zip"
# Platform-independent update payload, built alongside the desktop
# apps above (same game.love that gets fused into each of them).
love_file="dist/payload/game.love"
[ -f "$love_file" ] || { echo "::error::$love_file not found (expected from the love-payload job)"; exit 1; }
cp "$love_file" "$outdir/gen1recomp-${v}.love"
ls -lh "$outdir"
# Checksums for every staged release asset (sums file itself is
# written after this and named outside the gen1recomp-* glob, so it
# never lists itself).
(cd "$outdir" && shasum -a 256 gen1recomp-* > sha256sums.txt)
cat "$outdir/sha256sums.txt"
- name: Publish GitHub Release
if: github.repository == 'bryanthaboi/gen1recomp'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
v="${{ needs.version.outputs.version }}"
tag="${{ needs.version.outputs.tag }}"
notes_file="dist/release-notes/release-notes.md"
[ -f "$notes_file" ] || { echo "::error::$notes_file not found (expected from the notes job)"; exit 1; }
release_files=(
"dist/release/gen1recomp-${v}-macos.zip"
"dist/release/gen1recomp-${v}-windows.zip"
"dist/release/gen1recomp-${v}-linux-x86_64.AppImage"
"dist/release/gen1recomp-${v}-linux-arm64.AppImage"
"dist/release/gen1recomp-${v}-linux.flatpak"
"dist/release/gen1recomp-${v}-android.apk"
"dist/release/gen1recomp++-${v}-ios.ipa"
"dist/release/gen1recomp-${v}-switch.zip"
"dist/release/gen1recomp-${v}-xbox-uwp.zip"
"dist/release/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"
"dist/release/gen1recomp-${v}-sbc-portmaster.zip"
"dist/release/gen1recomp-${v}.love"
"dist/release/sha256sums.txt"
)
if [ "$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null || true)" = "true" ]; then
gh release delete "$tag" --yes
fi
gh release create "$tag" \
--draft \
--target "$GITHUB_SHA" \
--title "$v" \
--notes-file "$notes_file"
printf '%s\n' "${release_files[@]}" | xargs -P 6 -I{} gh release upload "$tag" {} --clobber
gh release edit "$tag" --draft=false
echo "Published release $tag"
- name: Update iOS app repository
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
set -euo pipefail
v="${{ needs.version.outputs.version }}"
ipa="dist/release/gen1recomp++-${v}-ios.ipa"
app_repo="mobile/ios/app-repo.json"
[ -f "$ipa" ] || { echo "::error::$ipa not found"; exit 1; }
[ -f "$app_repo" ] || { echo "::error::$app_repo not found"; exit 1; }
date="$(date -u +"%Y-%m-%d")"
size="$(wc -c < "$ipa" | tr -d '[:space:]')"
download_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/v${v}/gen1recomp++-${v}-ios.ipa"
bundle_id="com.theboisclub.gen1recompplusplus"
localized_description="Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke"
release_notes="$(GH_TOKEN="${{ github.token }}" gh release view "v${v}" --json body --jq '.body // ""' 2>/dev/null || true)"
if [ -n "$release_notes" ]; then
localized_description="$release_notes"
fi
entry="$(jq -n \
--arg version "$v" \
--arg date "$date" \
--arg download_url "$download_url" \
--arg localized_description "$localized_description" \
--argjson size "$size" \
'{version: $version, date: $date, size: $size, downloadURL: $download_url, localizedDescription: $localized_description}')"
if jq -e --arg bundle_id "$bundle_id" --arg version "$v" \
'any(.apps[] | select(.bundleIdentifier == $bundle_id).versions[]?; .version == $version)' \
"$app_repo" >/dev/null; then
jq --arg bundle_id "$bundle_id" --arg version "$v" --argjson entry "$entry" \
'(.apps[] | select(.bundleIdentifier == $bundle_id).versions) |= map(if .version == $version then $entry else . end)' \
"$app_repo" > "$app_repo.tmp"
else
jq --arg bundle_id "$bundle_id" --argjson entry "$entry" \
'(.apps[] | select(.bundleIdentifier == $bundle_id).versions) |= [$entry] + .' \
"$app_repo" > "$app_repo.tmp"
fi
mv "$app_repo.tmp" "$app_repo"
# main is PR-only for everyone except deploy keys (the "main protection"
# ruleset's bypass actor), so this push must authenticate with the
# RELEASE_DEPLOY_KEY deploy key over SSH; the workflow's GITHUB_TOKEN
# would be rejected by the branch protection.
- name: Commit iOS app repository
if: github.repository == 'bryanthaboi/gen1recomp'
env:
DEPLOY_KEY: ${{ secrets.RELEASE_DEPLOY_KEY }}
run: |
set -euo pipefail
git add mobile/ios/app-repo.json
if git diff --cached --quiet; then
echo "app-repo.json unchanged; nothing to push"
exit 0
fi
key="$RUNNER_TEMP/release-deploy-key"
printf '%s\n' "$DEPLOY_KEY" > "$key"
chmod 600 "$key"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "chore(ios): update app-repo.json [skip ci]"
git -c core.sshCommand="ssh -i $key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \
push "git@github.com:${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF_NAME}"
rm -f "$key"