-
Notifications
You must be signed in to change notification settings - Fork 2
371 lines (340 loc) · 15.1 KB
/
Copy pathrelease-apk.yml
File metadata and controls
371 lines (340 loc) · 15.1 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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
name: Release builds
# On every push of a `v*` tag (e.g. v0.9.0), build every surface with
# consistent filenames and attach them to the matching GitHub release:
#
# phone-<version>-slim.apk (slim release-signed phone APK)
# wearos-<version>.apk (release-signed wear APK)
# hud-<version>.apk (release-signed HUD APK)
# garmin-<version>.iq (multi-device Connect IQ bundle —
# this is the file you upload to the
# Connect IQ Store; one .iq covers
# every supported device)
# garmin-<version>-prgs.zip (per-device .prg files bundled —
# for advanced sideload via
# Connect IQ Mobile on Garmins
# without store access)
#
# `<version>` is the tag with the leading `v` stripped (v0.9.0 -> 0.9.0).
# Naming matches the branch-builds workflow's `<device>-...` convention.
#
# Why slim? The Play AAB embeds the wear APK so Google Play auto-delivers
# it to a paired Wear OS watch. Sideload users who download the APK from
# GitHub don't need that path (many of them don't even own a watch), so
# we save them ~40 MB by stripping the embedded wear from the GitHub copy.
# The companion wear APK is uploaded as its own file for sideloaders.
#
# Garmin CI is opt-in: set the GARMIN_SDK_BUNDLE_URL repo secret to a stable
# download URL for the bundle produced by tools/bundle-ciq-sdk-for-ci.ps1.
# Without that secret the garmin job exits as `skipped` cleanly.
#
# Required signing secrets (Settings -> Secrets and variables -> Actions):
# RELEASE_KEYSTORE : base64-encoded contents of the release .jks file
# (locally: `base64 -w0 path/to/release.jks > out.txt`)
# KEYSTORE_PASSWORD : store password
# KEY_ALIAS : signing key alias
# KEY_PASSWORD : key password
#
# If any signing secret is missing the workflow short-circuits with a clear
# message rather than producing an unsigned APK by accident.
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing release tag to attach the artifacts to (e.g. v0.8.11)"
required: true
permissions:
contents: write
jobs:
meta:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.ref }}
version: ${{ steps.tag.outputs.version }}
steps:
- name: Resolve target tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
ref="${{ inputs.tag }}"
else
ref="${GITHUB_REF##*/}"
fi
version="${ref#v}"
echo "ref=${ref}" >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
android:
runs-on: ubuntu-latest
needs: meta
steps:
- name: Checkout tag
uses: actions/checkout@v4
with:
ref: ${{ needs.meta.outputs.tag }}
fetch-depth: 1
- name: Verify signing secrets are configured
env:
RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
missing=()
[ -z "$RELEASE_KEYSTORE" ] && missing+=(RELEASE_KEYSTORE)
[ -z "$KEYSTORE_PASSWORD" ] && missing+=(KEYSTORE_PASSWORD)
[ -z "$KEY_ALIAS" ] && missing+=(KEY_ALIAS)
[ -z "$KEY_PASSWORD" ] && missing+=(KEY_PASSWORD)
if [ "${#missing[@]}" -gt 0 ]; then
echo "::error::Release signing secrets missing: ${missing[*]}"
echo "::error::Add them in Settings -> Secrets and variables -> Actions, then re-run this workflow."
exit 1
fi
- name: Restore release keystore
env:
RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }}
run: |
echo "$RELEASE_KEYSTORE" | base64 -d > release.jks
test -s release.jks || {
echo "::error::Decoded RELEASE_KEYSTORE is empty."; exit 1; }
magic4=$(head -c4 release.jks | xxd -p)
magic2=$(printf '%s' "$magic4" | cut -c1-4)
case "$magic4" in
feedfeed|cececece) : ;;
*)
case "$magic2" in
3082) : ;;
*)
echo "::error::Decoded RELEASE_KEYSTORE has unexpected magic header (got $magic4). Expected feedfeedXX (JKS), cececeXX (JCEKS) or 3082XXXX (PKCS#12). Re-encode with: base64 -w0 path/to/release.jks"
exit 1 ;;
esac ;;
esac
- name: Write keystore.properties
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
cat > keystore.properties <<EOF
storeFile=release.jks
storePassword=$KEYSTORE_PASSWORD
keyAlias=$KEY_ALIAS
keyPassword=$KEY_PASSWORD
EOF
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v3
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Verify checkout supports the slim flag
run: |
# The `-PembedWear=false` path was added after v0.8.11 was tagged.
# If we're firing against an older ref that doesn't carry the flag,
# we'd silently build a 60 MB fat APK without realising it, and
# then overwrite the manually-uploaded slim APK on the release.
# Hard-fail with a clear message so the operator knows to either
# cherry-pick the flag onto that tag, or upload the slim APK by
# hand.
if ! grep -q "embedWear" app/build.gradle.kts; then
echo "::error::This ref does not contain the embedWear Gradle flag (added after v0.8.11). The workflow would silently build a fat APK. Skipping."
exit 1
fi
- name: Build slim release phone APK + wear + HUD
run: |
targets=":app:assembleRelease"
if [ -f wear/build.gradle.kts ]; then
targets="$targets :wear:assembleRelease"
fi
if [ -f hud/build.gradle.kts ]; then
targets="$targets :hud:assembleRelease"
fi
./gradlew :app:testDebugUnitTest :hud-protocol:test $targets -PembedWear=false --no-daemon --stacktrace
env:
GRADLE_OPTS: -Xmx4g -Dorg.gradle.jvmargs=-Xmx4g
- name: Stage APKs with canonical filenames
run: |
mkdir -p /tmp/out
cp app/build/outputs/apk/release/phone-release.apk \
"/tmp/out/phone-${{ needs.meta.outputs.version }}-slim.apk"
# Wear release APK lives under wear/build/outputs/apk/release/. Some
# branches build only debug for wear; fall back to that.
if [ -f wear/build/outputs/apk/release/wearos-release.apk ]; then
cp wear/build/outputs/apk/release/wearos-release.apk \
"/tmp/out/wearos-${{ needs.meta.outputs.version }}.apk"
elif [ -f wear/build/outputs/apk/debug/wearos-debug.apk ]; then
cp wear/build/outputs/apk/debug/wearos-debug.apk \
"/tmp/out/wearos-${{ needs.meta.outputs.version }}.apk"
fi
# HUD release APK — same release/debug fallback as wear so older
# tags without a release signing config still produce something
# installable.
if [ -f hud/build/outputs/apk/release/hud-release.apk ]; then
cp hud/build/outputs/apk/release/hud-release.apk \
"/tmp/out/hud-${{ needs.meta.outputs.version }}.apk"
elif [ -f hud/build/outputs/apk/debug/hud-debug.apk ]; then
cp hud/build/outputs/apk/debug/hud-debug.apk \
"/tmp/out/hud-${{ needs.meta.outputs.version }}.apk"
fi
ls -la /tmp/out
- name: Stage for publish
uses: actions/upload-artifact@v4
with:
name: stage-android
path: /tmp/out/*
retention-days: 1
- name: Wipe sensitive files
if: always()
run: |
rm -f release.jks keystore.properties
garmin:
runs-on: ubuntu-latest
needs: meta
env:
GARMIN_SDK_BUNDLE_URL: ${{ secrets.GARMIN_SDK_BUNDLE_URL }}
steps:
- name: Skip if bundle URL not configured
id: precheck
run: |
if [ -z "$GARMIN_SDK_BUNDLE_URL" ]; then
echo "::notice::GARMIN_SDK_BUNDLE_URL secret not set; skipping Garmin build."
echo "::notice::See tools/bundle-ciq-sdk-for-ci.ps1 to enable."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Checkout tag
if: steps.precheck.outputs.skip == 'false'
uses: actions/checkout@v4
with:
ref: ${{ needs.meta.outputs.tag }}
fetch-depth: 1
- name: Set up JDK 17
if: steps.precheck.outputs.skip == 'false'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Download CIQ SDK bundle
if: steps.precheck.outputs.skip == 'false'
run: |
mkdir -p /tmp/ciq
curl -fL --retry 3 -o /tmp/ciq/bundle.zip "$GARMIN_SDK_BUNDLE_URL"
unzip -q /tmp/ciq/bundle.zip -d /tmp/ciq
test -x /tmp/ciq/ciq-bundle/sdk/bin/monkeyc || chmod +x /tmp/ciq/ciq-bundle/sdk/bin/monkeyc /tmp/ciq/ciq-bundle/sdk/bin/monkeydo
# monkeyc hard-codes ~/.Garmin/ConnectIQ/Devices as the device-
# profile root — it doesn't honour GARMIN_DEVICE_DIR or any
# equivalent flag. Symlink the bundled devices into the expected
# location so monkeyc finds every manifest entry, otherwise it
# errors with "Device ids ... not recognized" for everything in
# garmin-watch-app/manifest.xml.
mkdir -p "$HOME/.Garmin/ConnectIQ"
ln -sfn /tmp/ciq/ciq-bundle/devices "$HOME/.Garmin/ConnectIQ/Devices"
- name: Generate one-off developer key
if: steps.precheck.outputs.skip == 'false'
run: |
openssl genrsa -out /tmp/ciq/dev_key.pem 4096
openssl pkcs8 -topk8 -inform PEM -outform DER -in /tmp/ciq/dev_key.pem -out /tmp/ciq/dev_key.der -nocrypt
- name: Build .iq + per-device .prg files
if: steps.precheck.outputs.skip == 'false'
run: |
mkdir -p /tmp/out
export PATH="/tmp/ciq/ciq-bundle/sdk/bin:$PATH"
export CIQ_HOME=/tmp/ciq/ciq-bundle/sdk
export GARMIN_DEVICE_DIR=/tmp/ciq/ciq-bundle/devices
IQ_OUT="/tmp/out/garmin-${{ needs.meta.outputs.version }}.iq"
monkeyc \
-f garmin-watch-app/monkey.jungle \
-o "$IQ_OUT" \
-y /tmp/ciq/dev_key.der \
-e \
-w
ls -la "$IQ_OUT"
# Build each per-device .prg into /tmp/prgs first, then zip
# the lot into a single garmin-<version>-prgs.zip — uploading
# 17 individual files clutters the release page and most
# riders only need the .iq above. Power-users who specifically
# want a single device's .prg can unzip locally.
mkdir -p /tmp/prgs
for d in venu2 venu3 fenix843mm fenix847mm fenix6xpro fenix8solar47mm epix2pro47mm instinct2 instinct2s instinct2x instinct3amoled45mm instinct3amoled50mm instinct3solar45mm instinctcrossover instinctcrossoveramoled instincte40mm instincte45mm; do
OUT="/tmp/prgs/garmin-${{ needs.meta.outputs.version }}-${d}.prg"
echo "--- building $d ---"
monkeyc \
-f garmin-watch-app/monkey.jungle \
-o "$OUT" \
-y /tmp/ciq/dev_key.der \
-d "$d" \
-w
done
(cd /tmp/prgs && zip -q "/tmp/out/garmin-${{ needs.meta.outputs.version }}-prgs.zip" *.prg)
ls -la /tmp/out
- name: Stage for publish
if: steps.precheck.outputs.skip == 'false'
uses: actions/upload-artifact@v4
with:
name: stage-garmin
path: /tmp/out/*
retention-days: 1
publish:
runs-on: ubuntu-latest
needs: [meta, android, garmin]
# Run even if garmin was skipped, as long as android succeeded.
if: always() && needs.android.result == 'success'
steps:
- name: Download staged artifacts
uses: actions/download-artifact@v4
with:
path: /tmp/stage
pattern: stage-*
merge-multiple: true
- name: Replace prior artifacts on the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
tag="${{ needs.meta.outputs.tag }}"
# Delete only the canonical-named assets we're about to re-upload,
# so manually-uploaded extras on the same release aren't touched.
for f in /tmp/stage/*; do
name=$(basename "$f")
gh release delete-asset "$tag" "$name" --yes 2>/dev/null || true
done
gh release upload "$tag" /tmp/stage/* --clobber
ls -la /tmp/stage
- name: Announce release on Telegram
# Posts the release into the @EUCPlanetApp "Github" topic. Opt-in via the
# TELEGRAM_* secrets; a Telegram hiccup never fails the release (|| true).
# github.event / notes are passed via env (never inlined) to avoid
# expression injection.
env:
BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT_ID: ${{ secrets.TELEGRAM_RELEASES_CHAT_ID }}
THREAD_ID: ${{ secrets.TELEGRAM_RELEASES_THREAD_ID }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
SERVER_URL: ${{ github.server_url }}
TAG: ${{ needs.meta.outputs.tag }}
VERSION: ${{ needs.meta.outputs.version }}
run: |
if [ -z "$BOT_TOKEN" ] || [ -z "$CHAT_ID" ] || [ -z "$THREAD_ID" ]; then
echo "::notice::Telegram secrets not set; skipping announcement."; exit 0
fi
notes=$(gh release view "$TAG" --json body -q .body 2>/dev/null || true)
url="${SERVER_URL}/${GH_REPO}/releases/tag/${TAG}"
esc() { printf '%s' "$1" | sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g'; }
text="🚀 <b>EUC Planet $(esc "$VERSION")</b> released
$(esc "$notes")
⬇️ Download (phone, Wear OS, HUD, Garmin):
${url}
⌚ Garmin on the Connect IQ Store:
https://apps.garmin.com/apps/630e5d32-637d-4612-84e3-35e6d0bbee10"
curl -sS -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
--data-urlencode "chat_id=${CHAT_ID}" \
-d "message_thread_id=${THREAD_ID}" \
--data-urlencode "text=${text}" \
-d "parse_mode=HTML" -d "disable_web_page_preview=true" \
-o /dev/null -w "Telegram HTTP %{http_code}\n" || true