Step 1 of M8 (docs/android/PLAN.md item 9).
- compileSdk/targetSdk 36 (Play's floor since 2026-08-31). AGP 8.6.1
cannot build against 36, so AGP goes to 8.13.2 (the last 8.x) with the
Gradle 8.14.3 wrapper. Kotlin, KSP and Hilt are unchanged. All three
workflows install platforms;android-36; AGP 8.13 still defaults to
build-tools 35.0.0.
- release.yml also runs bundleRelease, signed with the same keystore,
and attaches the .aab to the Gitea release beside the APK (the first
bundle has to be uploaded by hand, and this is where it comes from).
- A last step uploads the bundle to Play's internal track through the
Play Developer API v3 edit flow. The OAuth token comes from a
service-account JWT signed with openssl; the key is the new
PLAY_SERVICE_ACCOUNT_JSON secret. While that secret is unset, the
step warns and skips. The step runs after the Gitea release, so a
Play failure never holds back the APK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The push-to-main release model kept failing: the job builds the signed APK
fine, but the final `git push origin HEAD:main` (version-bump commit) is
rejected by main's branch protection — "pre-receive hook declined / Internal
Server Error" — across runs #187, #194. main is deliberately protected
(allowlist push, required approvals, required status checks), which is
fundamentally incompatible with a CI job pushing a fresh commit to it.
Flip the trigger: the workflow now runs on pushing a `v*` tag (or via
workflow_dispatch with a tag input). The tag *is* the release input, so:
- version/versionCode are derived from the tag name (no version-planning engine);
- app/build.gradle.kts is set for the build only, never committed back;
- no `git push` to main, no tag creation, no REGISTRY_USER needed —
only REGISTRY_TOKEN, to create the Gitea release + upload the APK/SHA256SUMS.
To cut a release now: `git tag v0.1.0 && git push origin v0.1.0`.
Keeps the speed fixes from #13 (trimmed setup-android, no Gradle cache,
timeout-minutes). Changelog is still generated from conventional-commit
subjects since the previous tag.
Co-Authored-By: Claude <noreply@anthropic.com>
The release workflow was slow and sometimes appeared to hang mid-build:
- android-actions/setup-android@v3 pulled the entire Android emulator and the
legacy `tools` package (hundreds of MB, network-bound on the self-hosted
runner) that a headless APK build never uses. Pin `packages: ''` so it only
puts cmdline-tools on PATH; the next step installs exactly what we need.
- The `actions/cache@v4` Gradle step timed out every run against the Gitea
artifact-cache backend (`getCacheEntry failed: Request timeout`), adding
latency with no benefit. Dropped it.
- The job had no timeout, and with `concurrency.cancel-in-progress: false` a
genuinely wedged run would hang forever and block every later release behind
it. Added `timeout-minutes: 30` so a hang fails fast.
Co-Authored-By: Claude <noreply@anthropic.com>
Replace the tag-triggered release.yml with link/'s language-agnostic release
engine, adapted for Android. On every push to main it derives the next version
from conventional-commit subjects since the last v* tag (feat!/BREAKING -> major,
feat -> minor, fix|perf -> patch; nothing releasable -> no release), generates a
grouped changelog, bumps versionName in build.gradle.kts (versionCode derived
major*10000+minor*100+patch, monotonic), builds the SIGNED release APK, then
commits the bump [skip ci], tags vX.Y.Z, and creates the Gitea release with the
notes + APK + SHA256SUMS.
Uses REGISTRY_USER/REGISTRY_TOKEN (write:repository) to push the bump + create
the release, matching link/. main must allow that account to push (bump lands on
main; the [skip ci] + head-commit guard prevent a re-trigger loop). Signing
secrets (ANDROID_KEYSTORE_BASE64/_PASSWORD, ANDROID_KEY_ALIAS/_PASSWORD) unchanged.
Same self-hosted-runner handling as pr-checks.yml (apt JDK 17, sdkmanager, chmod).
Co-Authored-By: Claude <noreply@anthropic.com>
Release-hardening pass (PLAN.md §9 M6, §10, §12). No architecture, data-flow,
or endpoint changes; the app remains a pure API client.
App icons (default brand assets):
- New gateway-medallion launcher icon set (all densities, adaptive fg/bg, round,
Play Store icon) + an RG notification icon staged for M7 push.
- Replace the Image Asset wizard's default green-grid adaptive background with the
deep-indigo brand fill (@color/ic_launcher_background #1B1033); recomposite the
legacy square/round webps and the 512 Play icon over indigo so the whole set is
coherent (the green never shipped). Restore the SPDX headers the wizard stripped;
drop the orphaned placeholder foreground vector. No <monochrome> layer — the
full-colour medallion has no clean silhouette, so themed mode falls back to the
standard icon rather than a tinted blob.
Version-mismatch guard (§3):
- The connect probe now refuses a Runic Gateway backend whose API version this
build can't speak (e.g. a future v2) with a clear "app out of date" message,
instead of mis-rendering; lenient on a blank api (older backend). Decision logic
extracted to a pure ConnectionRepository.evaluateVersion() with unit tests.
Release build hardening (§7, §12):
- Enable R8 full-mode minify + resource shrink for release (~31 MB debug -> 4.2 MB
signed release). ProGuard keep-rules for kotlinx.serialization serializers + our
wire DTOs, Retrofit service interfaces, and a -dontwarn for Tink's compile-only
Error Prone annotations (EncryptedSharedPreferences).
- Release signingConfig reads keystore material from a gitignored keystore.properties
or env vars; absent -> unsigned (debug + PR gate unaffected). Keystore never in repo.
- versionName/versionCode overridable via -P so the release tag + CI run number
drive them (§10).
CI:
- release.yml: on a `v*` tag, build a SIGNED release APK (keystore from a base64
Gitea secret) and attach it + SHA256SUMS to a Gitea release; workflow_dispatch is
a signing dry run. Mirrors pr-checks.yml's self-hosted-runner handling (apt JDK 17,
explicit sdkmanager, in-step chmod +x gradlew).
Co-Authored-By: Claude <noreply@anthropic.com>