fix(history-viewer): bigger prediction markers with halo #33
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |