diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d4e97a..864e3ca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -61,6 +61,21 @@ android { applicationIdSuffix = ".debug" isMinifyEnabled = false } + // A locally-installable twin of `release`: same R8 shrinking + obfuscation + // and resource shrinking, but debug-signed and given its own applicationId + // suffix so it installs alongside both the production app (signed with the + // real key) and the debug build. Used to smoke-test a release candidate on + // a real device before tagging — R8-only breakage and first-run/permission + // states don't surface in the unminified debug build, nor on a device that + // already holds the permission. Never published. See docs/RELEASING.md. + create("releaseTest") { + initWith(getByName("release")) + applicationIdSuffix = ".releasetest" + signingConfig = signingConfigs.getByName("debug") + isMinifyEnabled = true + isShrinkResources = true + matchingFallbacks += "release" + } } compileOptions { diff --git a/docs/RELEASING.md b/docs/RELEASING.md index f268511..f4f82b0 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -22,20 +22,44 @@ Published version codes so far: `v0.1.0`→100 … `v1.0.0`→10000 … `v2.0.0` ## Cutting a release -1. Move the `## [Unreleased]` section of `CHANGELOG.md` under a new +1. **Assemble the release branch.** Create `release/vX.Y.Z` and merge the + feature/fix branches that make up this release into it. This branch is the + release candidate — everything below happens on it, before it reaches `main`. +2. Move the `## [Unreleased]` section of `CHANGELOG.md` under a new `## [X.Y.Z] — ` heading (Keep a Changelog format). The text between that heading and the next `## [` becomes both the Gitea release notes and the F-Droid per-version changelog. -2. Optionally bump the committed `versionCode`/`versionName` in - `app/build.gradle.kts` to match the new version (keeps local builds tidy). -3. Commit, then tag and push: +3. Bump the committed `versionCode`/`versionName` in `app/build.gradle.kts` to + match the new version (keeps local builds tidy; CI overwrites from the tag). +4. **Verify the release build on a real device** — the mandatory gate. The + shipped APK is R8-shrunk/obfuscated, and bugs that only appear there, or + only on first run, never show up in the debug build or on a device that + already granted the calendar permission (this is how the v2.7.0 launch + crash slipped through). Run: + ```bash + scripts/verify-release.sh + ``` + It builds the `releaseTest` variant (same R8 config as `release`, debug-signed + with a `.releasetest` suffix so it installs alongside the real app) and resets + it to a first-run state. Then, on the device: + - launch from a **clean / permission-not-granted** state — the permission + screen must appear, no crash; + - grant access — the calendar must load; + - add both home-screen **widgets** and confirm they render; + - exercise this release's headline changes. + + Only proceed once all of that passes on-device. +5. Merge `release/vX.Y.Z` into `main`, then tag and push: ```bash git tag vX.Y.Z git push origin vX.Y.Z ``` -4. The push triggers the release workflow. **Hold UI releases for on-device +6. The push triggers the release workflow. **Hold UI releases for on-device review and explicit go-ahead before tagging.** +> The `releaseTest` build type exists only for step 4 — it is never published. +> The pipeline always builds and signs the real `release` variant from the tag. + ## What the pipeline does `release.yaml` has three jobs: diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh new file mode 100755 index 0000000..01baaa1 --- /dev/null +++ b/scripts/verify-release.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# Build the release-candidate APK and install it on a connected device for the +# mandatory pre-tag on-device check (see docs/RELEASING.md). +# +# It builds the `releaseTest` variant: the same R8 shrinking + obfuscation and +# resource shrinking as the published `release` build, but debug-signed and +# with a `.releasetest` applicationId suffix so it installs alongside the +# production and debug apps. This is what surfaces release-only breakage (R8 +# stripping) and first-run states (permission not yet granted) that the +# unminified debug build — or a device that already holds the permission — +# silently hides. +# +# Usage: scripts/verify-release.sh +set -euo pipefail + +cd "$(dirname "$0")/.." + +PKG="de.jeanlucmakiola.calendula.releasetest" +APK="app/build/outputs/apk/releaseTest/app-releaseTest.apk" + +echo "==> Building release-candidate APK (releaseTest, R8 minified)…" +./gradlew :app:assembleReleaseTest + +echo "==> Installing $PKG …" +adb install -r "$APK" + +echo "==> Resetting to a first-run state (revoking calendar permission)…" +# The release build crashed at launch precisely because this state was never +# tested. Force it so the permission gate / onboarding is exercised every time. +adb shell pm revoke "$PKG" android.permission.READ_CALENDAR 2>/dev/null || true +adb shell pm revoke "$PKG" android.permission.WRITE_CALENDAR 2>/dev/null || true + +echo +echo "Installed and reset. Now verify ON THE DEVICE before tagging:" +echo " 1. Launch from a clean state — the permission screen must appear (no crash)." +echo " 2. Grant calendar access — the calendar must load with events." +echo " 3. Add both home-screen widgets and confirm they render (not a spinner)." +echo " 4. Exercise the release's headline changes end to end." +echo +echo "Watch for crashes with: adb logcat -b crash" +echo "Only tag the release once all of the above pass on a real device."