iOS: give CI builds the RetroAchievements client identity

Nightly and MoonStore builds identify themselves to RetroAchievements as
stock PCSX2, so the server allows softcore only. Hardcore has worked on
locally built IPAs and nowhere else.

Nothing was broken. pcsx2/Host.cpp reads the client version from
ra_ua_secret.h behind __has_include and falls back to a stock agent when
the macro is absent. That header is gitignored, so it has never existed on
a runner.

CI now writes it from the IOS_RA_UA_VERSION repository secret, the way the
Android job already writes its keystores from NIGHTLY_RELEASE_KS_B64. A
missing secret warns and continues, because fork pull requests never
receive secrets and a nightly that fails to publish is worse than one
without hardcore. A malformed version fails the build instead:
RetroAchievements refuses a version it cannot order, and a refused agent is
indistinguishable from an unknown one on the client.

Only the nightly and pushes to master embed it. Pull request artifacts keep
the stock agent, so a test build is not one more public copy of an identity
that needs a release to revoke.

A second step reads the finished binary and fails unless the exact version
is in it, trailing space included, since a stale 1.2.3 is a prefix of a
current 1.2.345. Both iOS jobs are continue-on-error, so that failure does
not stop the workflow, but it does skip the upload that follows it, and the
publish step treats a missing IPA as absent rather than fatal. A softcore
build is not shipped.

None of this makes the version private. The compiler bakes the finished
agent into the binary as a plain literal, so anyone holding a build can
read it out. What the gitignored header prevents is a fork inheriting a
live identity straight from source.
This commit is contained in:
J1coding
2026-08-22 16:26:22 +02:00
committed by Jeen
parent 9111cd617d
commit 1b737e25f0
4 changed files with 129 additions and 0 deletions
+28
View File
@@ -169,6 +169,19 @@ jobs:
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode.app
# RetroAchievements grants hardcore on the client name and version in the HTTP user
# agent, and pcsx2/Host.cpp reads that version from a gitignored header. Only builds
# that reach players get it: a pull request artifact would be one more public copy of
# an identity that cannot be revoked without a release. Those keep the stock PCSX2
# agent and stay softcore, which is what a test build should be.
- name: Embed the RetroAchievements client identity
if: >-
github.repository == 'ARMSX2/ARMSX2' &&
github.event_name == 'push' && github.ref == 'refs/heads/master'
env:
IOS_RA_UA_VERSION: ${{ secrets.IOS_RA_UA_VERSION }}
run: bash .github/workflows/scripts/common/write-ra-ua-secret.sh
- name: Configure (CMake, Xcode generator, real device SDK, no signing)
working-directory: platforms/ios/app/src/main/cpp
run: >
@@ -209,6 +222,21 @@ jobs:
echo "IPA_NAME=$IPA_NAME" >> $GITHUB_ENV
ls -lh "$IPA_NAME"
# Nothing on the client reports a rejected agent, so a master build that lost the
# identity looks healthy until a player loses an unlock. Fail here instead. Carries
# the same guard as the write step, or every pull request would fail this check.
- name: Verify the client identity reached the binary
if: >-
github.repository == 'ARMSX2/ARMSX2' &&
github.event_name == 'push' && github.ref == 'refs/heads/master'
working-directory: platforms/ios/app/src/main/cpp
env:
IOS_RA_UA_VERSION: ${{ secrets.IOS_RA_UA_VERSION }}
run: |
APP_PATH=$(find build -name "ARMSX2iOS.app" -type d | head -1)
bash "$GITHUB_WORKSPACE/.github/workflows/scripts/common/verify-ra-ua-agent.sh" \
"$APP_PATH/ARMSX2iOS"
- name: Upload .ipa
uses: actions/upload-artifact@v4
with:
+20
View File
@@ -293,9 +293,29 @@ jobs:
- uses: actions/checkout@v7
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode.app
# RetroAchievements grants hardcore on the client name and version in the HTTP user
# agent, and pcsx2/Host.cpp reads that version from a gitignored header. Without this
# step the nightly identifies as stock PCSX2 and every unlock is softcore. Add the
# repo secret IOS_RA_UA_VERSION (the bare version, no leading v) to turn it on; the
# script warns and continues when it is absent.
- name: Embed the RetroAchievements client identity
env:
IOS_RA_UA_VERSION: ${{ secrets.IOS_RA_UA_VERSION }}
run: bash .github/workflows/scripts/common/write-ra-ua-secret.sh
- name: Build unsigned IPA
run: ./platforms/ios/scripts/build-ios-ipa.sh
# Nothing on the client reports a rejected agent, so a nightly that lost the identity
# looks healthy until a player loses an unlock. Fail here instead.
- name: Verify the client identity reached the binary
env:
IOS_RA_UA_VERSION: ${{ secrets.IOS_RA_UA_VERSION }}
run: >
bash .github/workflows/scripts/common/verify-ra-ua-agent.sh
platforms/ios/build-ios-xcode/Release-iphoneos/ARMSX2iOS.app/ARMSX2iOS
- name: Upload IPA
uses: actions/upload-artifact@v7
with:
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Fail the build if the RetroAchievements client identity did not reach the binary.
#
# A reordered step, a stale build directory or a wrong path each produce a working IPA that
# reports itself as stock PCSX2. Nothing on the client says so, and the first sign is a
# player losing a hardcore unlock. Matching the exact version rather than the name also
# catches a build that compiled an older header.
set -euo pipefail
BINARY="${1:-}"
if [[ -z "$BINARY" ]]; then
echo "::error::usage: verify-ra-ua-agent.sh <path to the built binary>"
exit 1
fi
if [[ ! -f "$BINARY" ]]; then
echo "::error::$BINARY does not exist, so the build did not produce what this check reads."
exit 1
fi
VERSION="$(printf '%s' "${IOS_RA_UA_VERSION:-}" | tr -d '[:space:]')"
if [[ -z "$VERSION" ]]; then
echo "no secret was supplied, so this build identifies as stock PCSX2. Nothing to verify."
exit 0
fi
# The trailing space comes from Host.cpp's format string and is load bearing: a stale 1.2.3
# is a prefix of a current 1.2.345, so an unanchored match passes the build this exists to
# catch. grep drains the stream instead of using -q, which exits on the first match and
# SIGPIPEs strings, leaving pipefail to read a hit as a failed pipeline. Output is discarded
# so the version never reaches the log.
if strings -a "$BINARY" | grep -F "ARMSX2-iOS/v$VERSION " >/dev/null; then
echo "$BINARY carries the RetroAchievements client identity"
exit 0
fi
echo "::error::$BINARY does not carry the expected client identity, so this build would be softcore only."
exit 1
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Write the RetroAchievements client identity that pcsx2/Host.cpp expects.
#
# Host.cpp includes "ra_ua_secret.h" behind __has_include and falls back to a stock PCSX2
# agent when the macro is absent. RetroAchievements grants hardcore on the leading
# Name/Version token of that agent, so a build without this header is softcore only. The
# header is gitignored, which is why a runner has to write it.
#
# Holding the version in a repository secret stops a fork or a code-lift from inheriting a
# live ARMSX2 identity. It hides nothing from anyone holding a build: the compiler bakes
# the finished agent into the binary as a plain literal.
set -euo pipefail
HEADER="${GITHUB_WORKSPACE:-$PWD}/pcsx2/ra_ua_secret.h"
# Whitespace pasted into the secret field would end up inside an HTTP header.
VERSION="$(printf '%s' "${IOS_RA_UA_VERSION:-}" | tr -d '[:space:]')"
if [[ -z "$VERSION" ]]; then
echo "::warning::IOS_RA_UA_VERSION is unset, so this build identifies as stock PCSX2 and RetroAchievements will allow softcore only."
exit 0
fi
# A refused agent behaves exactly like an unknown one on the client, so catch a version
# RetroAchievements cannot order here rather than at a player's first hardcore unlock.
if [[ ! "$VERSION" =~ ^[0-9]+([.-][0-9]+)*$ ]]; then
echo "::error::IOS_RA_UA_VERSION is not a numeric dotted version. RetroAchievements cannot order it and will treat the client as unknown."
exit 1
fi
if [[ ! -d "${HEADER%/*}" ]]; then
echo "::error::${HEADER%/*} does not exist. Run this from the repository checkout."
exit 1
fi
# Host.cpp's include is quoted, so one file beside it serves every platform building pcsx2/.
printf '#pragma once\n#define ARMSX2_IOS_RA_UA_VERSION "%s"\n' "$VERSION" > "$HEADER"
echo "wrote $HEADER"