Skip to content

Commit 2d00446

Browse files
committed
ci: add release roborazzi gate
1 parent a75091a commit 2d00446

13 files changed

Lines changed: 136 additions & 35 deletions

File tree

.github/workflows/release.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ name: Release
22

33
# ROADMAP §6 N6.2 — manual-trigger release workflow:
44
# 1. Requires the reproducible-build verifier to pass for the same commit.
5-
# 2. Builds the release variant.
6-
# 3. Signs with the keystore stored as an encrypted secret.
7-
# 4. Uploads the signed APK + SHA-256 manifest to a GitHub Release.
8-
# 5. NOTICE / LICENSES attribution check (Apache-2.0 + SCOWL + LDNOOBW + FlorisBoard upstream).
5+
# 2. Runs release-variant Roborazzi screenshot verification.
6+
# 3. Builds the release variant.
7+
# 4. Signs with the keystore stored as an encrypted secret.
8+
# 5. Uploads the signed APK + SHA-256 manifest to a GitHub Release.
9+
# 6. NOTICE / LICENSES attribution check (Apache-2.0 + SCOWL + LDNOOBW + FlorisBoard upstream).
910
#
1011
# Required repo secrets (set up under Settings → Secrets and variables → Actions):
1112
# SIGNING_KEYSTORE_BASE64 — base64-encoded contents of the .jks file
@@ -91,6 +92,15 @@ jobs:
9192
- name: Run unit tests
9293
run: ./gradlew :app:testDebugUnitTest
9394

95+
# F24 — release resources / BuildConfig can drift from the debug visual
96+
# gate. The app module exposes :app:verifyRoborazziRelease as a stable
97+
# alias backed by the non-shipping releaseRoborazzi variant, which
98+
# initWith(release) but carries only the screenshot host overlay. Keep
99+
# this before APK signing/publication so a release-only screenshot
100+
# regression blocks the dispatch.
101+
- name: Roborazzi visual-regression verify (release variant)
102+
run: ./gradlew :app:verifyRoborazziRelease
103+
94104
- name: Lint debug
95105
run: ./gradlew :app:lintDebug
96106

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,51 @@
22

33
All SwiftFloris release history is consolidated here. This replaces the former root-level `RELEASE_NOTES_v*.md` file-per-release pattern.
44

5+
<a id="v1.8.213"></a>
6+
## v1.8.213
7+
8+
Released: 2026-06-04
9+
10+
### Release Roborazzi gate
11+
12+
F24 adds a non-shipping `releaseRoborazzi` build type that `initWith(release)` and carries only the Robolectric screenshot-host manifest overlay. A stable `:app:verifyRoborazziRelease` alias runs Roborazzi against that release-equivalent variant, and the manual Release workflow now runs the alias before lint, APK assembly, signing, artifact upload, or GitHub Release creation.
13+
14+
This gives release dispatches their own screenshot-baseline check against release resources and build flags, closing the gap where the PR/push gate only exercised `debug`.
15+
16+
### Changes
17+
18+
- **`app/build.gradle.kts`** - adds the non-shipping `releaseRoborazzi` build type, enables its UnitTest component, and exposes a stable `verifyRoborazziRelease` alias.
19+
- **`app/src/releaseRoborazzi/AndroidManifest.xml`** - declares the screenshot host only for the release-equivalent Roborazzi variant, keeping the real release manifest unchanged.
20+
- **`release.yml`** - adds a required release-variant Roborazzi step before signing/publication.
21+
- **`README.md` / `LOCAL_VERIFICATION.md`** - document the release visual gate next to the existing debug gate.
22+
- **`ROADMAP.md` / `COMPLETED.md`** - moved F24 out of active work and into shipped state.
23+
24+
### Verification
25+
26+
- `./gradlew.bat :app:tasks --all` -> exposes `verifyRoborazziRelease` with `JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot`.
27+
- `./gradlew.bat :app:verifyRoborazziRelease` -> green with the same `JAVA_HOME`.
28+
- `./gradlew.bat :app:verifyNoInternetPermission :app:testDebugUnitTest :app:lintDebug :app:assembleDebug` -> green with the same `JAVA_HOME`.
29+
- `./gradlew.bat :app:verifyRoborazziDebug` -> green with the same `JAVA_HOME`.
30+
- `./gradlew.bat :app:assembleRelease` -> green with the same `JAVA_HOME`.
31+
- `aapt2 dump xmltree --file AndroidManifest.xml app/build/outputs/apk/release/app-release.apk` -> confirms the real release manifest excludes `RoborazziHostActivity`.
32+
- `git diff --check` -> green; CRLF normalization warnings only.
33+
- `bash scripts/check-fastlane-metadata.sh` -> green for versionCode 2013.
34+
- `bash scripts/check-repo-hygiene.sh` -> green.
35+
36+
### Files Touched
37+
38+
- `.github/workflows/release.yml`
39+
- `app/build.gradle.kts`
40+
- `app/src/releaseRoborazzi/AndroidManifest.xml` (new)
41+
- `app/src/main/kotlin/dev/patrickgold/florisboard/screenshot/RoborazziHostActivity.kt`
42+
- `app/src/test/AndroidManifest.xml`
43+
- `app/src/test/kotlin/dev/patrickgold/florisboard/screenshot/ExtensionMaintainerChipScreenshotTest.kt`
44+
- `docs/LOCAL_VERIFICATION.md`
45+
- `fastlane/metadata/android/en-US/changelogs/2013.txt` (new)
46+
- `gradle.properties` (versionCode 2012->2013, versionName 1.8.212->1.8.213)
47+
- `README.md` (version badge/current release/verification docs)
48+
- `ROADMAP.md` / `COMPLETED.md` (F24 moved to shipped work)
49+
550
<a id="v1.8.212"></a>
651
## v1.8.212
752

COMPLETED.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Consolidated from the archived open-work checklist (closed items). Full per-rele
5151
- [x] F6 (P2) — Per-app accent opt-in discovery hint and Settings preview, with process-local three-app threshold tracking, persisted hint state only, Settings search coverage, and privacy docs. Shipped v1.8.210. — *Source: TODO_2026-06-03.md*
5252
- [x] EI2 (P3, reframed) — Settings home regrouped into Typing experience, Personalization, Privacy & data, Advanced, and About buckets, with Physical keyboard surfaced directly under Advanced while preserving existing deep links. Shipped v1.8.211. — *Source: TODO_2026-06-03.md*
5353
- [x] F23 (P1) — Release workflow now depends on the reusable reproducible-build verifier before signing or GitHub Release publication, blocking release dispatches when the build-twice APK check fails. Shipped v1.8.212. — *Source: TODO_2026-06-03.md*
54+
- [x] F24 (P1) — Non-shipping `releaseRoborazzi` variant mirrors release build flags for Roborazzi, exposed through `:app:verifyRoborazziRelease`; the release workflow runs it before APK signing/publication. Shipped v1.8.213. — *Source: TODO_2026-06-03.md*
5455
- [x] F1, F2, F15, F16, F17, F19, F20, F25, F26, F32, F34, F35, F36, F41, F42, EI8, EI11, EI4 (doc) — Closed across v1.8.174 -> v1.8.187. — *Source: TODO_2026-06-03.md*
5556
- [x] IMPROVEMENT_PLAN Workstreams 1, 3, 4, 5, 6 complete; Workstream 2 (lint) monotonically decreasing. — *Source: IMPROVEMENT_PLAN_2026-05-18.md*
5657

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SwiftFloris
22

3-
![Version](https://img.shields.io/badge/version-v1.8.212-blue) ![License](https://img.shields.io/badge/license-Apache%202.0-green) ![Platform](https://img.shields.io/badge/platform-Android%208.0+-orange) ![Network](https://img.shields.io/badge/network-none-lightgrey) ![Dictionary imports](https://img.shields.io/badge/dictionary%20imports-local%20files-green)
3+
![Version](https://img.shields.io/badge/version-v1.8.213-blue) ![License](https://img.shields.io/badge/license-Apache%202.0-green) ![Platform](https://img.shields.io/badge/platform-Android%208.0+-orange) ![Network](https://img.shields.io/badge/network-none-lightgrey) ![Dictionary imports](https://img.shields.io/badge/dictionary%20imports-local%20files-green)
44

55
**SwiftFloris** is a privacy-first Android keyboard, forked from FlorisBoard and pushed toward SwiftKey-class multilingual typing without the cloud. It ships under Apache-2.0, holds no `INTERNET` permission, and binds zero accounts.
66

@@ -37,7 +37,7 @@
3737
3838
## Highlights
3939

40-
| Area | What's in v1.8.212 | Privacy posture |
40+
| Area | What's in v1.8.213 | Privacy posture |
4141
|------|-------------------|-----------------|
4242
| **Autocorrect / prediction** | SCOWL 117k English dictionary, SymSpell d1+d2, bigram + trigram next-word, capitalization-aware completions, contraction handling, instant-remember user-dictionary overlay | On-device |
4343
| **Multilingual typing** | Bilingual subtype presets (EN+ES / EN+FR / EN+DE), per-token Latin language identification, top-two straddle guard, sentence-local context scoring, and opt-in remembered keyboard language per app | On-device |
@@ -185,6 +185,9 @@ The IME's main work lives under `app/src/main/kotlin/dev/patrickgold/florisboard
185185

186186
# Roborazzi screenshot verify (visual-regression CI)
187187
./gradlew :app:verifyRoborazziDebug
188+
189+
# Release-variant Roborazzi gate (run before publishing)
190+
./gradlew :app:verifyRoborazziRelease
188191
```
189192

190193
**Signed release build**
@@ -269,7 +272,7 @@ Real device-number collection is tracked in [`docs/BENCHMARKS.md`](docs/BENCHMAR
269272
## Testing
270273

271274
- **Unit tests:** Kotest, run with `./gradlew test`. Last reported HEAD: 998+ tests (post-v1.8.40), expanding with each release. The v1.8.47 hardening pass added defensive tests around dictionary import limits, voice-model atomic install, theme asset traversal, and quick-action serializer fallback.
272-
- **Visual regression:** Roborazzi 1.60.0, plugin alias active. CI runs `:app:verifyRoborazziDebug` on every push / PR as a hard gate, backed by committed baselines for the maintainer chip, SwiftKey High Contrast, Aurora Animated, and Settings -> Addons surfaces.
275+
- **Visual regression:** Roborazzi 1.60.0, plugin alias active. CI runs `:app:verifyRoborazziDebug` on every push / PR as a hard gate, and the release workflow runs `:app:verifyRoborazziRelease` before APK publication. Baselines cover the maintainer chip, SwiftKey High Contrast, Aurora Animated, and Settings -> Addons surfaces.
273276
- **Macrobenchmark:** `:benchmark` is wired for AndroidX trace/frame runs, and the adb harness scripts record repeatable IME first-render, first-suggestion, dictionary-load, candidate-row recomposition, theme-switch, and backup/restore baselines.
274277
- **No-network gate:** CI verifies the absence of `INTERNET` permission on every build.
275278
- **Lint drift:** CI lint runs through `scripts/run-lint-debug-with-baseline-check.sh`, which fails stale baseline entries instead of leaving them as console-only noise.
@@ -442,7 +445,7 @@ limitations under the License.
442445

443446
## Status
444447

445-
🚀 **Active development.** Current release: **v1.8.212** (2026-06-04). The SwiftKey account export window closed on **2026-05-31**; local/on-device migration paths remain documented above.
448+
🚀 **Active development.** Current release: **v1.8.213** (2026-06-04). The SwiftKey account export window closed on **2026-05-31**; local/on-device migration paths remain documented above.
446449

447450
---
448451

ROADMAP.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> Single source of truth for all planned work. Items above the --- are existing plans; items below are research conducted 2026-06-03.
44
5-
**Current release:** v1.8.212 (versionCode 2012). **Baseline green:** `:app:verifyNoInternetPermission :app:testDebugUnitTest :app:lintDebug :app:assembleDebug`.
5+
**Current release:** v1.8.213 (versionCode 2013). **Baseline green:** `:app:verifyNoInternetPermission :app:testDebugUnitTest :app:lintDebug :app:assembleDebug`.
66

77
Hard rules still apply (see `AGENTS.md`): no `INTERNET` permission in `:app`; Apache-2.0 ceiling on `:app`; no closed-source blobs; one logical change per commit; every shipped release bumps `gradle.properties` version, writes a `CHANGELOG.md` section, and adds a `fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` (draft <=480 chars for headroom).
88

@@ -62,11 +62,6 @@ Item IDs trace to their origin research: `F#`/`EI#` from the archived 2026-05-25
6262

6363
### CI, build & release hardening
6464

65-
- [ ] P1 — `:app:verifyRoborazziRelease` gate (F24)
66-
- Why: R8/minify can rename Compose semantics nodes and nothing catches it today.
67-
- Touches: add a release-variant Roborazzi verification task.
68-
- Acceptance: release-variant screenshot gate runs and catches minify-induced semantics drift.
69-
- Source: TODO.md A4 / research feature plan F24.
7065
- [ ] P2 — Macrobenchmark trend-regression job (EI9)
7166
- Why: No automated regression check against benchmark baselines.
7267
- Touches: `workflow_dispatch` job diffing against `docs/benchmark-results/baseline-*.json`; floor/target ranges documented in `docs/BENCHMARKS.md`.

app/build.gradle.kts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ plugins {
2828
alias(libs.plugins.kotlinx.kover)
2929
// Roborazzi 1.55.0 (Jan 2026 line) ships AGP-9 support via PR #782,
3030
// so the Gradle plugin is now applied. This lights up the
31-
// `:app:recordRoborazziDebug` (baseline capture) and
32-
// `:app:verifyRoborazziDebug` (regression verify) tasks so CI can
33-
// gate every PR on a visual diff. Baseline images live under
31+
// `:app:recordRoborazziDebug` (baseline capture),
32+
// `:app:verifyRoborazziDebug` (PR/push regression verify), and the
33+
// `:app:verifyRoborazziRelease` alias backed by the non-shipping
34+
// releaseRoborazzi variant so the release workflow can run the same
35+
// baselines against release resources/build flags before publishing.
36+
// Baseline images live under
3437
// `app/src/test/snapshots/images/` per Roborazzi convention.
3538
alias(libs.plugins.roborazzi)
3639
}
@@ -166,6 +169,16 @@ configure<ApplicationExtension> {
166169
signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug")
167170
}
168171

172+
create("releaseRoborazzi") {
173+
initWith(getByName("release"))
174+
matchingFallbacks += listOf("release")
175+
176+
// F24: non-shipping release-equivalent variant used only by
177+
// :app:verifyRoborazziRelease. Its source-set overlay declares the
178+
// screenshot host activity that Robolectric needs; the real release
179+
// APK manifest stays untouched.
180+
}
181+
169182
create("benchmark") {
170183
initWith(getByName("release"))
171184

@@ -317,6 +330,15 @@ afterEvaluate {
317330
// is caught. Wired against AGP's SingleArtifact.MERGED_MANIFEST so the task
318331
// runs after the manifest merger and before assemble.
319332
androidComponents {
333+
beforeVariants(selector().withBuildType("releaseRoborazzi")) { variantBuilder ->
334+
// F24: AGP only creates the debug unit-test component by default for
335+
// application modules. Roborazzi wires tasks from AGP UnitTest
336+
// components, so explicitly enable unit tests on the non-shipping
337+
// releaseRoborazzi variant and expose it through the stable
338+
// :app:verifyRoborazziRelease alias below.
339+
(variantBuilder as com.android.build.api.variant.HasUnitTestBuilder).enableUnitTest = true
340+
}
341+
320342
onVariants { variant ->
321343
val verifyMerged = tasks.register("verifyNoInternetPermissionMerged${variant.name.replaceFirstChar { it.uppercase() }}") {
322344
group = "verification"
@@ -358,6 +380,12 @@ androidComponents {
358380
}
359381
}
360382

383+
tasks.register("verifyRoborazziRelease") {
384+
group = "verification"
385+
description = "Runs Roborazzi against the non-shipping releaseRoborazzi variant, which mirrors release build flags and carries only the test host overlay (F24)."
386+
dependsOn("verifyRoborazziReleaseRoborazzi")
387+
}
388+
361389
// ROADMAP §6 N7.4 — pin the load-bearing excludes in
362390
// `app/src/main/res/xml/data_extraction_rules.xml` against accidental
363391
// rewrite. Android Lint validates the XML against the data-extraction-rules

app/src/main/kotlin/dev/patrickgold/florisboard/screenshot/RoborazziHostActivity.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ import androidx.activity.ComponentActivity
2929
* Activity carries the full nav graph). This shim exists purely so
3030
* the screenshot tests can launch an empty `ComponentActivity` host.
3131
*
32-
* Declared only under the **debug** manifest overlay
33-
* (`app/src/debug/AndroidManifest.xml`), so it never ships in release
34-
* builds and never widens the production attack surface. Set
35-
* `exported=false` so no external app can launch it even on debug.
32+
* Declared only under the **debug** and **releaseRoborazzi** manifest overlays,
33+
* so it never ships in release builds and never widens the production attack
34+
* surface. Set `exported=false` so no external app can launch it even on debug.
3635
*/
3736
class RoborazziHostActivity : ComponentActivity()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
F24 — non-shipping release-equivalent Roborazzi overlay.
4+
5+
The real release APK must not expose the screenshot host activity. The
6+
releaseRoborazzi build type initWith(release) for release resources/build
7+
flags but adds this test host so Robolectric can resolve
8+
createAndroidComposeRule<RoborazziHostActivity>().
9+
-->
10+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
11+
<application>
12+
<activity
13+
android:name="dev.patrickgold.florisboard.screenshot.RoborazziHostActivity"
14+
android:exported="false"/>
15+
</application>
16+
</manifest>

app/src/test/AndroidManifest.xml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
<!--
33
ROADMAP §7 Next-12.2a — Test-only AndroidManifest overlay.
44
5-
Robolectric's `createComposeRule()` is internally backed by
6-
`ActivityScenario.launch(ComponentActivity::class)`, which itself
7-
needs the activity declared in the merged manifest. The :app
8-
module's production manifest only declares the IME service, so
9-
ComponentActivity is "unresolvable" — the same failure shape
10-
documented in https://github.com/robolectric/robolectric/pull/4736.
5+
Robolectric's `createAndroidComposeRule<RoborazziHostActivity>()` is
6+
internally backed by ActivityScenario, which needs the activity declared in
7+
the merged unit-test manifest. The :app module's production manifest only
8+
declares the IME service, so the screenshot host is "unresolvable" unless
9+
this test-only overlay declares it — the same failure shape documented in
10+
https://github.com/robolectric/robolectric/pull/4736.
1111
1212
This test-tier manifest adds a stub declaration so Roborazzi
1313
captures work. The file is *only* picked up by the unit-test
@@ -18,7 +18,7 @@
1818
xmlns:tools="http://schemas.android.com/tools">
1919
<application>
2020
<activity
21-
android:name="androidx.activity.ComponentActivity"
21+
android:name="dev.patrickgold.florisboard.screenshot.RoborazziHostActivity"
2222
android:exported="false"
2323
tools:node="merge">
2424
<intent-filter>

app/src/test/kotlin/dev/patrickgold/florisboard/screenshot/ExtensionMaintainerChipScreenshotTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ import org.robolectric.annotation.GraphicsMode
6060
// still representative because Roborazzi snapshots are layout-driven
6161
// rather than API-level-dependent.
6262
//
63-
// Next-12.2a fix: the test-only AndroidManifest at app/src/test/AndroidManifest.xml
64-
// declares the androidx.activity.ComponentActivity launcher Robolectric
65-
// needs to back createComposeRule(). The activity never lands in any
66-
// shipped APK — Robolectric reads it via the test classpath only.
63+
// Next-12.2a/F24 fix: the test-only AndroidManifest at
64+
// app/src/test/AndroidManifest.xml declares RoborazziHostActivity so
65+
// Robolectric can resolve createAndroidComposeRule() for both debug and
66+
// release unit-test variants. The activity never lands in any shipped APK;
67+
// Robolectric reads it via the unit-test classpath only.
6768
@RunWith(AndroidJUnit4::class)
6869
@Config(qualifiers = "w360dp-h640dp-xxhdpi", sdk = [35])
6970
@GraphicsMode(GraphicsMode.Mode.NATIVE)

0 commit comments

Comments
 (0)