-
Notifications
You must be signed in to change notification settings - Fork 0
258 lines (233 loc) · 11.9 KB
/
Copy pathandroid-release.yml
File metadata and controls
258 lines (233 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# Tag-triggered RELEASE build for Goemon64Recomp-Android.
#
# Push a tag like `v1.0.0` and this builds a SIGNED release APK (arm64-v8a) and
# publishes it as a GitHub Release. Same recompilation recipe as android.yml, but
# assembleRelease + upload-key signing, with the version derived from the tag.
#
# Triggers ONLY on tag pushes (never on PRs), so fork PRs cannot reach the ROM or
# signing secrets through this workflow — the maintainer pushes the tag.
#
# Required repository secrets:
# G64RS_REPO_WITH_PAT -> authenticated clone URL of the private ROM repo
# (same secret android.yml uses; must contain mnsg.z64).
# RELEASE_KEYSTORE_BASE64 -> base64 of the release .jks (`base64 -w0 release.jks`)
# RELEASE_STORE_PASSWORD -> keystore password
# RELEASE_KEY_ALIAS -> key alias (e.g. goemon-upload)
# RELEASE_KEY_PASSWORD -> key password (= store password for a PKCS12 keystore)
#
# The APK contains the recompiled game code + the mod's UI assets, but NO game
# data — users supply their own ROM at runtime.
name: android-release
on:
push:
tags:
- 'v*'
permissions:
contents: write # needed to create the GitHub Release
concurrency:
group: android-release-${{ github.ref }}
cancel-in-progress: false
jobs:
release-apk:
name: build + publish signed arm64 APK
runs-on: ubuntu-latest
env:
N64RECOMP_COMMIT: '81213c1831fab2521a6a5459c67b63437d67e253'
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
submodules: recursive
fetch-depth: 0 # full history + tags (versionCode is now tag-derived, not commit count)
# Issue #15 regression tripwire -- see the same step in android.yml. This
# matters more here: a release ships to users whose GPUs we cannot test.
- name: Guard dual source blending gating
run: ./.github/scripts/check-dual-src-blend.sh
# See the same step in android.yml -- the guard has regressed twice while
# being hardened, so its own behaviour is pinned by a suite.
- name: Test the dual source blending guard
run: ./.github/scripts/test-dual-src-blend-guard.sh
# Validate the committed gradle-wrapper.jar against Gradle's published good
# hashes before it runs with the signing keystore + ROM PAT in scope.
# SHA-pinned like every other action here (H7).
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@ed408507eac070d1f99cc633dbcf757c94c7933a # v4
- name: Guard — tag commit must be on main
run: |
# Any `v*` tag on ANY commit would otherwise produce a signed public
# release, so a stray/sprayed tag on a WIP commit could ship unreviewed
# signed code. Require the tagged commit to be an ancestor of main (i.e.
# reviewed + merged). fetch-depth: 0 above gives us full history; fetch
# main explicitly since checkout only fetched the tag ref.
git fetch --no-tags origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" FETCH_HEAD; then
echo "::error::Tag ${GITHUB_REF_NAME} (${GITHUB_SHA}) is not on main; refusing to publish a signed release from an unreviewed commit."
exit 1
fi
echo "Tag commit is on main — OK."
- name: Derive version from tag
id: ver
run: |
TAG="${GITHUB_REF_NAME}"
# Require a semver tag: vMAJOR.MINOR.PATCH with an optional -suffix/+suffix
# (e.g. v1.2.0, v1.1.0-rc1). Fail loudly rather than let a malformed tag
# arithmetic its way into a garbage versionCode.
if ! printf '%s' "$TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+([-+].*)?$'; then
echo "::error::Tag '$TAG' is not vMAJOR.MINOR.PATCH[-suffix]; refusing to release."
exit 1
fi
VNAME="${TAG#v}" # v1.1.0-rc1 -> 1.1.0-rc1
CORE="${VNAME%%[-+]*}" # 1.1.0-rc1 -> 1.1.0
IFS=. read -r MAJOR MINOR PATCH <<< "$CORE"
# versionCode = major*10000 + minor*100 + patch. Derived purely from the
# tag, so it is deterministic and CANNOT drift on a history rewrite the
# way `git rev-list --count` would. Any release >= v1.0.1 maps to >= 10001,
# comfortably above the 661 that v1.0.0 shipped with (commit count), so the
# switch is monotonic with no transition scheme. Scheme's only limit:
# minor and patch must each stay < 100.
if [ "$MINOR" -ge 100 ] || [ "$PATCH" -ge 100 ]; then
echo "::error::minor/patch must each be < 100 for the versionCode scheme (got $CORE)."
exit 1
fi
VCODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
# NOTE: a -rc tag maps to the SAME versionCode as its final release
# (v1.1.0-rc1 and v1.1.0 both -> 10100). rc builds are manual dry-run
# sideloads, so this collision is intentional — rc is not encoded into
# the code. (Sideloading an rc over a debug-signed install needs an
# uninstall first — signatures differ — so back up saves beforehand.)
# A -suffix tag is a PRERELEASE: it must not be published as the repo's
# "Latest release", or users would sideload a release candidate thinking
# it is the stable build.
if [ "$VNAME" != "$CORE" ]; then PRERELEASE=true; else PRERELEASE=false; fi
echo "vname=$VNAME" >> "$GITHUB_OUTPUT"
echo "vcode=$VCODE" >> "$GITHUB_OUTPUT"
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
echo "Release $TAG -> versionName=$VNAME versionCode=$VCODE prerelease=$PRERELEASE"
- name: ccache
uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23
with:
# Release-only cache namespace. Signed release builds must not restore a
# ccache populated by the debug workflow, which runs in the lower-trust
# push-to-dev context. Keep this prefix distinct from android.yml's.
key: android-release-arm64-${{ env.N64RECOMP_COMMIT }}
- name: Fetch private build inputs (ROM)
run: |
git clone "${{ secrets.G64RS_REPO_WITH_PAT }}" Goemon64RecompSecrets
# Copy only the ROM, not a wildcard — a wildcard would spray any future
# file added to the secrets repo straight into the build tree.
cp -v ./Goemon64RecompSecrets/mnsg.z64 ./
# Delete the clone immediately (its .git/config holds the PAT), then
# verify the ROM so a wrong/missing file fails clearly here rather than
# as a confusing recompiler error downstream.
rm -rf Goemon64RecompSecrets
echo "6ea0ed71032ce08fc2745f412d84936382197494 mnsg.z64" | sha1sum -c -
- name: Install host toolchain
run: |
sudo apt-get update
sudo apt-get install -y ninja-build cmake clang lld llvm make
- name: Build N64Recomp & RSPRecomp (host)
run: |
export PATH="/usr/lib/ccache:$PATH"
git clone https://github.com/Mr-Wiseguy/N64Recomp.git --recurse-submodules N64RecompSource
cd N64RecompSource
git checkout "$N64RECOMP_COMMIT"
git submodule update --init --recursive
cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_COMPILER=gcc \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache
cmake --build build --config Release --target N64RecompCLI -j "$(nproc)"
cmake --build build --config Release --target RSPRecomp -j "$(nproc)"
cp build/N64Recomp ../N64Recomp
cp build/RSPRecomp ../RSPRecomp
- name: Recompile game + RSP microcode (host; needs ROM)
run: |
./N64Recomp mnsg.toml
./RSPRecomp aspMain.toml
- name: Build host file_to_c
run: |
mkdir -p build-host-tools
clang++ -std=c++17 -O2 \
lib/rt64/src/tools/file_to_c/file_to_c.cpp \
-o build-host-tools/file_to_c
test -x build-host-tools/file_to_c
- name: Generate patches codegen (host; the Android CMake path skips this)
run: |
make -C patches CC=clang LD=ld.lld
./N64Recomp patches.toml
./build-host-tools/file_to_c \
patches/patches.bin mm_patches_bin \
RecompiledPatches/patches_bin.c RecompiledPatches/patches_bin.h
- name: Set up JDK 17
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
with:
distribution: temurin
java-version: '17'
- name: Set up Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4
- name: Install pinned NDK + CMake
run: |
yes | sdkmanager --licenses > /dev/null || true
sdkmanager "ndk;27.1.12297006" "cmake;3.22.1"
- name: Decode keystore + write keystore.properties
env:
KEYSTORE_B64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }}
STORE_PW: ${{ secrets.RELEASE_STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
KEY_PW: ${{ secrets.RELEASE_KEY_PASSWORD }}
run: |
if [ -z "$KEYSTORE_B64" ]; then
echo "::error::RELEASE_KEYSTORE_BASE64 secret is not set — cannot sign a release."
exit 1
fi
echo "$KEYSTORE_B64" | base64 -d > android/release.jks
# storeFile is resolved relative to keystore.properties' own directory (android/).
cat > android/keystore.properties <<EOF
storeFile=release.jks
storePassword=$STORE_PW
keyAlias=$KEY_ALIAS
keyPassword=$KEY_PW
EOF
- name: Build signed release APK (arm64-v8a)
working-directory: android
run: |
# -PcustomDriver=true compiles in the libadrenotools loader that backs
# Settings -> GPU Driver, and flips jniLibs.useLegacyPackaging so the
# hook libraries exist as real files in nativeLibraryDir (the loader
# silently falls back to the system driver without that). Released
# builds carry it from v1.0.4 on; drop the flag and the settings
# category disappears from the APK entirely.
./gradlew assembleRelease --no-daemon --stacktrace \
-PvName="${{ steps.ver.outputs.vname }}" \
-PvCode="${{ steps.ver.outputs.vcode }}" \
-PcustomDriver=true
- name: Verify the APK is release-signed (not debug)
run: |
APK=$(find android/app/build/outputs/apk/release -name "*.apk" | head -1)
test -n "$APK" || { echo "::error::no release APK found"; exit 1; }
BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1)
"$BUILD_TOOLS/apksigner" verify --print-certs "$APK" | tee certs.txt
if grep -qi "CN=Android Debug" certs.txt; then
echo "::error::APK is debug-signed — release signing did not take effect."
exit 1
fi
RENAMED="Goemon64Recomp-${{ steps.ver.outputs.vname }}-arm64-v8a.apk"
cp "$APK" "$RENAMED"
echo "APK_PATH=$RENAMED" >> "$GITHUB_ENV"
- name: Clean up signing material
if: always()
run: rm -f android/release.jks android/keystore.properties
- name: Publish GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
# Mark -suffix tags (v1.0.2-rc1, ...) as prereleases so they never become
# the public "Latest release".
PRERELEASE_FLAG=""
if [ "${{ steps.ver.outputs.prerelease }}" = "true" ]; then
PRERELEASE_FLAG="--prerelease"
fi
gh release create "${GITHUB_REF_NAME}" "${APK_PATH}" \
--title "Goemon64Recomp Android ${GITHUB_REF_NAME}" \
--generate-notes \
--verify-tag \
$PRERELEASE_FLAG