mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
24c5114745
scripts/build.sh's `linux` target only ever produces x86_64: it unpacks LOVE's official love-11.5-x86_64.AppImage and re-fuses game.love into it. There is no aarch64 equivalent to unpack -- LOVE 11.5 publishes win32, win64, macOS, Android, iOS and exactly one x86_64 AppImage -- so arm64 desktop Linux (Raspberry Pi 4/5, Armbian, arm64 VMs on Apple Silicon) had no artifact at all. Compile LOVE 11.5 from the official linux-src tarball instead, inside a Debian bullseye arm64 container, and assemble the AppImage from scratch. Both pinned inputs (the LOVE source tarball and the AppImage type-2 runtime, on a dated tag rather than `continuous`) are SHA-256 verified on the host, so the container runs with no network access. Bullseye is the compile environment, not a claim about where the artifact runs: glibc is backward but not forward compatible, so linking against the oldest supported glibc is the only thing that makes one artifact work everywhere. The binaries come out needing only glibc 2.29 / GLIBCXX_3.4.21, covering Raspberry Pi OS bullseye through trixie and Ubuntu 20.04 onward. The dependency walker copies in LOVE's own libraries and leaves the driver-coupled, loader-coupled and font-stack libraries to the host. That last category is not cosmetic: Debian's libtheoradec is linked against libcairo, so a host cairo gets loaded into the process, and because the loader resolves one SONAME once per process it then binds to whatever libfreetype we bundled -- bullseye's 2.10.4 has no FT_Get_Transform, which cairo 1.18 needs, and the game died at startup with a symbol lookup error. Excluding the whole font stack makes the process self-consistent. CI gets three path-gated jobs: an offline selftest on ubuntu-latest (pins, the host-arch guard, the exclude list, the AppRun fusion contract), a real build on ubuntu-24.04-arm that asserts the layout, that every bundled object resolves under AppRun's LD_LIBRARY_PATH, and that the glibc floor is still <= 2.31, and a release job that reuses the shared game.love payload. None of it needs secrets or self-hosted hardware, so it runs on fork PRs. Verified end to end on a Raspberry Pi 5 (Debian trixie, Wayland): the launcher boots from the AppImage and renders correctly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
648 lines
28 KiB
YAML
648 lines
28 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, and the Anbernic
|
|
# RG34XXSP (Stock OS 64-bit MOD / PortMaster) port, then 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
|
|
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: 1
|
|
|
|
linux-arm64:
|
|
name: build Linux arm64 AppImage
|
|
needs: [version, love-payload]
|
|
# 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: Build Linux arm64 AppImage
|
|
run: |
|
|
set -euo pipefail
|
|
scripts/build_linux_arm64.sh \
|
|
--version "${{ needs.version.outputs.version }}" \
|
|
--game-love .bazinga/work/game.love
|
|
- 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: 1
|
|
|
|
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: 1
|
|
- 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
|
|
}
|
|
|
|
release:
|
|
needs: [version, xbox-uwp, linux-arm64]
|
|
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
|
|
with:
|
|
fetch-depth: 0
|
|
fetch-tags: true
|
|
|
|
- name: Import signing certificate into a temporary keychain
|
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
|
run: |
|
|
set -euo pipefail
|
|
KEYCHAIN_PATH="$RUNNER_TEMP/pokemon-signing.keychain-db"
|
|
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
|
|
p12="$ci_dir/signing.p12"
|
|
passfile="$ci_dir/signing.pass"
|
|
if [ ! -f "$p12" ] || [ ! -f "$passfile" ]; then
|
|
echo "::error::Signing material not found in $ci_dir. Run scripts/ci-setup-signing.sh on the runner."
|
|
exit 1
|
|
fi
|
|
p12pw="$(cat "$passfile")"
|
|
|
|
kcpw="$(openssl rand -base64 24)"
|
|
echo "::add-mask::$kcpw"
|
|
|
|
# Fresh, dedicated keychain — no dependence on the login keychain/session.
|
|
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
|
|
security create-keychain -p "$kcpw" "$KEYCHAIN_PATH"
|
|
security set-keychain-settings "$KEYCHAIN_PATH" # disable auto-lock
|
|
security unlock-keychain -p "$kcpw" "$KEYCHAIN_PATH"
|
|
|
|
security import "$p12" -P "$p12pw" -k "$KEYCHAIN_PATH" \
|
|
-T /usr/bin/codesign -T /usr/bin/security
|
|
|
|
# Let codesign use the key non-interactively.
|
|
security set-key-partition-list -S apple-tool:,apple:,codesign: \
|
|
-s -k "$kcpw" "$KEYCHAIN_PATH" >/dev/null
|
|
|
|
# Make the keychain visible to find-identity/codesign (prepend to search list).
|
|
existing="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
|
|
security list-keychains -d user -s "$KEYCHAIN_PATH" $existing
|
|
|
|
echo "Identities available to codesign:"
|
|
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
|
|
|
- name: Build macOS + Windows + Linux
|
|
run: |
|
|
set -euo pipefail
|
|
# Sign in-build (identity auto-detected from the temp keychain);
|
|
# notarize separately below so it uses secret credentials, not a
|
|
# login-keychain profile. "all" also builds the Linux AppImage,
|
|
# which needs no signing/notarization.
|
|
scripts/build.sh all --version "${{ needs.version.outputs.version }}" --no-notarize
|
|
|
|
- name: Build Android
|
|
run: |
|
|
set -euo pipefail
|
|
scripts/build_android.sh --version "${{ needs.version.outputs.version }}"
|
|
|
|
- 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' }}
|
|
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: 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: Build Anbernic RG34XXSP port
|
|
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: 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: Download Xbox UWP release
|
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: gen1recomp-xbox-uwp-release
|
|
path: dist/xbox-uwp
|
|
|
|
- name: Download Linux arm64 release
|
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
name: gen1recomp-linux-arm64-release
|
|
path: dist/linux-arm64
|
|
|
|
- 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"
|
|
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
|
|
|
|
# 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 rather than a zip 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"
|
|
apk="$(find dist/android/debug -name '*.apk' | head -1)"
|
|
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; 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"
|
|
|
|
# Platform-independent update payload, built alongside the desktop
|
|
# apps above (same game.love that gets fused into each of them).
|
|
love_file=".bazinga/work/game.love"
|
|
[ -f "$love_file" ] || { echo "::error::$love_file not found (expected from scripts/build.sh)"; 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 }}"
|
|
|
|
# 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.
|
|
base="${prev_tag:-$(git rev-list --max-parents=0 "$GITHUB_SHA" | tail -1)}"
|
|
contributors="$(gh api --paginate \
|
|
"repos/$GITHUB_REPOSITORY/compare/${base}...${GITHUB_SHA}" \
|
|
--jq '.commits[] | if .author and .author.login
|
|
then "@" + .author.login
|
|
else .commit.author.name end' 2>/dev/null \
|
|
| grep -viE '\[bot\]$' | sort -uf | sed 's/^/- /' || true)"
|
|
|
|
notes="Download the correct version for your computer below."
|
|
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"
|
|
|
|
release_files=(
|
|
"dist/release/gen1recomp-${v}-macos.zip"
|
|
"dist/release/gen1recomp-${v}-windows.zip"
|
|
"dist/release/gen1recomp-${v}-linux.zip"
|
|
"dist/release/gen1recomp-${v}-linux-arm64.AppImage"
|
|
"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}.love"
|
|
"dist/release/sha256sums.txt"
|
|
)
|
|
|
|
gh release create "$tag" \
|
|
--target "$GITHUB_SHA" \
|
|
--title "$v" \
|
|
--notes "$notes" \
|
|
"${release_files[@]}"
|
|
|
|
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"
|
|
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 version "$v" \
|
|
'any(.apps[] | select(.bundleIdentifier == "com.theboisclub.gen1recomp").versions[]?; .version == $version)' \
|
|
"$app_repo" >/dev/null; then
|
|
jq --arg version "$v" --argjson entry "$entry" \
|
|
'(.apps[] | select(.bundleIdentifier == "com.theboisclub.gen1recomp").versions) |= map(if .version == $version then $entry else . end)' \
|
|
"$app_repo" > "$app_repo.tmp"
|
|
else
|
|
jq --argjson entry "$entry" \
|
|
'(.apps[] | select(.bundleIdentifier == "com.theboisclub.gen1recomp").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"
|
|
|
|
- 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
|
|
rm -f "$RUNNER_TEMP/release-deploy-key"
|