wake-alarm/.github/workflows/release-apk.yml
Krzysztof kuhy Rudnicki c3c129b068 Make the morning session the carrot: earn 11:00 by finishing it in time
On 2026-09-19 the phone was powered off after the bathroom tag and nothing
on the PC noticed: every finished session was published and nothing read
it. Now wake-alarm distils the phone's morning into an HMAC-signed
~/.local/state/wake_alarm/morning_session.json (wake-alarm-session.timer,
07:00-11:00 every 2 min) that screen-locker and leetcode-guard read to
defer their locks -- provisionally until min(start + 90 min, 09:30) while
the session is live, until 11:00 once it completed inside that cutoff,
and not at all for a session finished later (the phone switched on at
noon completes the legs then; that must un-lock nothing).

The phone publishes the session at start as well as at finish
(current_session, cleared with the finish stamp), so the PC can tell
"started, not finished" from "never started". A required 1-5 energy tap
joins the weigh-in and ships as latest_energy with a PC reader mirroring
_weight (the shared half is _field_reader).

Also fixes the vmbox harness, red since the 2026-09-11 home-tidy: the
guest tree lives at ~/wake-alarm, and the INSTALL-OK grep matched vm's
echo of the command line. Takes Flutter 3.47.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp54ewdXRLaimUuT8LgYyn
2026-09-19 16:40:51 +02:00

191 lines
7.9 KiB
YAML

name: release-apk
on:
push:
branches: [main]
# Lets a release be rebuilt from the current main without a dummy commit.
workflow_dispatch:
# gh release create needs write access to the repo's contents.
permissions:
contents: write
# Releases are tag-anchored; two concurrent runs would race on the same tag.
concurrency:
group: release-apk-${{ github.ref }}
cancel-in-progress: false
# NOTE: versionCode is the commit count (`git rev-list --count HEAD`), which
# only ever increases and tracks the code rather than the CI file. It replaced
# github.run_number on 2026-08-09: run_number counts workflow RUNS, so it
# resets to 1 if this file is renamed AND it can fall behind a build made
# outside this workflow — which happened, leaving CI unable to install over a
# manually sideloaded APK (INSTALL_FAILED_VERSION_DOWNGRADE). The checkout
# below therefore needs fetch-depth: 0; a shallow clone counts 1 commit.
#
# This job is NOT gated on a test suite: this repo has no Flutter analyze/test
# job to depend on, so a broken commit still publishes. The build failing is
# the only backstop. Add `needs:` here if a Flutter CI job is ever added.
jobs:
release:
name: release (APK)
runs-on: ubuntu-latest
defaults:
run:
working-directory: phone_app
steps:
- uses: actions/checkout@v4
with:
# versionCode is the commit count; a shallow clone would report 1.
fetch-depth: 0
# `.versioncode-offset` is the recovery valve for a phone stranded
# above CI by a hand-numbered local build. Android only ever compares
# versionCode upward, so a device ahead of CI locks CI out permanently
# and the only other escape is an uninstall, which wipes app data.
# ~/.claude/scripts/lib/phone_deploy_version.sh reads the SAME file, so
# raising it moves both sides together; raising either alone re-strands
# the device. Never lower it.
- name: Derive versionCode from the commit count plus the offset
id: vc
# working-directory overrides the job default of phone_app. The offset
# file lives at the REPO ROOT, and reading it from the app subdirectory
# silently finds nothing and yields 0 -- git rev-list still works there
# because git searches upward, so the step goes green with the wrong
# number. That exact mistake shipped versionCode 108 instead of 209 on
# 2026-08-31; it is also the failure phone_deploy_version.sh warns
# about on its own side.
working-directory: ${{ github.workspace }}
run: |
offset=0
if [[ -r .versioncode-offset ]]; then
read -r offset < .versioncode-offset
fi
[[ "$offset" =~ ^[0-9]+$ ]] || offset=0
# Fail loudly rather than mint a too-low code: a silent 0 here is
# what leaves the phone unreachable by CI.
if [[ -r .versioncode-offset && "$offset" == 0 ]]; then
echo "::error::.versioncode-offset exists but parsed as 0" >&2
exit 1
fi
echo "code=$(( $(git rev-list --count HEAD) + offset ))" >> "$GITHUB_OUTPUT"
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- uses: subosito/flutter-action@v2
with:
flutter-version: 3.47.5
channel: stable
cache: true
- name: Read version from pubspec
id: ver
run: |
set -euo pipefail
# 1.0.0+1 -> 1.0.0; the +build part is replaced by the run number.
raw=$(grep -m1 -E '^version:' pubspec.yaml | sed -E 's/^version:[[:space:]]*//')
semver=${raw%%+*}
if ! echo "$semver" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::pubspec version '$raw' is not X.Y.Z"; exit 1
fi
echo "semver=$semver" >> "$GITHUB_OUTPUT"
# One step, no echoes, printf-into-file rather than echo: the key must
# never reach the log. storeFile is ABSOLUTE because key.properties is
# read from android/ while file() resolves against android/app/.
- name: Materialise the release keystore
env:
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASS: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
if [ -z "$KEYSTORE_B64" ]; then
echo "::error::ANDROID_KEYSTORE_BASE64 is not set; refusing to publish."
exit 1
fi
umask 077
ks="$GITHUB_WORKSPACE/phone_app/android/app/kuhy-release.jks"
printf '%s' "$KEYSTORE_B64" | base64 -d > "$ks"
{
printf 'storeFile=%s\n' "$ks"
printf 'storePassword=%s\n' "$STORE_PASS"
printf 'keyAlias=%s\n' "$KEY_ALIAS"
printf 'keyPassword=%s\n' "$KEY_PASS"
} > "$GITHUB_WORKSPACE/phone_app/android/key.properties"
ls -l "$ks"
- run: flutter pub get
- name: Build signed APK
run: |
flutter build apk --release \
--build-name="${{ steps.ver.outputs.semver }}" \
--build-number="${{ steps.vc.outputs.code }}"
# Fail closed. `findByName("release") ?: debug` silently falls back to
# the debug key if key.properties is misplaced, producing a green build
# that can never be installed over. This is the check that catches it.
- name: Verify the APK is release-signed
run: |
set -euo pipefail
apk=build/app/outputs/flutter-apk/app-release.apk
signer=$(ls -d "$ANDROID_SDK_ROOT"/build-tools/* | sort -V | tail -1)/apksigner
"$signer" verify --print-certs "$apk" | tee /tmp/certs.txt
if grep -qi 'CN=Android Debug' /tmp/certs.txt; then
echo "::error::APK is debug-signed; not publishing."; exit 1
fi
if ! grep -q 'CN=kuhy' /tmp/certs.txt; then
echo "::error::APK is not signed with the kuhy release key."; exit 1
fi
- name: Shred the signing material
if: always()
run: |
rm -f "$GITHUB_WORKSPACE/phone_app/android/key.properties" \
"$GITHUB_WORKSPACE/phone_app/android/app/kuhy-release.jks"
- name: Name the asset
id: art
run: |
set -euo pipefail
out="wake-alarm-${{ steps.ver.outputs.semver }}-build.${{ steps.vc.outputs.code }}.apk"
cp build/app/outputs/flutter-apk/app-release.apk "$out"
echo "path=$out" >> "$GITHUB_OUTPUT"
- name: Publish the per-commit prerelease
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="v${{ steps.ver.outputs.semver }}-build.${{ steps.vc.outputs.code }}"
gh release create "$tag" "${{ steps.art.outputs.path }}" \
--prerelease \
--target "${{ github.sha }}" \
--title "$tag" \
--notes "Automated build of ${{ github.sha }} (versionCode ${{ steps.vc.outputs.code }})."
# Cut a clean vX.Y.Z the first time a pubspec version is seen. A failing
# `gh release view` is the "not published yet" signal, which also covers
# the first-ever run on a repo with no releases.
- name: Publish the clean version release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="v${{ steps.ver.outputs.semver }}"
if gh release view "$tag" >/dev/null 2>&1; then
echo "$tag already exists; leaving it as-is."; exit 0
fi
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then
echo "Tag $tag exists without a release; refusing to clobber."; exit 0
fi
gh release create "$tag" "${{ steps.art.outputs.path }}" \
--target "${{ github.sha }}" \
--title "$tag" \
--notes "Release $tag (versionCode ${{ steps.vc.outputs.code }})."