mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
ci(release): add HealthChat Play Store release pipeline
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user