From 9df5d957320dcf57a45ab06b2894934fa0266b22 Mon Sep 17 00:00:00 2001 From: Sedat Qaja Date: Tue, 20 Oct 2026 11:03:45 +0200 Subject: [PATCH] docs(healthchat): add the upstream sync skill --- .claude/skills/releasing-healthchat/SKILL.md | 20 +- .../syncing-upstream-healthchat/SKILL.md | 190 ++++++++++++++++++ 2 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 .claude/skills/syncing-upstream-healthchat/SKILL.md diff --git a/.claude/skills/releasing-healthchat/SKILL.md b/.claude/skills/releasing-healthchat/SKILL.md index 15ad3c747f..cf671199de 100644 --- a/.claude/skills/releasing-healthchat/SKILL.md +++ b/.claude/skills/releasing-healthchat/SKILL.md @@ -261,12 +261,12 @@ compliance metadata review. - **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 +## Known branch divergence when syncing with 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): +Upstream is taken by **rebasing** the HealthChat commits onto the new release +tag, never by merging — the procedure, Git LFS handling and force-push live in +the `syncing-upstream-healthchat` skill. The HealthChat-only facts that must +survive every sync: - **`.github/workflows/healthchat-release-playstore.yml`**, **`HEALTHCHAT_CHANGES.md`**, and @@ -279,9 +279,13 @@ remote): 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. +- **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** — the + homeserver lock: `accountProviderAllowList() = listOf(Generic("matrix.healthchat.ch"))`, + `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 `appicon/healthchat` dependency must survive; upstream signs release with the debug keystore and depends on `appicon/element`. diff --git a/.claude/skills/syncing-upstream-healthchat/SKILL.md b/.claude/skills/syncing-upstream-healthchat/SKILL.md new file mode 100644 index 0000000000..023ccea340 --- /dev/null +++ b/.claude/skills/syncing-upstream-healthchat/SKILL.md @@ -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`. + `git fetch https://github.com/element-hq/element-x-android.git tag v --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 v -- 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..release/healthchat # the fork's commits +git merge-base --is-ancestor v 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 v 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 ` 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 v `, 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` commit, not amended into owners — +every amend replays the rest. A mistake in an already-committed stop: `git commit --fixup=`, +then `GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash ^` (conflict-free when later +commits do not touch the file). Intermediates may not compile; the tip must. + +**3. Review** — `git range-diff v..release/healthchat v..`. `!` 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..v -- '*.png' '*.jpg' '*.gif' '*.mp4' '*.m4a' '*.webp' \ + | sort -u | while read c; do git lfs fetch upstream "$c"; done + git lfs push origin # must report no "(missing)"; refetch that commit + git push origin + ``` + 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 ` + 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 ` 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 ` 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//actions/runs//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`, 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- +git push --force-with-lease=release/healthchat: origin :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^{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.