mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
docs(healthchat): add the upstream sync skill
This commit is contained in:
@@ -261,12 +261,12 @@ compliance metadata review.
|
|||||||
- **Production rollout gone wrong** → Play Console → Production → **Halt
|
- **Production rollout gone wrong** → Play Console → Production → **Halt
|
||||||
rollout** (works during staged rollout), then ship a hotfix as above.
|
rollout** (works during staged rollout), then ship a hotfix as above.
|
||||||
|
|
||||||
## Known branch divergence when merging upstream
|
## Known branch divergence when syncing with upstream
|
||||||
|
|
||||||
When merging a newer upstream Element X release tag into `release/healthchat`
|
Upstream is taken by **rebasing** the HealthChat commits onto the new release
|
||||||
(temporarily add `https://github.com/element-hq/element-x-android` as a
|
tag, never by merging — the procedure, Git LFS handling and force-push live in
|
||||||
remote, fetch, merge, remove the remote — never keep a permanent upstream
|
the `syncing-upstream-healthchat` skill. The HealthChat-only facts that must
|
||||||
remote):
|
survive every sync:
|
||||||
|
|
||||||
- **`.github/workflows/healthchat-release-playstore.yml`**,
|
- **`.github/workflows/healthchat-release-playstore.yml`**,
|
||||||
**`HEALTHCHAT_CHANGES.md`**, and
|
**`HEALTHCHAT_CHANGES.md`**, and
|
||||||
@@ -279,9 +279,13 @@ remote):
|
|||||||
Upstream uses `io.element.android.x` / `Element X` / nulls.
|
Upstream uses `io.element.android.x` / `Element X` / nulls.
|
||||||
- **`appconfig/.../ApplicationConfig.kt`** — `PRODUCTION_APPLICATION_NAME` /
|
- **`appconfig/.../ApplicationConfig.kt`** — `PRODUCTION_APPLICATION_NAME` /
|
||||||
`DESKTOP_APPLICATION_NAME = "HealthChat"` must survive. Upstream: `Element`.
|
`DESKTOP_APPLICATION_NAME = "HealthChat"` must survive. Upstream: `Element`.
|
||||||
- **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** —
|
- **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** — the
|
||||||
`defaultHomeserverList() = listOf("matrix.healthchat.ch")` must survive.
|
homeserver lock: `accountProviderAllowList() = listOf(Generic("matrix.healthchat.ch"))`,
|
||||||
Upstream returns an empty list.
|
`canConnectToAnyAccountProvider() = false`, `isAllowedToConnectToAccountProvider()`
|
||||||
|
= membership in that list (since upstream 26.10.0; before that
|
||||||
|
`defaultHomeserverList()`). Upstream returns an empty list / `true` / `true`.
|
||||||
|
`BuildTimeConfig.OAUTH_CLIENT_URL_PATH` must stay `null` (upstream:
|
||||||
|
`"apps/android"`), or MAS rejects the redirect scheme.
|
||||||
- **`app/build.gradle.kts`** — the `healthchat` signing config and the
|
- **`app/build.gradle.kts`** — the `healthchat` signing config and the
|
||||||
`appicon/healthchat` dependency must survive; upstream signs release with
|
`appicon/healthchat` dependency must survive; upstream signs release with
|
||||||
the debug keystore and depends on `appicon/element`.
|
the debug keystore and depends on `appicon/element`.
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
---
|
||||||
|
name: syncing-upstream-healthchat
|
||||||
|
description: Bring the HealthChat fork (release/healthchat) onto a newer element-hq/element-x-android release tag. Use when the user asks to rebase, sync, merge or update to upstream, take a new Element X version, or asks why the fork is behind upstream. Covers the patch-stack model, the rebase procedure, Git LFS, conflicts, verification, the force-push and the rewind.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Syncing the HealthChat fork with upstream
|
||||||
|
|
||||||
|
`release/healthchat` = upstream release tag + HealthChat commits on top. Patch stack, same as
|
||||||
|
healthchat-web and element-x-ios. A sync replaces the stack's base and force-pushes the branch.
|
||||||
|
That is the one sanctioned history rewrite in this repo; everything else in `AGENTS.md` still
|
||||||
|
holds. We rebase, we never merge upstream into the branch.
|
||||||
|
|
||||||
|
First done v26.07.0 → v26.10.0 (2026-10-16 to 10-19). Numbers below come from that run.
|
||||||
|
|
||||||
|
## Before touching anything
|
||||||
|
|
||||||
|
- [ ] **Toolchain.** Upstream pins the JDK (`java-version` in `.github/workflows/*.yml`, 21),
|
||||||
|
`COMPILE_SDK` / `BUILD_TOOLS_VERSION` (`plugins/src/main/kotlin/Versions.kt`), AGP, Kotlin
|
||||||
|
and Metro (`gradle/libs.versions.toml`). Gradle and AGP install themselves; the Rust SDK
|
||||||
|
comes from Maven. What does not: the self-hosted incus runners ship no Android SDK — the
|
||||||
|
`Set up Android SDK` step installs it (see the releasing skill, "Runner and build tuning").
|
||||||
|
If `COMPILE_SDK` moved, check that step still resolves the platform. 26.10.0 moved nothing.
|
||||||
|
- [ ] **Dev Mac Gradle.** A warm daemon can hang `assembleDebug` forever (0 % CPU, no log).
|
||||||
|
`./gradlew --stop` first, then compile-only tasks with `--max-workers=2`, e.g.
|
||||||
|
`:tests:uitests:compileDebugUnitTestKotlin` compiles every module. Gate scripts on the
|
||||||
|
exit code — a `rebase --continue` after an unchecked compile committed a broken stop once.
|
||||||
|
- [ ] **Upstream.** There is no upstream remote by default. Tags are `v<version>`.
|
||||||
|
`git fetch https://github.com/element-hq/element-x-android.git tag v<new> --no-tags` is
|
||||||
|
enough for the rebase. LFS needs a named remote:
|
||||||
|
`git remote add upstream https://github.com/element-hq/element-x-android.git`. After
|
||||||
|
adding it, `gh` may resolve to upstream — always pass `-R Novaloop-AG/element-x-android`.
|
||||||
|
- [ ] **Decisions, product owner:** which upstream feature-flag defaults flipped
|
||||||
|
(`git diff v<old> v<new> -- libraries/featureflag/api/.../FeatureFlags.kt`; 26.10.0:
|
||||||
|
message search default on, kept on), where fork UI lands when upstream restructures a
|
||||||
|
screen (26.10.0: settings split, HealthChat entries went to the account section), and
|
||||||
|
whether fork commits may be squashed (only within one HealthChat release, never across).
|
||||||
|
|
||||||
|
## The stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --oneline --no-merges v<old>..release/healthchat # the fork's commits
|
||||||
|
git merge-base --is-ancestor v<old> release/healthchat # must be true
|
||||||
|
```
|
||||||
|
|
||||||
|
Upstream files the fork edits — conflicts live here every time:
|
||||||
|
|
||||||
|
- `features/enterprise/impl-foss/.../DefaultEnterpriseService.kt` + test — the homeserver
|
||||||
|
lock (since 26.10.0: `accountProviderAllowList()` = `Generic("matrix.healthchat.ch")`,
|
||||||
|
`canConnectToAnyAccountProvider() = false`, membership check).
|
||||||
|
- `plugins/src/main/kotlin/config/BuildTimeConfig.kt` — app id, name, OIDC URLs,
|
||||||
|
`OAUTH_CLIENT_URL_PATH = null` (MAS: redirect scheme must reverse-match the apex host).
|
||||||
|
- `plugins/src/main/kotlin/Versions.kt` — version; dropped from the stack, set again at release.
|
||||||
|
- `features/home/impl` (Members tab: `HomeView`, `HomePresenter`, `HomeState`,
|
||||||
|
`HomeStatePreviewParam`, `HomeNavigationBarItem`, `HomeTopBar`, `HomeFlowNode`, tests) and
|
||||||
|
`features/home/api/HomeEntryPoint.kt`, `appnav/.../LoggedInFlowNode.kt` (`navigateToUserProfile`).
|
||||||
|
- `features/preferences/impl` root node/view + `PreferencesFlowNode` (profile editor replaces
|
||||||
|
the display-name editor, account status banner).
|
||||||
|
- `features/userprofile`, `features/roomdetails` (profile fields section).
|
||||||
|
- `libraries/deeplink/impl`, `libraries/matrix/{api,impl}` permalink (HealthChat hosts),
|
||||||
|
`features/login/impl/DefaultLoginIntentResolver.kt`.
|
||||||
|
- `.github/workflows/{tests,quality,maestro-local,recordScreenshots}.yml` (self-hosted
|
||||||
|
runners, heap, fork guards), `app/src/gplay/AndroidManifest.xml` (permission strip),
|
||||||
|
`HEALTHCHAT_CHANGES.md`.
|
||||||
|
|
||||||
|
## Rebuilding the stack
|
||||||
|
|
||||||
|
Measure first: dry-run `git rebase --onto v<new> v<old> HEAD` in a throwaway worktree with a
|
||||||
|
script that records the conflicting files per commit and resolves blindly, then abort. The
|
||||||
|
conflict map decides the cleanup. 26.10.0: 11 of 31 commits stopped, 29 hunks, 62 goldens.
|
||||||
|
|
||||||
|
**1. Clean the series on the old base** (scratch branch off `release/healthchat`). Interactive
|
||||||
|
rebase is not available to agents; rebuild with `cherry-pick` (`-n` + `commit -C <orig>` for
|
||||||
|
edited commits; `cherry-pick` has no `-q`). Drop golden-only commits and the version bump;
|
||||||
|
strip every PNG, and edits to files upstream deleted, out of feature commits; squash the CI
|
||||||
|
run-of-commits within one release. `git diff --stat release/healthchat` must show only the
|
||||||
|
intended removals; compile once.
|
||||||
|
|
||||||
|
**2. Plain rebase** — `git rebase --onto v<new> v<old> <scratch>`, resolve per stop, keep each
|
||||||
|
commit's subject and intent. Upstream API removals cannot be "resolved", they need new code
|
||||||
|
*inside that commit* (26.10.0: `EnterpriseService` redesign). After a large stop compile its
|
||||||
|
modules before `--continue`. Cross-commit fallout (renames, moved fakes, signature changes)
|
||||||
|
goes into ONE trailing `Adapt HealthChat to upstream v<new>` commit, not amended into owners —
|
||||||
|
every amend replays the rest. A mistake in an already-committed stop: `git commit --fixup=<sha>`,
|
||||||
|
then `GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <sha>^` (conflict-free when later
|
||||||
|
commits do not touch the file). Intermediates may not compile; the tip must.
|
||||||
|
|
||||||
|
**3. Review** — `git range-diff v<old>..release/healthchat v<new>..<scratch>`. `!` on a commit
|
||||||
|
that applied cleanly is context drift; check the `-`/`+` lines are not ours.
|
||||||
|
|
||||||
|
Recurring fallout to expect: Konsist naming (`*PreviewParam` for `PreviewParameterProvider`
|
||||||
|
subclasses, `*Event` for sealed interfaces), design-system signature changes (26.10.0:
|
||||||
|
`ListItem(headlineContent)` → `ListItem(content)`), test fakes moving modules, action pins in
|
||||||
|
our CI hunks regressing upstream's bumps, exhaustive `when` over `HomeNavigationBarItem`.
|
||||||
|
|
||||||
|
zsh: `for c in $VAR` does not split on newlines, a variable named `path` *is* `PATH`, and
|
||||||
|
`$TAG:file` applies a history modifier — use `while read`, never name a variable `path`,
|
||||||
|
write `${TAG}:file`.
|
||||||
|
|
||||||
|
## Git LFS — the part that bites
|
||||||
|
|
||||||
|
Goldens, compound screenshots, `docs/images-lfs`, and `libraries/mediaupload/impl` test assets
|
||||||
|
are LFS-tracked (`.gitattributes`, ~4500 files). A fresh clone holds pointers only.
|
||||||
|
|
||||||
|
- origin is a standalone repo; upstream's LFS objects are not there. GitHub rejects a push
|
||||||
|
whose *new commits* reference LFS objects it lacks (`GH008 … unknown Git LFS objects`). The
|
||||||
|
rebased branch carries every upstream commit since the old tag. Fetch the LFS tree of each
|
||||||
|
upstream commit that touched LFS paths, then push the objects before the branch:
|
||||||
|
```bash
|
||||||
|
git log --format=%H v<old>..v<new> -- '*.png' '*.jpg' '*.gif' '*.mp4' '*.m4a' '*.webp' \
|
||||||
|
| sort -u | while read c; do git lfs fetch upstream "$c"; done
|
||||||
|
git lfs push origin <scratch> # must report no "(missing)"; refetch that commit
|
||||||
|
git push origin <scratch>
|
||||||
|
```
|
||||||
|
26.10.0: 210 such commits, 9837 objects, 525 MB locally, 3809 uploaded (174 MB). Run the loop
|
||||||
|
detached (`nohup`) — an agent's tool call times out at 10 minutes — and keep a done-file so it
|
||||||
|
resumes. Objects still "(missing)" after the loop are usually goldens from the fork's *own*
|
||||||
|
commits that a pointer-only clone never held: `git lfs fetch origin <original commit sha>`
|
||||||
|
for each commit on the old branch that touched them. The dry run lists every object until
|
||||||
|
the branch itself is pushed; verify presence with the LFS batch API or trust a clean upload.
|
||||||
|
- `git lfs push <branch>` only covers objects introduced by commits absent from the remote. 26.10.0:
|
||||||
|
167 objects referenced by the v26.10.0 tree were outside that set and `Validate Git LFS` failed
|
||||||
|
with "Object does not exist on the server". Read the oids from that job's log and
|
||||||
|
`git lfs push --object-id origin <oid…>` them (they are local after the fetch loop). Treat a
|
||||||
|
green `Validate Git LFS` on the PR as the only proof the store is complete.
|
||||||
|
- The recorder commits as ElementBot, a non-member: its CI runs land in `action_required` and
|
||||||
|
need `gh api -X POST repos/<repo>/actions/runs/<id>/approve` (or the PR's "Approve and run").
|
||||||
|
Never push to the branch while a recording runs: the recorder's final `git push` is a plain
|
||||||
|
push and is rejected once the tip has moved.
|
||||||
|
- `git lfs checkout` before running `libraries/mediaupload/impl` tests locally; pointer files
|
||||||
|
fail them. The screenshot tests are never run locally (`AGENTS.md`).
|
||||||
|
- `validate-lfs.yml` fails a PR that commits a PNG outside LFS.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Take upstream's goldens on every conflict and never commit locally rendered ones (macOS
|
||||||
|
renders differ). After the push, add the `Record-Screenshots` label to the PR or
|
||||||
|
`workflow_dispatch` `recordScreenshots.yml`; the Linux runner records and commits all goldens.
|
||||||
|
Review only the HealthChat ones. A PR whose base is `release/healthchat` is *conflicting* by
|
||||||
|
construction (old and new stack touch the same files) and GitHub runs no `pull_request`
|
||||||
|
workflow on a conflicting PR — open the PR against a branch at the upstream tag
|
||||||
|
(`upstream/v<new>`, pushed by the owner) so the PR shows the 19-odd stack commits and CI runs.
|
||||||
|
Retargeting a PR is an `edited` event: tests/quality/build only start on the next push.
|
||||||
|
The label path needs `persist-credentials: true` on the LFS checkout steps (private fork,
|
||||||
|
fixed in 26.10.0). If the job dies on the LayoutLib static-initialiser crash,
|
||||||
|
restore the "one JVM per screenshot test class" workaround (`forkEvery = 1L` in the root
|
||||||
|
`build.gradle.kts`, dropped in 26.10.0 because upstream's Paparazzi bump fixed it).
|
||||||
|
|
||||||
|
## Verification of the tip
|
||||||
|
|
||||||
|
Local, all green before handover (26.10.0: 1200 unit tests):
|
||||||
|
```bash
|
||||||
|
./gradlew :tests:uitests:compileDebugUnitTestKotlin :app:compileGplayDebugKotlin
|
||||||
|
./gradlew :features:healthchat:impl:testDebugUnitTest :features:home:impl:testDebugUnitTest \
|
||||||
|
:features:preferences:impl:testDebugUnitTest :features:enterprise:impl-foss:testDebugUnitTest \
|
||||||
|
:features:login:impl:testDebugUnitTest :libraries:matrix:impl:testDebugUnitTest :appnav:testDebugUnitTest
|
||||||
|
./gradlew :tests:konsist:test ktlintCheck detekt lint
|
||||||
|
```
|
||||||
|
CI on the branch (tests, quality, screenshots) must be green on the self-hosted runners.
|
||||||
|
|
||||||
|
Device matrix (owner, debug and release build): MAS OIDC login on a fresh install — chooser
|
||||||
|
skipped, client URI `https://healthchat.ch` without appended path — is the gate for everything
|
||||||
|
else; previous release → new keeps the session; last Classic build → new lands on login without
|
||||||
|
crashing; member directory, profile editor, invites, status banners; deeplinks on HealthChat
|
||||||
|
hosts and `to.healthchat.ch` share links; push in background and killed; release build under
|
||||||
|
R8 exercising the suspend Retrofit calls; whatever upstream switched on (26.10.0: German
|
||||||
|
whole-word search in an encrypted room).
|
||||||
|
|
||||||
|
## Force-push, rewind
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin release/healthchat:refs/heads/backup/release-healthchat-before-<new>
|
||||||
|
git push --force-with-lease=release/healthchat:<old tip> origin <scratch>:release/healthchat
|
||||||
|
git checkout release/healthchat && git reset --hard origin/release/healthchat # every clone
|
||||||
|
```
|
||||||
|
|
||||||
|
The branch is unprotected: the push lands immediately. Old `healthchat-v*` tags keep their
|
||||||
|
commits alive and off the branch line. Rewind =
|
||||||
|
`git push --force origin healthchat-v<last>^{commit}:release/healthchat`. A rewind does not
|
||||||
|
remove a tag or Play build made on the new history — tag only after the device tests. Then set
|
||||||
|
the version, update `HEALTHCHAT_CHANGES.md`, the releasing skill ("Known branch divergence")
|
||||||
|
and this skill with what changed.
|
||||||
|
|
||||||
|
## What not to do
|
||||||
|
|
||||||
|
- Merge upstream into `release/healthchat`, or squash the fork into a side branch and merge
|
||||||
|
upstream there: both duplicate every fork commit and conflict on every touched file.
|
||||||
|
- Pull on a rewritten branch (merges the old history back in). Fetch and reset.
|
||||||
|
- Commit goldens from a Mac, or resolve a golden conflict by hand — take upstream's and record.
|
||||||
|
- Amend compile fixes into owner commits mid-rebase; collect them in the adaptation commit.
|
||||||
|
- Touch `release/healthchat` before the owner has reviewed the range-diff.
|
||||||
Reference in New Issue
Block a user