Compare commits

..

3 Commits

Author SHA1 Message Date
joel.poyet 558802b552 Remove the simulator reset again
It was added on the theory that the runner's reused simulators were leaking state
between runs. That theory was wrong: erasing both devices changed nothing — the
same seven suites failed identically before and after.

The leftover files LoggingTests trips on are its own, from a previous attempt of
the same test within one run (the suite runs with -test-iterations 4), so nothing
crosses runs for an erase to clean. Keeping a step that mutates shared state on the
release machine for no measured benefit is worse than not having it.
2026-07-16 09:44:43 +02:00
joel.poyet 7a37282d0b Erase the test simulators before running the tests
The unit tests assume a clean simulator, which costs nothing on a hosted VM
because it hands out a fresh device every run. This runner reuses the same two
devices, so app containers, caches and logs survive between runs and six suites
fail on the previous run's leftovers rather than on anything in the code.
LoggingTests is the clearest: it expects no log files and finds nine, in the same
device container each time.

Only the two devices the tooling pins are erased, resolved by UDID. A blanket
erase would take out simulator state belonging to other work on a machine the
release pipeline shares.
2026-07-16 09:25:01 +02:00
joel.poyet 728ace6f81 Make CI work on this internal fork
Every check was red on every pull request. Upstream declares permissions: {} in its
workflows, which costs nothing on a public repo and is fatal here: the token is
limited to Metadata:read, so actions/checkout cannot read an internal repo and dies
with a misleading "Repository not found". Repo settings cannot fix it — an explicit
permissions block always overrides the default.

Each job now gets the minimum it needs. Danger also reads the pull request through
the issues API, so it needs issues:read alongside pull-requests:write, and zizmor's
SARIF upload needs actions:read on a private repo.

The unit tests move to the org's Mac Studio: GitHub-hosted macos-26 could not finish
inside the hour and bills at 10x. Preview tests are skipped there, using the flag
upstream already uses for its own enterprise tests — they render a snapshot per
preview and never once finished on either runner. The trade is that preview snapshot
regressions are no longer caught in CI.

Workflows that only make sense upstream are gated off rather than deleted, so they
keep rebasing cleanly. The enterprise tests need Element's bot token and their
private submodule. Three nightlies fail at checkout every weeknight — this repo is a
detached copy rather than a network fork, so GitHub never disabled their schedules —
and one of them posts to Element's Codecov on every run.
2026-07-16 08:08:41 +02:00
21 changed files with 1765 additions and 1083 deletions
@@ -1,298 +0,0 @@
---
name: releasing-healthchat
description: Cut a HealthChat TestFlight release of element-x-ios by tagging healthchat-v*. Use when the user asks to release, publish, tag, deploy, ship, or cut a new HealthChat iOS version, asks about the TestFlight or App Store release process, asks about renewing the Apple Distribution signing certificate, or asks about the required GitHub Actions secrets for the release pipeline.
---
# Releasing HealthChat (element-x-ios)
Releases are driven by git tags of the form `healthchat-v<version>` pushed to
`origin`. The tag triggers `.github/workflows/release-healthchat.yml`, which runs
on the org's self-hosted macOS runner, builds a signed App Store IPA, and uploads
it to **TestFlight only**. No GitHub Release is created.
**App Store submission is always a manual step** in App Store Connect after a
TestFlight build has been validated — see "Promoting to App Store" below.
**HealthChat ships as an in-place update of the legacy element-ios app.** Same
bundle id (`ch.healthchat.element`), same team (`6G37K72W82`), same App Store
record. Existing users' keychain and app-group container survive the update. Read
"The migration contract" before changing anything that looks like an identifier.
## 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. The pipeline's awk extractor matches this exact heading format;
no match ships a fallback placeholder as the TestFlight "What to Test" text.
- [ ] The version is **numeric only** — see "Version rules". This is the single
most common way to waste 30 minutes here.
- [ ] `CHANGES.md` is **not** used for HealthChat releases — upstream's
`release-to-github` owns it. Only edit `HEALTHCHAT_CHANGES.md`.
## Version rules
**Numeric only. One to three dot-separated integers.** `26.7.4`. Never `26.7.4-rc1`.
The marketing version lands verbatim in `CFBundleShortVersionString`, and Apple
rejects anything that is not at most three period-separated integers. A marker
version passes the tag regex and every local gate, then dies at the **upload
step, after ~30 minutes of signing**, with an ITMS error indistinguishable from a
signing failure.
The Fastfile guards this in `release_testflight`, before any build or signing work. Do not relax that guard.
The legacy element-ios skill documents `-rc1` / `-hotfix1` / `-renew-test` tag
suffixes. **Do not carry that convention over.** The workflow derives the version
by stripping only the `healthchat-v` prefix, so a suffix becomes part of the
version and hits the wall above. Put markers in the release notes, and use the
`workflow_dispatch` path for test builds.
`project.yml:56`'s `MARKETING_VERSION` is only a default — **the tag is the source
of truth**, and the pipeline injects the version at build time without touching
any tracked file. Rebases from upstream will drag `project.yml`'s version around
(`automatic-calendar-version.yml` no-ops in this fork); ignore it.
## Cutting the release (normal path)
```bash
# from release/healthchat, fully committed:
git tag healthchat-v<MAJOR>.<MINOR>.<PATCH>
git push origin healthchat-v<MAJOR>.<MINOR>.<PATCH>
```
Example: `git tag healthchat-v26.7.4`
The `healthchat-` prefix keeps these tags clear of upstream's `release/<version>`
and `nightly/*` namespaces.
The workflow triggers automatically. Watch it with `gh run watch` or the Actions
UI. The TestFlight build appears in App Store Connect within a few minutes of the
run finishing (Apple's processing queue).
**Build numbers are not encoded in tags.** `generate_build_number` in
`fastlane/Fastfile` produces a `YYYYMMDDHHMMSS` timestamp, so ordering is
monotonic and a retry never collides with a build number Apple has already seen.
## Manual / retry path (workflow_dispatch)
Use when retrying a failed run, testing the pipeline (e.g. cert-renewal
verification), or rebuilding without moving tags.
1. GitHub → Actions → "Release HealthChat" → **Run workflow**.
2. Fill in:
- **version** (required): e.g. `26.7.99`. Numeric only — same rule as tags.
- **release_notes_override** (optional): replaces the `HEALTHCHAT_CHANGES.md`
lookup. Use this to mark test builds, since the version cannot carry a marker.
3. Run. The IPA is built and uploaded to TestFlight exactly as the tag path would.
## Release notes
`HEALTHCHAT_CHANGES.md` is the single source. The workflow extracts the section
matching the version being released. Format:
```markdown
## Changes in 26.7.4 (2026-08-01)
🙌 Improvements
- Bullet one.
- Bullet two.
```
No matching section emits a workflow warning and falls back to
`HealthChat <version> — automated build from <ref>.` Prefer fixing the changelog.
A **missing file** is a hard error, not a fallback.
## After the release
- [ ] Workflow run is green in GitHub Actions.
- [ ] Build appears under App Store Connect → TestFlight within ~10 minutes.
- [ ] Internal testers are invited (the auto-distribution group may handle this).
- [ ] Artifacts (IPA, dSYM, Gym logs) attached to the run, retained 90 days.
## Never commit the post-lane tree
A local lane run rewrites `ElementX.xcodeproj/project.pbxproj` — `xcodegen`
regenerates it, then `disable_automatic_code_signing` and
`update_project_provisioning` write Manual signing and profile UUIDs into it. That
file is tracked.
```bash
git checkout -- ElementX.xcodeproj
```
Committing it pins every future build to one machine's profile UUIDs.
## Required GitHub Actions secrets
Five. Missing any fails the workflow at the signing step with a clear error.
| Secret | Source |
|---|---|
| `HEALTHCHAT_ASC_KEY_ID` | App Store Connect → Users and Access → Integrations → Team Keys (10-char Key ID) |
| `HEALTHCHAT_ASC_KEY_ISSUER_ID` | Same page, UUID at the top |
| `HEALTHCHAT_ASC_KEY_CONTENT` | `.p8` contents including the `-----BEGIN/END PRIVATE KEY-----` lines |
| `HEALTHCHAT_CERTIFICATES_P12` | Base64 distribution cert (`base64 -i healthchat.p12 \| pbcopy`) |
| `HEALTHCHAT_CERTIFICATES_P12_PASSWORD` | Password used when exporting the `.p12` |
**There is no `HEALTHCHAT_KEYCHAIN_PASSWORD`.** The legacy skill lists one and the
legacy workflow references it, but it exists at neither repo nor org level and
that pipeline runs green anyway — `import-codesign-certs` tolerates its absence.
Do not add it back.
Managing these needs the `snowping` GitHub account; this repo is INTERNAL and
invisible to the other account.
## Self-hosted runner requirements
Runs on `novaloop-mac-studio-001` — an **org-level** runner (`[self-hosted,
macOS, ARM64]`) shared with the legacy fork, not registered per-repo.
- **One Xcode, globally selected.** A single install at `/Applications/Xcode.app`
(**26.6**, build `17F113`, today) — no versioned `Xcode_*.app` siblings.
**Nothing pins it.** The Fastfile builds with whatever `xcode-select` points at, so
**whatever the runner has is what ships**. Upgrading the runner's Xcode silently
changes the compiler every subsequent release is built with — no tag, no diff, no
review. Treat that upgrade as a release-affecting change.
- **Do not reintroduce a version pin.** Pinning a version the runner lacks sends
`xcodes` into a multi-GB authenticated download that no CI secret can satisfy, and
the run hangs. Change the runner's Xcode instead.
- **Toolchain installed once by hand**, never by CI: `xcodegen swiftlint swiftformat
xcbeautify xcresultparser pkl` (`xcresultparser` is not in homebrew-core — only
`a7ex/homebrew-formulae/xcresultparser` ships it). `git-lfs`, `xcodes`, `brew` and
rbenv `ruby`/`bundle` are already present. There is deliberately **no `brew bundle`
step and no `Brewfile`**: this machine is shared with CI, and a per-run
`brew update` could upgrade `xcodegen` underneath a running release. CI asserts the
tools are present and fails loudly naming the fix; it never provisions.
- **No passwordless sudo.** Nothing in CI may call `sudo` — that is why upstream's
`setup_github_actions_environment` cannot run here.
- **`APPLE_ID`** exported as a machine-level shell variable — the `Appfile` reads
it and it is not carried in any secret.
- Network access to `developer.apple.com`, `api.appstoreconnect.apple.com`,
`github.com`, `rubygems.org`.
**A release's only Xcode provenance is its own log.** `release-healthchat.yml` runs
`xcodebuild -version` (`Record toolchain`) before every build, so each run records the
compiler that produced it. Nothing pins that version — upgrading the runner's Xcode
silently changes the compiler every subsequent release is built with, and that log
line is the only place it shows up. Do not reconstruct a past build's toolchain from
the runner: it answers for today, not for the build you are asking about. Read the
run's log.
## Renewing the Apple Distribution certificate (annual)
Apple Distribution certificates are valid for **exactly 1 year**. There is no
longer-lived option. Last renewed **2026-04-07**, so the next is due around
**April 2027**. Renew proactively.
1. **~4 weeks before expiry**, check on the Mac holding the private key:
```bash
security find-certificate -c "Apple Distribution: E-HealthSolutions GmbH" -p | openssl x509 -noout -enddate
```
2. **Generate a CSR** on whichever Mac will hold the new private key: Keychain
Access → Certificate Assistant → Request a Certificate from a Certificate
Authority… → "Saved to disk".
3. **Apple Developer portal → Certificates** → **+** → Apple Distribution → upload
the CSR → download the `.cer` → double-click to install.
4. **Regenerate all 3 App Store provisioning profiles** — they reference the old
cert and become invalid:
- `HealthChat App Store`
- `HealthChat NSE: App Store`
- `HealthChat Share Extension: App Store`
5. **Verify locally**: archive in Xcode → Distribute App → App Store Connect →
Upload. TestFlight accepting the build proves the cert works.
6. **Export as `.p12`**: Keychain Access → My Certificates → right-click the new
`Apple Distribution: E-HealthSolutions GmbH` → Export → set a password.
7. **Re-encode and update secrets**: `base64 -i healthchat-new.p12 | pbcopy`, then
replace `HEALTHCHAT_CERTIFICATES_P12` and `HEALTHCHAT_CERTIFICATES_P12_PASSWORD`.
8. **Run the workflow** via `workflow_dispatch` with a throwaway numeric version
to verify end-to-end. Discard the resulting build.
9. **Revoke the old cert** once the new one works in both Xcode and CI.
**Keep the expired `.p12` archived for 2 weeks** in case a rollback is needed.
## Promoting a TestFlight build to the App Store
TestFlight and App Store builds are the same binary. To release a validated one:
1. App Store Connect → My Apps → HealthChat → **TestFlight** → find the build.
2. Confirm it passed internal testing.
3. **Distribution** tab → **+** next to "iOS App" → "Prepare for Submission" →
fill in metadata → **Add for Review**.
4. Wait for review (24–48 hours typical).
5. Release manually, or configure automatic release.
**Deliberately not automated.** Automating submission needs `fastlane/metadata/`
plus HealthChat-branded screenshots (neither exists) and a `Deliverfile`
refactored for API key auth.
**Before the first submission**, confirm the install base against the iOS floor:
element-x requires **iOS 18.5**, the legacy required 15.1. Every user below 18.5
stays on 1.4.1 forever — the App Store will not offer them the update, silently.
That floor is upstream's and cannot be lowered.
## Rollback
- **Bad build, notes fine** → fix, commit, tag the next patch version. TestFlight
supersedes the bad build. (Build numbers are timestamps, so no version games.)
- **Bad release notes only** → edit `HEALTHCHAT_CHANGES.md`, then update the
TestFlight "What to Test" text in App Store Connect by hand — the pipeline only
writes it at upload time.
- **Wrong version number** → treat as burned; tag a higher one.
- **Rolling back to legacy 1.4.1 loses push** — element-x overwrites the user's
sygnal pusher in place on first launch (same app id and pushkey).
## The migration contract
**Read this before "cleaning up" anything.** HealthChat replaces the legacy app in
place, and these are what stop existing users being silently logged out.
- **`CLASSIC_APP_*` in `Variants/HealthChat/healthchat.yml` is load-bearing.**
`ClassicAppManager` reads the legacy account and crypto-store keys through them.
Upstream's defaults in `app.yml` point at Element's own `group.im.vector` /
`im.vector.app.*`, which this team cannot provision. **Never restore upstream's
values.** Failure is silent — `loadAccounts()` throws, and
`AuthenticationService` logs it without telling the user or you.
- **The classic keychain group in `ElementX/SupportingFiles/target.yml` must stay
in the `keychain-access-groups` array.** Entitlement arrays live there, never in
the variant — XcodeGen concatenates arrays on include-merge and only overrides
scalars.
- **The `.requiresBackup` fall-through in `AuthenticationStartScreenViewModel` is a
deliberate upstream divergence.** Upstream sends those accounts to a sheet that
tells the user to open Element Classic. That app no longer exists — this update
replaced it — and the sheet's button opens a URL scheme nothing registers. A
rebase that reverts this reintroduces a dead button.
- **Both apps share the `group.ch.healthchat` UserDefaults suite**
(`AppSettings.swift:56`). An unqualified `@UserPreference` name lands in a suite
the legacy also writes to. Collisions are cheap to introduce.
- **`hasSignedInBefore` voids migration testing.** It lives in that shared suite,
survives app deletion, and short-circuits `loadAccounts()`. Any device that has
run an element-x HealthChat build shows a plain login screen *while migration
works perfectly*. Test on a device that has never run an element-x HealthChat
build, or clear the `group.ch.healthchat` suite first;
`"Already signed in to Element X before, skipping loadAccounts"` in the log
means the run is **void, not failed**. A
`"Classic App not configured, skipping loadAccounts"` line is the opposite —
the `CLASSIC_APP_*` Info.plist keys are missing, `ClassicAppManager` never
initialised, and migration is broken for every user. Investigate, do not
dismiss.
## Known branch divergence on rebase
`fastlane/`, `.github/workflows/release-healthchat.yml`, `HEALTHCHAT_CHANGES.md`,
`Gemfile` and this skill are **fork-only** — upstream element-x-ios has
no `fastlane/` directory at all. A rebase conflict in any of them means something
upstream has started to overlap; keep `--ours` and investigate.
What does need watching:
- **`project.yml`'s `MARKETING_VERSION`** — `automatic-calendar-version.yml` and
`post-release.yml` are gated on `github.repository == 'element-hq/element-x-ios'`
and silently no-op here, so rebases drag upstream's version along. Harmless: the
tag is the source of truth.
- **`AuthenticationStartScreenViewModel.login(classicAppAccount:)`** — the only
upstream logic file this fork edits. See "The migration contract".
- **`app.yml`'s `CLASSIC_APP_*`** — upstream owns these and they stay pointed at
Element's ids. The variant overrides them. Do not "fix" `app.yml`.
-133
View File
@@ -1,133 +0,0 @@
name: Release HealthChat
on:
push:
tags:
- 'healthchat-v*'
workflow_dispatch:
inputs:
version:
description: 'Marketing version (e.g. 26.7.4). Must be 1-3 dot-separated integers — Apple rejects anything else.'
required: true
type: string
release_notes_override:
description: 'Optional release notes. If empty, falls back to the HEALTHCHAT_CHANGES.md section for the given version.'
required: false
type: string
permissions:
# Minimum needed for actions/checkout to clone an internal repo. The workflow does not push,
# comment, or create releases — everything else authenticates with the ASC key and .p12.
contents: read
concurrency:
group: release-healthchat-${{ github.event.inputs.version || github.ref_name }}
cancel-in-progress: false
jobs:
release:
name: Build & upload to TestFlight
runs-on: [self-hosted, macOS]
timeout-minutes: 90
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# This repo's only submodule is element-hq's private Enterprise repo, which Novaloop
# cannot clone. Checking out submodules fails the job on its very first step.
submodules: false
persist-credentials: false
- name: Record toolchain
# HealthChat: nothing pins Xcode (the runner carries a single globally-selected one), so this
# log line is the only record of which compiler produced a given TestFlight build.
run: xcodebuild -version
- name: Resolve marketing version
id: version
env:
# Read via env, never `${{ }}` inside the script: the input is attacker-controlled text
# that GitHub would splice into the shell before the regex below could reject it.
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION="$INPUT_VERSION"
else
VERSION="${GITHUB_REF_NAME#healthchat-v}"
fi
# Apple caps CFBundleShortVersionString at three integers. The Fastfile guards this too,
# but failing here costs seconds instead of a signing run.
if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
echo "::error::Invalid marketing version '$VERSION' — expected 1-3 dot-separated integers, e.g. 26.7.4"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved marketing version: $VERSION"
- name: Extract release notes
id: notes
env:
VERSION: ${{ steps.version.outputs.version }}
OVERRIDE: ${{ github.event.inputs.release_notes_override }}
run: |
set -euo pipefail
if [[ -n "$OVERRIDE" ]]; then
NOTES="$OVERRIDE"
echo "Using release notes from the workflow input"
elif [[ ! -f HEALTHCHAT_CHANGES.md ]]; then
echo "::error::HEALTHCHAT_CHANGES.md is missing — it is the release-notes source"
exit 1
else
NOTES=$(awk -v ver="$VERSION" '
/^## Changes in / {
if (found) { exit }
if ($0 ~ "## Changes in " ver "( |$|\\()") { found = 1; next }
}
found { print }
' HEALTHCHAT_CHANGES.md | sed '/^[[:space:]]*$/{N;/^\n$/D;}')
if [[ -z "$NOTES" ]]; then
NOTES="HealthChat ${VERSION} — automated build from ${GITHUB_REF_NAME}."
echo "::warning::No HEALTHCHAT_CHANGES.md section found for $VERSION — using fallback"
fi
fi
{
echo 'notes<<NOTES_EOF'
echo "$NOTES"
echo 'NOTES_EOF'
} >> "$GITHUB_OUTPUT"
- name: Bundle install
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Import signing certificate
uses: apple-actions/import-codesign-certs@b610f78488812c1e56b20e6df63ec42d833f2d14 # v6.0.0
with:
p12-file-base64: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12 }}
p12-password: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12_PASSWORD }}
- name: Build & upload to TestFlight
run: bundle exec fastlane release_testflight
env:
# Passed via env rather than lane options so multi-line notes containing quotes,
# $, or backticks survive without shell escaping.
HEALTHCHAT_MARKETING_VERSION: ${{ steps.version.outputs.version }}
HEALTHCHAT_RELEASE_NOTES: ${{ steps.notes.outputs.notes }}
APPSTORECONNECT_KEY_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ID }}
APPSTORECONNECT_KEY_ISSUER_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ISSUER_ID }}
APPSTORECONNECT_KEY_CONTENT: ${{ secrets.HEALTHCHAT_ASC_KEY_CONTENT }}
TEAM_ID: '6G37K72W82'
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: healthchat-${{ steps.version.outputs.version }}
path: |
out/ElementX.ipa
out/ElementX.app.dSYM.zip
build/Logs/Gym/*.log
retention-days: 90
if-no-files-found: warn
-7
View File
@@ -77,13 +77,6 @@ jobs:
# on either runner. Unit tests alone take ~16 min here. Upstream skips them the same way in
# unit-tests-enterprise.yml. Cost: preview snapshot regressions are not caught in CI, so
# re-record and check them locally when changing UI.
env:
# HealthChat: VoiceMessageCacheTests cannot write its URLFileProtection.complete cache file on
# this runner's simulator (NSCocoaErrorDomain 513) — a runner-environment issue, not a code bug:
# the suite passes locally and upstream, and neither recreating+booting the device nor clearing
# state fixes it. Skip it here only, via -skip-testing. Drop this once the runner can write
# data-protected files again.
CI_SKIP_TESTS: VoiceMessageCacheTests
run: swift run -q tools ci unit-tests --skip-previews
- name: Archive artifacts
-7
View File
@@ -49,10 +49,3 @@ test_output
# Only commit Pkl packages (there may be other caches in here).
Secrets/vendor/*
!Secrets/vendor/package-2
# fastlane release outputs
out/
provisioning_profiles/
# Regenerated by every fastlane invocation, including on the runner. Ignored rather than
# committed, matching element-ios.
fastlane/README.md
-8
View File
@@ -63,14 +63,6 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
---
## Releasing
HealthChat ship via tag `healthchat-v*` → TestFlight. Full runbook in
`.agents/skills/releasing-healthchat/SKILL.md`. Read skill before release, cert
renewal, or touching `fastlane/`. No duplicate runbook here.
---
## Project Structure
### Build System
+6 -6
View File
@@ -9893,9 +9893,9 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CLASSIC_APP_DEEP_LINK_URL = "element://open";
CLASSIC_APP_GROUP_IDENTIFIER = group.ch.healthchat;
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared";
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "ch.healthchat.element.encryption-manager-service";
CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector;
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared";
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
@@ -9968,9 +9968,9 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CLASSIC_APP_DEEP_LINK_URL = "element://open";
CLASSIC_APP_GROUP_IDENTIFIER = group.ch.healthchat;
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared";
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "ch.healthchat.element.encryption-manager-service";
CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector;
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared";
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service";
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
@@ -113,14 +113,13 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
private func login(classicAppAccount: ClassicAppAccount? = nil) async {
if let classicAppAccount {
// HealthChat diverges from upstream here. Upstream sends `.requiresBackup` accounts to a sheet
// asking the user to set up key backup in Element Classic first. HealthChat ships with the same
// bundle id as the legacy app, so this update *replaces* it — there is no Classic app left to
// open, and the sheet's only button resolves to nothing. Fall through to a normal sign-in
// instead: the user re-verifies and loses only history that by definition has no backup.
await configureAccountProvider(classicAppAccount.serverName,
loginHint: "mxid:\(classicAppAccount.userID)",
fallbackHomeserverURL: classicAppAccount.homeserverURL)
if classicAppAccount.state.availableSecrets == .requiresBackup {
state.bindings.showClassicAppBackupInstructions = true
} else {
await configureAccountProvider(classicAppAccount.serverName,
loginHint: "mxid:\(classicAppAccount.userID)",
fallbackHomeserverURL: classicAppAccount.homeserverURL)
}
} else if let serverName = state.serverName {
await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint)
} else {
@@ -23,7 +23,6 @@
<key>keychain-access-groups</key>
<array>
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
<string>$(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
</array>
</dict>
</plist>
+2 -7
View File
@@ -125,17 +125,12 @@ targets:
com.apple.developer.usernotifications.communication: true
com.apple.security.app-sandbox: true
com.apple.security.application-groups:
# HealthChat: dropped $(CLASSIC_APP_GROUP_IDENTIFIER) — not because it is un-provisionable,
# but because it resolves to group.ch.healthchat, the same value as $(APP_GROUP_IDENTIFIER)
# below. The legacy container is already entitled; listing it twice would duplicate the entry.
# HealthChat: dropped $(CLASSIC_APP_GROUP_IDENTIFIER) (group.im.vector) — owned by Element's team, not provisionable here.
- $(APP_GROUP_IDENTIFIER)
com.apple.security.network.client: true
keychain-access-groups:
# HealthChat: dropped $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER) (im.vector.app.*) — not provisionable here.
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
# HealthChat: the legacy app's keychain group. ClassicAppManager reads the account and
# crypto-store keys from here to migrate existing users. Same team prefix, so it is
# provisionable — unlike Element's im.vector.app.* default in app.yml.
- $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
settings:
base:
-6
View File
@@ -1,6 +0,0 @@
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
-350
View File
@@ -1,350 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.8)
abbrev (0.1.2)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1269.0)
aws-sdk-core (3.254.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.130.0)
aws-sdk-core (~> 3, >= 3.254.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.227.0)
aws-sdk-core (~> 3, >= 3.254.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
benchmark (0.5.0)
bigdecimal (4.1.2)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (1.5.0)
logger
faraday (1.10.6)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.4)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.1)
fastlane (2.237.0)
CFPropertyList (>= 2.3, < 5.0.0)
abbrev (~> 0.1)
addressable (>= 2.9.0, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.197)
babosa (>= 1.0.3, < 2.0.0)
base64 (~> 0.2)
benchmark (>= 0.1.0)
bundler (>= 2.4.0, < 5.0.0)
colored (~> 1.2)
commander (~> 4.6)
csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 2.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.1.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.3.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.10.3, < 4)
logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_json (~> 1.12)
multipart-post (>= 2.0.0, < 3.0.0)
mutex_m (~> 0.3)
naturally (~> 2.2)
nkf (~> 0.2)
optparse (>= 0.1.1, < 1.0.0)
ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-plugin-brew (0.1.1)
fastlane-plugin-xcodegen (1.1.0)
fastlane-plugin-brew (~> 0.1.1)
fastlane-sirp (1.1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.104.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0)
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.28.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-playcustomapp_v1 (0.18.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.64.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.9.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (2.2.2)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.7.0)
google-cloud-storage (1.62.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (>= 0.18, < 2)
google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6)
googleauth (~> 1.9)
mini_mime (~> 1.0)
google-logging-utils (0.2.0)
googleauth (1.17.1)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 4.0)
os (>= 0.9, < 2.0)
pstore (~> 0.1)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.21.1)
jwt (3.2.0)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.21.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.3.0)
optparse (0.8.1)
os (1.1.4)
ostruct (0.6.3)
plist (3.7.2)
pstore (0.2.1)
public_suffix (7.0.5)
rake (13.4.2)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.8.0)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.22.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 4.0)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.28.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
base64
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
nkf
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
arm64-darwin-25
ruby
DEPENDENCIES
fastlane
fastlane-plugin-xcodegen
CHECKSUMS
CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261
abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263
atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
aws-partitions (1.1269.0) sha256=80beca6c8a35ddfc11b0b529424ea6167ace3da92cd7368847ce694679dedea5
aws-sdk-core (3.254.0) sha256=ee3e3220b8468a3c9e59daba18e6ec897bf5c7ce8adcc0670cfa2f1f092112fe
aws-sdk-kms (1.130.0) sha256=a2e83662ca31b77a2a19c9aa2f40a98165a67270c18c718fe1c70d0cbd7cd749
aws-sdk-s3 (1.227.0) sha256=552b23bf9a37c7db4957e9291543e61c8de886cf31d1d45ea3b429df0feea939
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e
colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c
colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a
commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9
digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8
emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b
excon (1.5.0) sha256=c503ad1d0123bc8ab2a062ff3789dc891ec368cb9e13765ab88a9c58c8bb6d50
faraday (1.10.6) sha256=7ff4802a6b312876a2241b3e641ce0d5045e168dd871b422c35b505e5261ad4d
faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb
faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689
faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750
faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940
faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b
faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757
faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682
faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335
faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7
faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0
faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa
faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9
fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20
fastlane (2.237.0) sha256=bb1e867bc070fb328741b5e6e7606d5ba596941a9ecca0478f60ef22d1009db9
fastlane-plugin-brew (0.1.1) sha256=c2d1c98ead82f78818621b5b1eb123e0bf9da7e1a7b3efeaac77a776725a7fbf
fastlane-plugin-xcodegen (1.1.0) sha256=9bbed6c2ea21ca6869567e350d0b62cf41770949d15e40869058d689f1a6d06d
fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847
gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939
google-apis-androidpublisher_v3 (0.104.0) sha256=3bf7f0dee23ae71e070e279848374834853204e304831cf6aa76fa435a4d6eff
google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee
google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106
google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254
google-apis-storage_v1 (0.64.0) sha256=75b11afa2edcee859b84c7a6972ee4456314eeef5f762827fd6cf5c5ffaf93f2
google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4
google-cloud-env (2.2.2) sha256=94bed40e05a67e9468ce1cb38389fba9a90aa8fc62fc9e173204c1dca59e21e7
google-cloud-errors (1.7.0) sha256=6e682f42d89aae08689f36f28495de629d756da076bafff0003bac7f8042ebb3
google-cloud-storage (1.62.0) sha256=e2c3c08bf8fd40d50be92304084942203314d4fc0ee52028e99f9359c3ad1330
google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b
googleauth (1.17.1) sha256=0f7e6fc70e204cee1b2d71f1e1de2d3b349d432404197fe68ebf7fa23d0821b9
highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479
http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6
httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583
jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080
multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723
naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01
nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895
optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a
os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42
pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace
retriable (3.8.0) sha256=9f2f1b0207594c7817f17f671587b8ec7587387ac6cebda6c941a802bb98a8e5
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20
ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef
rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615
security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7
signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780
simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b
terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea
terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91
trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3
tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48
tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50
tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542
uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc
unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7
xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc
xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892
xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93
BUNDLED WITH
4.0.3
-16
View File
@@ -1,16 +0,0 @@
# HealthChat Changes
Release notes for HealthChat builds of Element X. Each release adds a section at the
top; the release workflow extracts the one matching the version being released and
uses it as the TestFlight "What to Test" text.
The heading format is load-bearing — `.github/workflows/release-healthchat.yml`
matches `## Changes in <version>` exactly.
Upstream's `CHANGES.md` tracks element-hq's own releases and must not be edited here.
## Changes in 26.07.3 (2026-07-15)
🙌 Improvements
- First HealthChat release built from Element X.
-14
View File
@@ -55,16 +55,6 @@ struct RunTests: AsyncParsableCommand {
"com.apple.CoreSimulator.SimRuntime.iOS-\(runtime.replacingOccurrences(of: ".", with: "-"))"
}
/// Test identifiers to pass to `-skip-testing`, read from the `CI_SKIP_TESTS` environment variable
/// (comma-separated, e.g. "VoiceMessageCacheTests"). Lets a workflow exclude a suite that only fails in
/// its environment without touching the shared test.
private var skippedTests: [String] {
(ProcessInfo.processInfo.environment["CI_SKIP_TESTS"] ?? "")
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
}
func run() async throws {
if let createName = createSimulatorName {
guard let createType = createSimulatorType else {
@@ -152,10 +142,6 @@ struct RunTests: AsyncParsableCommand {
command += " -only-testing:\(scheme)/\(testName)"
}
for skippedTest in skippedTests {
command += " -skip-testing:\(scheme)/\(skippedTest)"
}
command += " | \(formatter)"
try await CI.run(.path("/bin/zsh"), ["-cu", command])
@@ -238,8 +238,8 @@ final class AuthenticationStartScreenViewModelTests {
}
@Test
func classicAppAccountRequiresBackupFallsThroughToLogin() async throws {
// Given a view model with a Classic app account that has no usable key backup.
func classicAppAccountRequiresBackup() async throws {
// Given a view model with a Classic app account that requires backup before signing in.
let classicAppAccount = makeClassicAppAccount()
await setupViewModel(classicAppAccount: classicAppAccount, availableSecrets: .requiresBackup)
guard case .welcomeBack(let account) = context.viewState.classicAppMode else {
@@ -248,15 +248,27 @@ final class AuthenticationStartScreenViewModelTests {
}
#expect(account.state.availableSecrets == .requiresBackup)
// When continuing with the Classic account.
let deferred = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithOAuth }
// When continuing with the Classic account while backup is required.
var deferred = deferFulfillment(context.observe(\.viewState.bindings.showClassicAppBackupInstructions)) { $0 }
context.send(viewAction: .continueWithClassic(classicAppAccount))
// Then the flow continues to a normal sign-in rather than the backup instructions sheet.
// HealthChat replaces the Classic app in place, so those instructions could never be followed.
// Then the backup instructions should be shown.
try await deferred.fulfill()
#expect(context.viewState.bindings.showClassicAppBackupInstructions == false)
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "mxid:\(classicAppAccount.userID)")
// When the user completes the backup in the Classic app and the app returns to the foreground.
classicAppManager?.availableSecretsForReturnValue = .complete
deferred = deferFulfillment(context.observe(\.viewState.bindings.showClassicAppBackupInstructions)) { !$0 }
notificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil)
// Then the backup instructions sheet should be dismissed.
try await deferred.fulfill()
// When the user continues with the Classic account again.
let deferredAction = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithOAuth }
context.send(viewAction: .continueWithClassic(classicAppAccount))
// Then the flow should continue the login process.
try await deferredAction.fulfill()
}
// MARK: - Helpers
-8
View File
@@ -11,11 +11,3 @@ settings:
APP_GROUP_IDENTIFIER: group.ch.healthchat
BASE_BUNDLE_IDENTIFIER: ch.healthchat.element
DEVELOPMENT_TEAM: 6G37K72W82
# The legacy element-ios HealthChat's identifiers. ClassicAppManager reads the account and
# crypto-store keys from these to migrate existing users on an in-place update. Upstream's
# defaults in app.yml point at Element's own (group.im.vector / im.vector.app.*), which this
# team cannot provision — repoint, never remove. Failure here is silent.
CLASSIC_APP_GROUP_IDENTIFIER: group.ch.healthchat
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER: ch.healthchat.element.encryption-manager-service
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER: "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,445 @@
# HealthChat (element-x-ios) — App Store release capability
**Date:** 2026-07-15
**Branch:** `release/healthchat`
**Status:** Design approved; revised after adversarial review (26 issues, 0 rejected)
## Context
`Novaloop-AG/element-x-ios` is the HealthChat rebrand of Element X. The rebrand landed in `01dc703b4`
(branding, fixed homeserver, variant seam). What it cannot do is ship: the repo has **no release machinery
of any kind** — no `fastlane/`, no `Gemfile`, and none of its 17 GitHub workflows build or upload anything.
The legacy fork `Novaloop-AG/element-ios` ships HealthChat to the App Store today via a proven pipeline
(last green run 2026-04-15, version 1.4.1). This design brings equivalent capability to element-x-ios and
captures the process in a skill.
Both apps use **the same bundle id** (`ch.healthchat.element`) and **the same Apple team** (`6G37K72W82`).
element-x therefore reaches the App Store as an **in-place update** of the existing HealthChat record. That
single fact drives most of this design — including the fact that the two apps can never coexist on a
device, which upstream's migration UX assumes they can.
### Why upstream's own release path is unavailable
element-hq ships element-x via Xcode Cloud. `ci_scripts/ci_post_clone.sh` branches on `$CI_WORKFLOW` and
calls `swift run pipeline update-foss-secrets` (`Tools/Sources/Commands/CI/ConfigureProduction.swift:8`).
`pipeline` lives only in the private `Enterprise` submodule (`.gitmodules` → `element-hq/element-ios-enterprise`;
`Enterprise/` is empty here and Novaloop cannot obtain it). Separately `ci_scripts/ci_post_xcodebuild.sh:13`
unconditionally uploads dSYMs to Element's Sentry org. Xcode Cloud is a dead end for this fork.
## Decisions
| # | Decision | Rationale |
|---|---|---|
| D1 | **In-place update** of the existing App Store record | Identity already aligns. `26.07.3` already clears the legacy's `1.4.1`, so no version bump is needed. |
| D2 | **Preserve** legacy sessions + E2EE keys | The migration code already ships; the fix is 3 scalars + 1 entitlement, all HealthChat's own provisionable ids. |
| D3 | **Port the legacy fastlane** (slim, purpose-built) | Xcode Cloud is blocked. The legacy pipeline is proven and reuses the identical team, cert and ASC key. |
| D4 | Migration fix ships **first, as its own PR** | It corrects a defect in `01dc703b4`. **Note: it is _not_ monotonic — see D9.** |
| D5 | Skill lives at `.agents/skills/releasing-healthchat/SKILL.md` | Reachable via the existing `.claude/skills -> ../.agents/skills` symlink. A new subdirectory cannot conflict on rebase. |
| D6 | Reuse the legacy's `HEALTHCHAT_*` secrets | Same team, cert and ASC key. **Reject fastlane match** — the legacy deliberately has none. |
| D7 | Keep the `healthchat-v*` tag namespace | Upstream owns `release/<version>` (`ReleaseToGithub.swift:83-85`) and `nightly/*`. |
| D8 | **Create** `HEALTHCHAT_CHANGES.md` as the release-notes source | element-x's `CHANGES.md` is owned by upstream's `release-to-github`. The file does **not exist** in this repo — it must be created, not carried over. |
| D9 | Fix the `.requiresBackup` dead-end with a **small upstream edit** | Accepts the first-ever edit to an upstream logic file. Required for D4 to be safe — see Part 1. |
---
## Part 1 — Classic migration fix (PR1, `pr-bugfix`)
### The defect
`01dc703b4` dropped `$(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)` from the entitlements with the
rationale "not provisionable here". That was correct *about Element's identifiers* — but the fix is to
**repoint them at HealthChat's own**, not to remove them.
Left as-is, every existing user who updates is silently logged out and loses local encrypted history
unless they had key backup. `loadAccounts()` throws `invalidAppGroupIdentifier`, caught and logged only —
`AuthenticationService.swift:59-63`: *"No need to alert the user of the failure, just log it."*
### Why the fix is small
The plumbing already ships. `ElementX/SupportingFiles/target.yml:78-81` maps the values into `Info.plist`;
`InfoPlistReader.swift:28-31,129-143` reads them. `ClassicAppManager` reads exactly `accountIv`,
`accountAesKey`, `cryptoSDKStoreKey` (`ClassicAppManager.swift:31-35,61-68`) — and the legacy
`EncryptionKeyManager` writes exactly those three names, to service
`ch.healthchat.element.encryption-manager-service`, in access group
`$(AppIdentifierPrefix)ch.healthchat.element.keychain.shared`
(`element-ios/Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift:17,20-25,27`).
Only the scalars are wrong: `app.yml:11-13` points them at Element's `group.im.vector` / `im.vector.app.*`,
and `healthchat.yml` does not override them.
**Verified mechanics** (these were the design's load-bearing assumptions):
- **XcodeGen scalar precedence.** `healthchat.yml` is included after `app.yml` (`project.yml:71` vs `:61`)
and wins — proven by the already-generated pbxproj, where `APP_GROUP_IDENTIFIER = group.ch.healthchat`
and `BASE_BUNDLE_IDENTIFIER = ch.healthchat.element` beat app.yml's Element values.
- **No trailing-dot bug.** `$(AppIdentifierPrefix)` carries its own trailing dot, so the legacy resolves to
`6G37K72W82.ch.healthchat.element.keychain.shared`. `$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared`
is byte-equivalent and matches upstream's own convention (`app.yml:13`).
### The change
In `Variants/HealthChat/healthchat.yml`:
```yaml
CLASSIC_APP_GROUP_IDENTIFIER: group.ch.healthchat
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER: ch.healthchat.element.encryption-manager-service
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER: "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared"
```
In `ElementX/SupportingFiles/target.yml`, restore the classic keychain group to `keychain-access-groups`
and correct the misleading comment at `:132`:
```yaml
keychain-access-groups:
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
# HealthChat: the legacy app's keychain group — ClassicAppManager reads the account + crypto store
# keys from here to migrate existing users. Same team prefix, so it is provisionable.
- $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
```
**Do not** re-add `$(CLASSIC_APP_GROUP_IDENTIFIER)` to `com.apple.security.application-groups`: it resolves
to `group.ch.healthchat`, already the value of `$(APP_GROUP_IDENTIFIER)`, so it would duplicate the entry.
Correct the comment at `:128` to say this rather than implying the group was dropped as un-provisionable.
`CLASSIC_APP_DEEP_LINK_URL` needs no override — legacy HealthChat registers the `element` scheme
(`element-ios/Config/AppIdentifiers.xcconfig:22`) — but see the `.requiresBackup` fix below, which makes it
moot.
**Finally: run `xcodegen` and commit both `ElementX.xcodeproj/project.pbxproj` and
`ElementX/SupportingFiles/ElementX.entitlements`.** Both are tracked and not ignored. Without this the
checked-in entitlements still lack the classic group and anyone building the branch gets the un-fixed,
silently-failing behaviour.
### The `.requiresBackup` fix (D9) — why Part 1 is not monotonic
`availableSecrets` maps `.withoutBackup` / `.unusedBackup` → `.requiresBackup`
(`ClassicAppManager.swift:82-85`). `login(classicAppAccount:)` routes that to
`state.bindings.showClassicAppBackupInstructions`, whose sheet's only button fires `.openClassicApp` →
`appMediator.open(element://open)` (`AuthenticationStartScreenViewModel.swift:106`).
**That URL resolves to nothing.** element-x registers only `$(BASE_BUNDLE_IDENTIFIER)` and `matrix`
(`ElementX/SupportingFiles/target.yml:60-63`) — no `element` scheme — and the legacy app that *did*
register it has been replaced in place by D1's same-bundle-id update. Upstream's UX assumes Element Classic
(`im.vector.app`) and Element X (`io.element.elementx`) coexist as separate apps. HealthChat cannot.
So making `loadAccounts()` succeed switches on a UI branch that is worse than the status quo for
`.requiresBackup` users: today they see a working login screen; after the fix they see a modal urging them
to open an app that no longer exists. The sheet is dismissible (`@Environment(\.dismiss)`) and `otherOptions`
(`AuthenticationClassicAppAccountView.swift:106`) is an escape hatch, so it is not a hard lockout — but it
is a knowingly broken call-to-action.
**Fix:** in `AuthenticationStartScreenViewModel.login(classicAppAccount:)`, treat `.requiresBackup` the same
way `.unavailable` is already treated — skip the sheet and fall through to
`configureAccountProvider(account.serverName, loginHint: "mxid:\(account.userID)", fallbackHomeserverURL:)`.
Those users sign into their own account and re-verify the device, losing only history that by definition has
no backup.
This is the **first edit to an upstream logic file** in this fork, ending the rebrand's "no upstream logic
edits" property. It is ~3 lines in a method upstream rarely touches. Record it in the skill's divergence
section so a future rebase does not silently revert it.
### Precondition for D2 (verified, but confirm on device)
`AuthenticationStartScreenViewModel:46` gates the whole classic flow on
`appSettings.accountProviders.contains(serverName)`, where `serverName` is derived from the **MXID domain**
(`ClassicAppAccountManager.swift:107-111`), not the homeserver URL. HealthChat pins
`accountProviders: ["healthchat.ch"]`.
`curl https://healthchat.ch/.well-known/matrix/server` → `{"m.server": "matrix.healthchat.ch:8448"}`. This is
*server-name delegation*: the server is **named** `healthchat.ch` and merely reachable at
`matrix.healthchat.ch:8448`. Legacy MXIDs are therefore `@user:healthchat.ch`, the gate passes, and D2 holds.
Confirm against a real legacy MXID during device testing.
---
## Part 2 — Release pipeline (PR2, `pr-build`)
### Design note: this fork owns `fastlane/` outright
Upstream element-x-ios has **no `fastlane/` directory**. The legacy skill devotes a whole section to rebase
divergence in `fastlane/.env.default` and `fastlane/Fastfile` precisely because those are upstream-owned in
element-ios. Here that class of conflict does not exist — which is why this is a slim purpose-built Fastfile
(~120 lines), not a copy of Vector's ~600-line multi-variant one.
**Dropped:** cocoapods (element-x is SPM), diawi, sentry (HealthChat has no Sentry), and the `adhoc` /
`alpha` / `setup_app_variant` lanes. Siri-intents and broadcast-upload have no counterpart target here.
### Files
| File | Purpose |
|---|---|
| `fastlane/Fastfile` | `before_all` (Xcode pin) + public `release_testflight` + private helpers |
| `fastlane/Appfile` | `apple_id ENV["APPLE_ID"]`, `team_id ENV["TEAM_ID"]` |
| `fastlane/.env.default` | Project/target/bundle-id/profile configuration (full key list below) |
| `fastlane/Pluginfile` | **`fastlane-plugin-xcodegen` only** |
| `Gemfile` + `Gemfile.lock` | `fastlane` + plugins. No cocoapods, no slather. **Commit the lock** — the workflow's gem cache keys on `hashFiles('**/Gemfile.lock')`; without it the key is constant and fastlane silently re-resolves between two runs of the same tag. |
| `Brewfile` | `xcodesorg/made/xcodes`. **Not** `xcodegen` — see runner notes. |
| `HEALTHCHAT_CHANGES.md` | **New file** (D8). Seed with a `## Changes in 26.07.3 (<date>)` section matching the awk pattern. |
| `.gitignore` | Add `out/` and **`provisioning_profiles/`** (signing material — security-relevant). `build` (`:35`) and `fastlane/report.xml` (`:28`) are already covered upstream. |
**Drop `fastlane-plugin-versioning`.** The legacy carries it but never uses it — its `update_marketing_version`
is raw `File.read`/`gsub!`, and no `increment_version_number` / `get_version_number` / `update_info_plist`
call exists anywhere. Copying it into a file we call "slim and purpose-built" would propagate dead weight.
### `.env.default`
```
PROJECT_PATH=ElementX.xcodeproj
SCHEME=ElementX
MAIN_TARGET=ElementX
NSE_TARGET=NSE
SHARE_EXTENSION_TARGET=ShareExtension
MAIN_BUNDLE_ID=ch.healthchat.element
NSE_BUNDLE_ID=ch.healthchat.element.nse
SHARE_EXTENSION_BUNDLE_ID=ch.healthchat.element.shareextension
DERIVED_DATA_PATH=build
IPA_NAME=ElementX
ARCHIVE_PATH=out/ElementX.xcarchive
BUILD_OUTPUT_DIRECTORY=out
BUILD_LOG_DIRECTORY=build/Logs/Gym/
PROVISIONING_PROFILES_PATH=./provisioning_profiles/
MAIN_PROVISIONING_PROFILE_FILENAME=main.mobileprovision
NSE_PROVISIONING_PROFILE_FILENAME=nse.mobileprovision
SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME=share_extension.mobileprovision
APPSTORE_CODESIGNING_IDENTITY="Apple Distribution: E-HealthSolutions GmbH (6G37K72W82)"
APPSTORE_SIGNING_CERTIFICATE="Apple Distribution: E-HealthSolutions GmbH (6G37K72W82)"
APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER="HealthChat App Store"
APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER="HealthChat NSE: App Store"
APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="HealthChat Share Extension: App Store"
TEAM_ID=6G37K72W82
# APPLE_ID is a machine-level shell variable on the runner, not a repo secret.
```
`APPSTORE_CODESIGNING_IDENTITY` and `APPSTORE_SIGNING_CERTIFICATE` are **distinct keys** in the legacy — both
are needed. The `out/ElementX.ipa` artifact path is only true because `BUILD_OUTPUT_DIRECTORY`/`IPA_NAME` are
set here.
Verified: bundle ids from `NSE/SupportingFiles/target.yml:75` and `ShareExtension/SupportingFiles/target.yml:77`;
schemes/targets from `ElementX/SupportingFiles/target.yml:3-4,41`, `NSE/SupportingFiles/target.yml:3-4,30`,
`ShareExtension/SupportingFiles/target.yml:3-4,30`.
### `release_testflight` lane
Mirrors `element-ios/fastlane/Fastfile:199-224`: accept inputs via options (local) or env vars (CI, avoiding
shell-escaping of multi-line notes), then:
1. `app_store_connect_api_key(...)` from `APPSTORECONNECT_KEY_{ID,ISSUER_ID,CONTENT}` when `is_ci`.
2. `generate_build_number` → `YYYYMMDDHHMMSS` (ported from `Fastfile:562`). Monotonic; makes retries free.
3. `xcodegen(spec: "project.yml")`.
4. `download_provisioning_profiles` → three profiles (main, NSE, ShareExtension).
5. `disable_automatic_code_signing` + `update_project_provisioning` per target.
6. `build_app`, `export_method: "app-store"`, `CODE_SIGN_STYLE=Manual`.
7. `upload_to_testflight(skip_submission: true, distribute_external: false, changelog: release_notes)`.
**Version injection.** element-x holds `MARKETING_VERSION: 26.07.3` / `CURRENT_PROJECT_VERSION: 1` in
`project.yml:56-57`, consumed by XcodeGen at *generate* time (unlike the legacy's `AppVersion.xcconfig`, read
at *build* time). Pass both via `xcargs` to `build_app` instead of rewriting `project.yml`. Verified sound:
NSE (`:51-52,76-77`) and ShareExtension (`:46-47,78-79`) both resolve their version keys from
`$(MARKETING_VERSION)`/`$(CURRENT_PROJECT_VERSION)` at build time, and xcodebuild command-line settings
override project *and* target level uniformly — so all three targets stay version-consistent. This matters:
App Store rejects an upload whose extension version disagrees with the app's.
This spares `project.yml`, but **it does not mean "no tracked file is mutated"** — `ElementX.xcodeproj/project.pbxproj`
is tracked and steps 3 and 5 rewrite it (xcodegen regenerates; `disable_automatic_code_signing` /
`update_project_provisioning` write Manual signing and profile UUIDs). The pbxproj footgun **moved, it did not
vanish**. The skill must say: never commit the post-lane tree; `git checkout -- ElementX.xcodeproj`.
### GitHub workflow
`.github/workflows/release-healthchat.yml` — near-copy of the legacy's, which is already well-built (pinned
action SHAs, `contents: read`, concurrency group, 90-minute timeout, awk notes extraction, artifact upload).
Changes from the legacy:
- **`submodules: false`** — critical. element-x-ios's only submodule is the **private**
`element-hq/element-ios-enterprise` over SSH; the legacy's `submodules: 'true'` would fail checkout on step
one of every run.
- Drop the CocoaPods cache and `rm -rf Pods/MatrixSDK`.
- Drop `MX_GIT_BRANCH` (legacy `:41`) — no `mx_git_branch` helper exists in the slim Fastfile.
- Re-evaluate `fetch-depth: 0` (legacy `:48`); keep only if something needs history.
- Keep the explicit `brew bundle` step (legacy `:115`).
- `bundle exec fastlane release_testflight`; artifacts `out/ElementX.ipa`, `out/ElementX.app.dSYM.zip`.
- Harden the notes step with `[[ -f HEALTHCHAT_CHANGES.md ]]`. The legacy's "warning fallback" only fires on
an *empty match*, not an absent file — under `set -euo pipefail` a missing file aborts the job.
**Do not carry `HEALTHCHAT_KEYCHAIN_PASSWORD` forward as mandatory.** The legacy skill lists it among six
required secrets and the legacy workflow references it, but it exists at neither repo nor org level — and the
pipeline runs green regardless. `apple-actions/import-codesign-certs` tolerates its absence. **Five secrets is
the real requirement.**
### Runner
`novaloop-mac-studio-001` — org-level, online, labels `[self-hosted, macOS, ARM64]`; already satisfies
`runs-on: [self-hosted, macOS]`. Confirm the org runner group's visibility includes element-x-ios (two
`Default` groups exist, one `all`, one `selected`).
**Xcode pin: use `"26.4"`.** The runner has it and the legacy pipeline proves it works.
`ci_scripts/ci_common.sh:34` (`sudo xcode-select -s /Applications/Xcode_26.5.0.app`) is an **Xcode Cloud image
path, not an `xcodes` version identifier**, and is not evidence the runner has 26.5. If element-x turns out to
need 26.5, install it on the runner manually **first** — otherwise `xcodes` attempts a multi-GB authenticated
download needing `XCODES_USERNAME`/`XCODES_PASSWORD`, which none of the five secrets supply, and hangs in
`before_all`.
Also required on the runner: `xcodegen` on PATH (installed manually once, deliberately **not** in `Brewfile`
so `brew bundle` cannot churn it), Homebrew, Ruby + bundler, and `APPLE_ID` exported as a machine-level shell
variable (the `Appfile` reads it; it is not repo-carried).
### Secrets to copy
element-x-ios has **zero** repository secrets. Copy these five from `Novaloop-AG/element-ios`:
`HEALTHCHAT_ASC_KEY_ID`, `HEALTHCHAT_ASC_KEY_ISSUER_ID`, `HEALTHCHAT_ASC_KEY_CONTENT`,
`HEALTHCHAT_CERTIFICATES_P12`, `HEALTHCHAT_CERTIFICATES_P12_PASSWORD`.
Requires the `snowping` GitHub account — `Novaloop-AG/element-x-ios` is INTERNAL and invisible to
`joel-poyet_GALGH`.
---
## Part 3 — Release skill (PR3, `pr-doc`)
`.agents/skills/releasing-healthchat/SKILL.md`, reachable as a project skill through the existing
`.claude/skills -> ../.agents/skills` symlink.
Mirror the legacy skill's shape — it is a good model: two-field frontmatter (`name`, `description`), terse
imperative prose, checklists and tables, **rationale inline on every prohibition**. Sections: preflight,
cutting a release, `workflow_dispatch` retry path, tag format, release notes, post-release checks, required
secrets, runner requirements, certificate renewal, TestFlight → App Store promotion, rollback, divergence.
Corrected for this repo:
- **Five** required secrets, not six.
- Version source is `project.yml:56`, not `Config/AppVersion.xcconfig`.
- Three profiles (main, NSE, **ShareExtension**), not main/NSE/broadcast.
- **Never commit the post-lane tree** — `git checkout -- ElementX.xcodeproj` after a local run.
- **Test versions must be numeric-only** (see Verification) — this trap costs 30 minutes per hit.
- The divergence section shrinks: `fastlane/` and the release workflow are fork-only here, so the legacy's
`--ours` guidance for `.env.default`/`Fastfile` does not apply. What *does* need calling out:
`automatic-calendar-version.yml` and `post-release.yml` are gated on
`github.repository == 'element-hq/element-x-ios'` and silently no-op in the fork, so rebases drag
`project.yml`'s version along — **the tag is the source of truth**.
- **New section — the migration contract** (no legacy equivalent): `CLASSIC_APP_*` in `healthchat.yml` is
load-bearing for existing users and fails *silently*; it must never be "cleaned up" to upstream's values.
The `.requiresBackup` edit in `AuthenticationStartScreenViewModel` is a deliberate upstream divergence — a
rebase that reverts it reintroduces a broken CTA. Both apps share the `group.ch.healthchat` UserDefaults
suite (`AppSettings.swift:56`), so unqualified `@UserPreference` property names land in a suite the legacy
also writes to — future key collisions are cheap to introduce.
Add a 2-line delegation in `AGENTS.md` pointing at the skill rather than duplicating the runbook — the legacy
does exactly this (`element-ios/CLAUDE.md:288-290`). element-x's `AGENTS.md` has no release section today, and
per its own "keep current" rule this is a new fact about the repo, so it belongs there, in caveman voice.
---
## Landing plan
`Dangerfile.swift:50-51` **fails** the PR unless it carries exactly one `pr-` label. (The empty-body rule
`:24` and the >1000-additions rule `:18-19` are `warn`s, not fails.)
| PR | Contents | Label |
|---|---|---|
| PR1 | Part 1 — `CLASSIC_APP_*`, entitlement, `.requiresBackup` edit, regenerated pbxproj | `pr-bugfix` |
| PR2 | Part 2 — fastlane, workflow, Gemfile(.lock), Brewfile, .gitignore, HEALTHCHAT_CHANGES.md | `pr-build` |
| PR3 | Part 3 — skill + AGENTS.md delegation | `pr-doc` |
AGENTS.md's 500-addition guidance argues for keeping these separate rather than combining PR2 and PR3.
---
## Apple Developer portal prerequisites
Manual, one-time, **blocking** — no signed archive is possible until all three are done.
1. **Create App ID `ch.healthchat.element.shareextension`** with App Groups + Keychain Sharing, then the
`HealthChat Share Extension: App Store` profile. Genuinely new: the legacy provisioned
`ch.healthchat.element.shareExtension` — **camelCase, a different string** — and disabled the target anyway
(`element-ios/fastlane/.env.default`: `SHARE_EXTENSION_TARGET=`). element-x embeds ShareExtension
unconditionally (`ElementX/SupportingFiles/target.yml:227-228`).
2. **Add `com.apple.developer.usernotifications.communication`** to `ch.healthchat.element`; regenerate
`HealthChat App Store`. New vs. legacy (absent from `Riot/SupportingFiles/Riot.entitlements`).
3. **Add `com.apple.developer.usernotifications.filtering`** to `ch.healthchat.element.nse`; regenerate
`HealthChat NSE: App Store`. New vs. legacy (`RiotNSE.entitlements` has only app-groups + keychain).
The `HealthChat Broadcast Upload Extension: App Store` profile becomes orphaned — leave it; the legacy
pipeline still uses it.
Certificate: **not due.** `HEALTHCHAT_CERTIFICATES_P12` was updated 2026-04-07 and the workflow ran green
2026-04-15. Apple Distribution certs last one year → renewal due around **April 2027**.
---
## Verification
### Part 1 — device-only
**Step 0 (or the test silently false-negatives).** `AuthenticationService.swift:49-52` short-circuits
`loadAccounts()` entirely when `appSettings.hasSignedInBefore` — which lives in the `group.ch.healthchat`
UserDefaults suite (`AppSettings.swift:56`), survives app deletion via the shared container, and is never
cleared. **Any dev device that has previously run a HealthChat element-x build will show a plain login screen
while the fix works perfectly.** Delete the app before step 1. A `"skipping loadAccounts"` log line means the
run is **void, not failed**.
1. Install the *shipping* legacy HealthChat on a physical device. Sign in. Send an encrypted message so the
crypto store is populated.
2. Build element-x with the fix and install **over it** — same bundle id → in-place update. **Do not delete
the app first**; that destroys the keychain and container and invalidates the test.
3. Expected: the app finds the account and migrates without a re-login.
4. Confirm the legacy MXID is `@user:healthchat.ch` (the D2 precondition).
5. Exercise **both** secrets paths if possible: `.complete` (migrates) and `.requiresBackup` (must fall
through to sign-in, never the backup sheet).
6. Watch the log for `ClassicAppManagerError` — the failure is silent in the UI.
7. **Send a push notification to the migrated device.** Part 2 adds two brand-new notification entitlements
and a first-ever NSE profile; nothing else exercises delivery, and upload proves signing, not push.
### Part 2 — pipeline
Use `workflow_dispatch` with a throwaway version, as the legacy skill prescribes for cert renewal:
1. Run "Release HealthChat" manually with version **`26.7.99`** and a release-notes override.
**The version must be numeric-only.** It lands verbatim in `CFBundleShortVersionString` (`Info.plist:36-37`)
and Apple requires ≤3 period-separated integers. A marker like `26.07.3-pipeline-test` passes the
workflow's own regex (`:59`) but earns an **ITMS rejection at the final step, after ~30 minutes of
signing** — indistinguishable from a real signing failure. Put markers in the tag or release notes only.
2. Green run → build appears in App Store Connect → TestFlight within ~10 minutes.
3. Discard the build.
Do **not** verify by submitting to the App Store. TestFlight acceptance proves signing, entitlements and
upload; review proves nothing extra about the pipeline.
`swiftformat --lint .` and SwiftLint run on build and are currently clean.
---
## Risks & open questions
| # | Risk | Status |
|---|---|---|
| **O1** | **Was the legacy build on the Rust crypto store?** `cryptoSDKStoreKey` is generated unconditionally (`EncryptionKeyManager.swift:42`), but whether the store is Rust-backed decides if `SecretsBundleWithUserId.fromDatabase` can read it. If it was still Olm, D2 collapses to "forced re-login" and needs product sign-off. **Settle on a real device before the first submission.** |
| **O2** | **iOS floor rises 15.1 → 18.5** (`element-ios/Config/Project.xcconfig:29` vs `project.yml:14-15`). Every user below 18.5 silently stays on 1.4.1 forever — the App Store will not offer the update. Not fixable (upstream's floor); check the install base before shipping. |
| **O3** | **Pusher overwrite.** element-x's `pusherAppID` and device pushkey are identical to the legacy's, so first launch overwrites the user's sygnal pusher in place and the format changes to `.eventIdOnly`. Benign under D1 — but a rollback to 1.4.1 loses push. |
| **O4** | **Runner group visibility** — confirm `novaloop-mac-studio-001` is reachable from element-x-ios. |
| **O5** | `Secrets/Secrets.swift` holds placeholders; `Secrets/Secrets.pkl:12-17` wants MAPLIBRE_API_KEY among others. Maps degrade without it. Not a release blocker — track separately. |
| **O6** | Cosmetic Element strings ship in a HealthChat binary, e.g. `BGTaskSchedulerPermittedIdentifiers: io.element.elementx.background.refresh` (`ElementX/SupportingFiles/target.yml:102-104`). Working; cosmetic only. |
**Closed:** the app-group collision question — container paths are disjoint, and the shared
`group.ch.healthchat` UserDefaults suite has a verified-empty key intersection. Captured as a standing hazard
in the Part 3 migration contract rather than an open risk.
### Corrected prior belief
An earlier session recorded that a legacy app-lock PIN could lock the new build out via a shared keychain
group. **Refuted.** The two differ in *group, service and account* — `…keychain.shared` vs `…element`;
`…pin-service` vs `…keychain.sessions`; `pin` vs `appLockPINCode`
(`element-ios/Riot/Modules/SetPinCode/PinCodePreferences.swift:20-24,37-38` vs
`ElementX/.../KeychainController.swift:21-23,34-37`). The groups were never shared. The conclusion
("harmless") was right; the mechanism was wrong — and the wrong mechanism is what motivated dropping the
classic keychain group in `01dc703b4`.
## Out of scope
App Store submission automation (needs `fastlane/metadata/` + HealthChat screenshots, neither exists), German
localisation, the `to.healthchat.ch` links service, blue outgoing bubbles.
-42
View File
@@ -1,42 +0,0 @@
## Project configuration
PROJECT_PATH=ElementX.xcodeproj
SCHEME=ElementX
MAIN_TARGET=ElementX
NSE_TARGET=NSE
SHARE_EXTENSION_TARGET=ShareExtension
MAIN_BUNDLE_ID=ch.healthchat.element
NSE_BUNDLE_ID=ch.healthchat.element.nse
SHARE_EXTENSION_BUNDLE_ID=ch.healthchat.element.shareextension
## Build configuration
DERIVED_DATA_PATH=build
IPA_NAME=ElementX
ARCHIVE_PATH=out/ElementX.xcarchive
BUILD_OUTPUT_DIRECTORY=out
BUILD_LOG_DIRECTORY=build/Logs/Gym/
## Provisioning profile files
PROVISIONING_PROFILES_PATH=./provisioning_profiles/
MAIN_PROVISIONING_PROFILE_FILENAME=main.mobileprovision
NSE_PROVISIONING_PROFILE_FILENAME=nse.mobileprovision
SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME=share_extension.mobileprovision
## App Store code signing
APPSTORE_CODESIGNING_IDENTITY="Apple Distribution: E-HealthSolutions GmbH (6G37K72W82)"
APPSTORE_SIGNING_CERTIFICATE="Apple Distribution: E-HealthSolutions GmbH (6G37K72W82)"
APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER="HealthChat App Store"
APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER="HealthChat NSE: App Store"
APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER="HealthChat Share Extension: App Store"
## Account information
TEAM_ID=6G37K72W82
# APPLE_ID is set as a shell environment variable on the machine running fastlane.
-5
View File
@@ -1,5 +0,0 @@
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile
apple_id ENV["APPLE_ID"]
team_id ENV["TEAM_ID"]
-154
View File
@@ -1,154 +0,0 @@
default_platform :ios
# No Xcode version pin: novaloop-mac-studio-001 carries a single, globally-selected Xcode, so
# whatever xcode-select points at is by definition the one we build with. Pinning bought nothing
# and risked everything — a version the runner lacks sends xcodes into a multi-GB authenticated
# download that no CI secret can satisfy, hanging the release. Change the runner's Xcode instead.
platform :ios do
#### Public ####
desc "Builds the App Store ipa and uploads it to TestFlight (internal testers only)"
# Named release_testflight to avoid colliding with fastlane's built-in :testflight action.
lane :release_testflight do |options|
# Accept inputs via options (local) or env vars (CI — lets the workflow pass multi-line
# release notes without shell-escaping them).
marketing_version = options[:marketing_version] || ENV["HEALTHCHAT_MARKETING_VERSION"]
release_notes = options[:release_notes] || ENV["HEALTHCHAT_RELEASE_NOTES"]
UI.user_error!("'marketing_version' is required (pass the option or set HEALTHCHAT_MARKETING_VERSION)") if marketing_version.to_s.empty?
# Apple caps CFBundleShortVersionString at three period-separated integers. The workflow's
# regex is laxer and accepts markers like 26.07.3-rc1, so catch it here — otherwise the run
# dies at the upload step after ~30 minutes of signing, looking exactly like a signing failure.
unless marketing_version =~ /\A\d+(\.\d+){0,2}\z/
UI.user_error!("'marketing_version' must be 1-3 dot-separated integers (got '#{marketing_version}'). " \
"Apple rejects anything else at upload. Put markers in the tag or release notes instead.")
end
build_number = options[:build_number] || generate_build_number
build_release(marketing_version: marketing_version, build_number: build_number)
upload_to_testflight(
ipa: lane_context[SharedValues::IPA_OUTPUT_PATH],
skip_submission: true,
skip_waiting_for_build_processing: true,
changelog: release_notes,
distribute_external: false,
)
end
#### Private ####
desc "Builds the signed App Store ipa"
private_lane :build_release do |options|
marketing_version = options[:marketing_version]
build_number = options[:build_number]
UI.user_error!("'build_number' parameter is missing") if build_number.to_s.empty?
if is_ci
%w[APPSTORECONNECT_KEY_ID APPSTORECONNECT_KEY_ISSUER_ID APPSTORECONNECT_KEY_CONTENT].each do |key|
UI.user_error!("'#{key}' environment variable should be set to use this lane") if ENV[key].to_s.empty?
end
app_store_connect_api_key(
key_id: ENV["APPSTORECONNECT_KEY_ID"],
issuer_id: ENV["APPSTORECONNECT_KEY_ISSUER_ID"],
key_content: ENV["APPSTORECONNECT_KEY_CONTENT"]
)
else
UI.user_error!("'APPLE_ID' environment variable should be set to use this lane") if ENV["APPLE_ID"].to_s.empty?
end
team_id = CredentialsManager::AppfileConfig.try_fetch_value(:team_id)
UI.user_error!("Failed to fetch the team id from the Appfile") if team_id.to_s.empty?
# Inject the version at build time rather than rewriting project.yml, so no tracked file ever
# carries a release-only value. All three targets resolve CFBundleShortVersionString and
# CFBundleVersion from these settings, which keeps the app and its extensions consistent —
# App Store rejects an upload where they disagree.
xcargs_hash = {
"MARKETING_VERSION" => marketing_version,
"CURRENT_PROJECT_VERSION" => build_number,
"CODE_SIGN_STYLE" => "Manual",
}
xcargs = xcargs_hash.map { |k, v| "#{k}=#{v.to_s.shellescape}" }.join(" ")
xcodegen(spec: "project.yml")
clear_derived_data(derived_data_path: ENV["DERIVED_DATA_PATH"])
download_provisioning_profiles
disable_automatic_code_signing(path: ENV["PROJECT_PATH"])
update_project_provisioning_profiles
build_app(
clean: true,
scheme: ENV["SCHEME"],
xcargs: xcargs,
export_method: "app-store",
derived_data_path: ENV["DERIVED_DATA_PATH"],
archive_path: ENV["ARCHIVE_PATH"],
output_directory: ENV["BUILD_OUTPUT_DIRECTORY"],
output_name: "#{ENV['IPA_NAME']}.ipa",
buildlog_path: ENV["BUILD_LOG_DIRECTORY"],
codesigning_identity: ENV["APPSTORE_CODESIGNING_IDENTITY"],
skip_profile_detection: true,
export_options: {
method: "app-store",
signingStyle: "manual",
teamID: team_id,
signingCertificate: ENV["APPSTORE_SIGNING_CERTIFICATE"],
provisioningProfiles: {
ENV["MAIN_BUNDLE_ID"] => ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"],
ENV["NSE_BUNDLE_ID"] => ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"],
ENV["SHARE_EXTENSION_BUNDLE_ID"] => ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"],
},
},
)
end
desc "Downloads the three App Store provisioning profiles"
private_lane :download_provisioning_profiles do
{
ENV["MAIN_BUNDLE_ID"] => [ENV["APPSTORE_MAIN_PROVISIONING_PROFILE_SPECIFIER"], ENV["MAIN_PROVISIONING_PROFILE_FILENAME"]],
ENV["NSE_BUNDLE_ID"] => [ENV["APPSTORE_NSE_PROVISIONING_PROFILE_SPECIFIER"], ENV["NSE_PROVISIONING_PROFILE_FILENAME"]],
ENV["SHARE_EXTENSION_BUNDLE_ID"] => [ENV["APPSTORE_SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER"], ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"]],
}.each do |bundle_id, (profile_name, filename)|
get_provisioning_profile(
app_identifier: bundle_id,
provisioning_name: profile_name,
ignore_profiles_with_different_name: true,
adhoc: false,
skip_certificate_verification: false,
output_path: ENV["PROVISIONING_PROFILES_PATH"],
filename: filename,
readonly: true,
)
end
end
desc "Points each target at its downloaded provisioning profile"
private_lane :update_project_provisioning_profiles do
{
ENV["MAIN_TARGET"] => ENV["MAIN_PROVISIONING_PROFILE_FILENAME"],
ENV["NSE_TARGET"] => ENV["NSE_PROVISIONING_PROFILE_FILENAME"],
ENV["SHARE_EXTENSION_TARGET"] => ENV["SHARE_EXTENSION_PROVISIONING_PROFILE_FILENAME"],
}.each do |target, filename|
update_project_provisioning(
xcodeproj: ENV["PROJECT_PATH"],
profile: "#{ENV['PROVISIONING_PROFILES_PATH']}#{filename}",
# Anchored: target_filter is a regex, and an unanchored "NSE" or "ElementX" would also
# match any future target containing that substring.
target_filter: "^#{target}$",
build_configuration: "Release",
)
end
end
end
# Generates a build number from the current timestamp. Monotonic across runs, so a retry never
# collides with a build number App Store Connect has already seen.
def generate_build_number
require 'date'
DateTime.now.strftime("%Y%m%d%H%M%S")
end
-5
View File
@@ -1,5 +0,0 @@
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-xcodegen'