ci(release): add HealthChat Play Store release pipeline

This commit is contained in:
2026-07-27 18:35:36 +02:00
committed by Sedat Qaja
parent b975611e63
commit 4778fe4a98
14 changed files with 533 additions and 31 deletions
@@ -0,0 +1,240 @@
---
name: releasing-healthchat
description: Cut a HealthChat Google Play release of element-x-android by tagging release/healthchat. Use when the user asks to release, publish, tag, deploy, ship, or cut a new HealthChat Android version, asks about the Play Store release process, asks about rotating the Android release keystore, or asks about the required GitHub Actions secrets for the release pipeline.
---
# Releasing HealthChat (element-x-android)
Releases are driven by git tags of the form `healthchat-v<version>` pushed to
`origin`. The tag triggers `.github/workflows/healthchat-release-playstore.yml`,
which runs on `ubuntu-latest`, builds a signed AAB, and uploads it to the Play
Store **internal** track. No GitHub Release is created.
**Promotion to production is always a manual step** in Play Console after an
internal build has been validated — see "Promoting to production" below.
## Preflight checklist
Verify each item before tagging. If any fail, fix and commit first.
- [ ] On branch `release/healthchat` with a clean working tree (`git status`).
- [ ] `HEALTHCHAT_CHANGES.md` has a section `## Changes in <new-version> (<date>)`
at the top of the file. The pipeline's awk extractor matches this exact
heading format; no match means a fallback placeholder ships as the Play
Store "What's new" text.
- [ ] `plugins/src/main/kotlin/Versions.kt` `versionYear`, `versionMonth`, and
`versionReleaseNumber` produce the tag's core version (format
`<year>.<month zero-padded to 2>.<releaseNumber>`, e.g. `26.07.1`).
**The workflow hard-fails** if they don't match — this is load-bearing,
not cosmetic.
- [ ] If you edited `HEALTHCHAT_CHANGES.md` or `Versions.kt`, both commits are
pushed to `release/healthchat` **before** you push the tag.
## Cutting the release (normal path)
```bash
# from release/healthchat, fully committed:
git tag healthchat-v<YEAR>.<MONTH>.<RELEASE>
git push origin healthchat-v<YEAR>.<MONTH>.<RELEASE>
```
Examples:
- `git tag healthchat-v26.07.1` (next release in July 2026)
- `git tag healthchat-v26.08.0` (first release in August 2026)
- `git tag healthchat-v26.07.2-rc1` (release candidate)
Watch the workflow with `gh run watch` or in the GitHub Actions UI. When it
finishes, the build appears in Play Console → Internal testing within a few
minutes (Play Store's processing queue).
## Manual / retry path (workflow_dispatch)
Use when retrying a failed run or rebuilding without moving tags.
1. GitHub → Actions → "HealthChat Release to Google Play Store" → **Run workflow**.
2. Fill in:
- **version** (required): e.g. `26.07.1`. Same regex validation as tag path.
Must still match `Versions.kt`.
- **track**: default `internal`; pick `alpha`/`beta`/`production` to promote
an already-uploaded build to a higher track without rebuilding (note:
Play Console is usually a better UX for promotion).
- **release_status**: default `completed`; use `draft` for dry-runs that
land in Play Console but stay unpublished.
- **release_notes_override** (optional): replaces the
`HEALTHCHAT_CHANGES.md` lookup. Useful for ad-hoc test builds.
3. Run. The AAB is built and uploaded exactly as the tag path would.
## Versioning (calendar scheme — differs from the old Classic repo)
Element X uses calendar versioning, defined in
`plugins/src/main/kotlin/Versions.kt`:
```
versionName = <versionYear>.<versionMonth padded to 2>.<versionReleaseNumber> // e.g. 26.07.1
versionCode = ((2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber) * 10
```
The trailing `* 10` is the ABI multiplier applied per output APK
(`app/build.gradle.kts` androidComponents block); the AAB gets ABI code `0`.
Example: `26.07.1` → versionCode `202607010`.
To bump: edit `versionYear`/`versionMonth`/`versionReleaseNumber` in
`Versions.kt` (increment `versionReleaseNumber` within a month; reset it to 0
when the month changes). `versionCode` must be strictly monotonic in Play
Store across all uploads — the calendar scheme guarantees this as long as you
never decrease the triple. All HealthChat codes (≥ 202,607,000) supersede the
old Classic app's last code (40,154,100).
**Keep the same version as the upstream Element X release the branch is based
on unless HealthChat ships an intermediate build** — then bump
`versionReleaseNumber` past upstream's. When merging a newer upstream release
into `release/healthchat`, take upstream's `Versions.kt` if its version is
higher.
## Release notes
`HEALTHCHAT_CHANGES.md` is the single source. The workflow extracts the
section matching the version being released, then writes it to
`fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` and
`fastlane/metadata/android/de-DE/changelogs/<versionCode>.txt`, which
`r0adkll/upload-google-play` picks up via its `whatsNewDirectory` input.
Format:
```markdown
## Changes in 26.07.1 (2026-07-20)
🙌 Improvements
- Bullet one.
- Bullet two.
```
Keep the entire section under **500 characters** — Play Store's "What's new"
hard cap per locale. The workflow emits a warning if you exceed it; Play Store
will truncate.
Never hand-edit the generated `fastlane/metadata/android/*/changelogs/<N>.txt`
files for this branch — they'll be overwritten on the next run. Only edit
`HEALTHCHAT_CHANGES.md`. (Older checked-in files from upstream are harmless.)
If no matching section exists, the pipeline emits a workflow warning and
falls back to `HealthChat <version> — automated build from <ref>.`. Prefer
fixing the changelog over relying on the fallback.
## After the release
- [ ] Workflow run is green in GitHub Actions.
- [ ] Build appears under Play Console → Internal testing within ~10 minutes.
- [ ] Internal testers get the update automatically if they're enrolled.
- [ ] AAB + `mapping.txt` are attached to the workflow run, retained 90 days.
## Required GitHub Actions secrets
The pipeline depends on these repository secrets. Missing any causes the
workflow to fail at the corresponding step. The values are identical to the
ones configured on the old `Novaloop-AG/element-android` repository (same
upload keystore, same Play listing) — except `GOOGLE_SERVICES_JSON_BASE64`,
which element-x-android does **not** use (Firebase config is compiled in via
`libraries/pushproviders/firebase/src/main/res/values/firebase.xml`).
| Secret | Source |
|---|---|
| `RELEASE_KEYSTORE_BASE64` | `base64 -w0 release.keystore` (the HealthChat upload keystore, alias `healthchat-upload`) |
| `RELEASE_KEYSTORE_PASSWORD` | Keystore password |
| `RELEASE_KEY_ALIAS` | Key alias inside the keystore |
| `RELEASE_KEY_PASSWORD` | Key password |
| `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON` | Play Console → Users and permissions → invite a service account → download JSON. Needs "Release manager" permissions scoped to `ch.healthchat.element`. |
## Keystore rotation (rare, high-risk)
Unlike Apple Distribution certificates, **Android upload keystores do not
expire** and should not be rotated casually. The upload key is tied to the
app's identity in Play Console (Play App Signing is enabled; the repo key is
the upload key); rotating it is a one-way process that uses Play App
Signing's "Upload key reset" flow.
If the keystore is lost or believed to be compromised:
1. Play Console → Release → Setup → App integrity → Upload key → **Request
upload key reset**. Instructions follow, requiring you to upload a new
keystore and wait for Google to rotate.
2. Generate a fresh keystore on the operator's machine:
```bash
keytool -genkey -v -keystore healthchat-upload-new.jks \
-keyalg RSA -keysize 2048 -validity 10000 -alias healthchat
```
3. Once Google confirms the rotation, re-encode and update secrets:
`base64 -w0 healthchat-upload-new.jks | pbcopy`, then replace
`RELEASE_KEYSTORE_BASE64`, `RELEASE_KEYSTORE_PASSWORD`, `RELEASE_KEY_ALIAS`,
`RELEASE_KEY_PASSWORD` in repo settings.
4. Run the release workflow via `workflow_dispatch` with a throwaway version
bump and `release_status: draft` to verify end-to-end. Discard the draft.
**Do not attempt rotation without reading Play App Signing docs end-to-end**
and coordinating a maintenance window. A botched rotation means users can't
receive updates until Google completes the reset.
## Promoting internal → production
Internal builds and production builds are the same AAB. To release a
validated internal build:
1. Play Console → HealthChat → Testing → **Internal testing** → find the
release.
2. Confirm it passed internal testing.
3. **Promote release** → Production → **Release details** → fill in "What's
new" (Play Console copies it from the internal release — verify) →
**Review release** → **Start rollout to Production**.
4. Choose staged rollout % if desired.
**This step is deliberately not automated.** Production releases require
human judgment on rollout percentage, Play Store listing changes, and
compliance metadata review.
## Rollback
- **Bad build, release notes fine** → fix the cause, bump
`versionReleaseNumber`, commit, tag `healthchat-v<ver>`. The new build
supersedes the bad one on the internal track.
- **Bad release notes only** → edit `HEALTHCHAT_CHANGES.md` on
`release/healthchat`, then update the "What's new" text in the Play Console
release manually (the pipeline only writes it at upload time).
- **Completely wrong version number** → treat as burned. Bump
`versionReleaseNumber` further, tag a higher `healthchat-v<ver>`. Play
Store's `versionCode` rule means the next run naturally supersedes.
- **Production rollout gone wrong** → Play Console → Production → **Halt
rollout** (works during staged rollout), then ship a hotfix as above.
## Known branch divergence when merging upstream
When merging a newer upstream Element X release tag into `release/healthchat`
(temporarily add `https://github.com/element-hq/element-x-android` as a
remote, fetch, merge, remove the remote — never keep a permanent upstream
remote):
- **`.github/workflows/healthchat-release-playstore.yml`**,
**`HEALTHCHAT_CHANGES.md`**, and
**`.claude/skills/releasing-healthchat/SKILL.md`** are HealthChat-only —
any merge conflict here means something upstream accidentally overlaps;
keep `--ours`.
- **`plugins/src/main/kotlin/config/BuildTimeConfig.kt`** —
`APPLICATION_ID = "ch.healthchat.element"`, `APPLICATION_NAME = "HealthChat"`,
`METADATA_HOST_REVERSED`, and the healthchat.ch URLs must survive merges.
Upstream uses `io.element.android.x` / `Element X` / nulls.
- **`appconfig/.../ApplicationConfig.kt`** — `PRODUCTION_APPLICATION_NAME` /
`DESKTOP_APPLICATION_NAME = "HealthChat"` must survive. Upstream: `Element`.
- **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** —
`defaultHomeserverList() = listOf("matrix.healthchat.ch")` must survive.
Upstream returns an empty list.
- **`app/build.gradle.kts`** — the `healthchat` signing config and the
`appicon/healthchat` dependency must survive; upstream signs release with
the debug keystore and depends on `appicon/element`.
- **`plugins/src/main/kotlin/Versions.kt`** — take upstream's version when
higher (see Versioning above).
- **`fastlane/metadata/android/`** — HealthChat listing texts (en-US, de-DE)
must survive; upstream ships Element X texts. The per-versionCode changelog
files generated by this pipeline carry HealthChat text; conflicts there
mean a versionCode collided — regenerate from `HEALTHCHAT_CHANGES.md` on
the next release run rather than hand-merging.