mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-09 16:22:55 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54db79d0ad | |||
| 0431543c5c | |||
| f6d80ee936 | |||
| be0e75fe68 | |||
| 3e353857dd | |||
| e01f44cd87 | |||
| 4b3cd2c1d0 | |||
| f741adfbf4 | |||
| 6b81aeef3f | |||
| 2bd8865d4e |
@@ -0,0 +1,298 @@
|
|||||||
|
---
|
||||||
|
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`.
|
||||||
@@ -11,6 +11,11 @@ permissions: {}
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Tests
|
name: Tests
|
||||||
|
# HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, because
|
||||||
|
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
|
||||||
|
# than deleted so the file keeps rebasing cleanly. Run locally with
|
||||||
|
# `swift run tools ci accessibility-tests`.
|
||||||
|
if: github.repository == 'element-hq/element-x-ios'
|
||||||
runs-on: macos-26
|
runs-on: macos-26
|
||||||
timeout-minutes: 150
|
timeout-minutes: 150
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ permissions: {}
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
|
# HealthChat: skip in this fork. It only triggers on changes under compound-ios/, which this fork
|
||||||
|
# has never edited, so it would only ever fire on an upstream sync and go red at checkout —
|
||||||
|
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
|
||||||
|
# than deleted so the file keeps rebasing cleanly.
|
||||||
|
if: github.repository == 'element-hq/element-x-ios'
|
||||||
runs-on: macos-26
|
runs-on: macos-26
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout. Upstream is public, hence permissions: {} there.
|
||||||
|
issues: read # HealthChat: Danger reads the PR via the issues API (a PR is an issue there); pull-requests alone 403s on GET /issues/{n}.
|
||||||
|
pull-requests: write # HealthChat: Danger posts its report comment and inline SwiftLint review comments (Dangerfile.swift:5 sets inline: true) with GITHUB_TOKEN.
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||||
group: ${{ format('danger-ci-{0}', github.ref) }}
|
group: ${{ format('danger-ci-{0}', github.ref) }}
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ permissions: {}
|
|||||||
jobs:
|
jobs:
|
||||||
integration_tests:
|
integration_tests:
|
||||||
name: Integration Tests
|
name: Integration Tests
|
||||||
|
# HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, and it is the one
|
||||||
|
# job that still calls out to Element's Codecov — its upload guard is true on schedule events, so
|
||||||
|
# it POSTs slug/branch/SHA every night with continue-on-error hiding the failure. Fixing it would
|
||||||
|
# need a real healthchat.ch test account and three new secrets, and would still not work:
|
||||||
|
# HealthChatAppSettingsHook sets allowOtherAccountProviders: false, hiding the change-server UI
|
||||||
|
# these tests drive. Gated rather than deleted so the file keeps rebasing cleanly.
|
||||||
|
if: github.repository == 'element-hq/element-x-ios'
|
||||||
runs-on: macos-26
|
runs-on: macos-26
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
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
|
||||||
@@ -15,6 +15,12 @@ permissions: {}
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Tests
|
name: Tests
|
||||||
|
# HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, because
|
||||||
|
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Fixing the
|
||||||
|
# permission would not help: the rebrand changed the start screen without re-recording any
|
||||||
|
# UITests snapshot, so it would fail on content instead. Gated rather than deleted so the file
|
||||||
|
# keeps rebasing cleanly. Run locally with `swift run tools ci ui-tests`.
|
||||||
|
if: github.repository == 'element-hq/element-x-ios'
|
||||||
runs-on: macos-26
|
runs-on: macos-26
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -17,7 +17,14 @@ jobs:
|
|||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
|
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
# HealthChat: this job can never pass in this fork — it needs
|
||||||
|
# secrets.ELEMENT_BOT_TOKEN (Element's bot, absent here) and recursively
|
||||||
|
# checks out the private element-hq/element-ios-enterprise submodule, which
|
||||||
|
# Novaloop cannot obtain. The guard below only asks whether the PR comes
|
||||||
|
# FROM a fork, not whether this repo IS one, so it does not skip us. Gate on
|
||||||
|
# the upstream repo as post-release.yml does. Gated rather than deleted so
|
||||||
|
# the file keeps rebasing cleanly against upstream.
|
||||||
|
if: ${{ github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||||
|
|||||||
@@ -13,24 +13,78 @@ permissions: {}
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: macos-26
|
# HealthChat: GitHub-hosted macos-26 could not finish this job inside
|
||||||
|
# timeout-minutes: 60 (two attempts: 51m42s, then killed at 60m44s), and macOS
|
||||||
|
# minutes bill at 10x. novaloop-mac-studio-001 is the org's Mac Studio, already
|
||||||
|
# used by the release pipeline; its labels are [self-hosted, macOS, ARM64].
|
||||||
|
runs-on: [self-hosted, macOS]
|
||||||
|
# Upstream's 60 is kept: with previews skipped (see Run tests) the build plus unit tests measured
|
||||||
|
# ~16 min here. A run approaching this budget is a signal, not normal — the earlier 60m timeouts
|
||||||
|
# were preview tests, not the build.
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout (and on git lfs pull). Upstream is public, hence permissions: {} there.
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||||
group: ${{ format('unit-tests-{0}', github.ref) }}
|
group: ${{ format('unit-tests-{0}', github.ref) }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
# HealthChat: upstream passes persist-credentials: false here, which the
|
||||||
|
# composite forwards to actions/checkout — stripping the auth header before
|
||||||
|
# its separate "Git LFS Pull" step runs. Upstream can afford that because
|
||||||
|
# its repo is public and git lfs pull works anonymously; this fork is
|
||||||
|
# INTERNAL, so the LFS batch endpoint would 404. Keep the composite's own
|
||||||
|
# default (true) so the pull is authenticated with GITHUB_TOKEN.
|
||||||
- uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
- uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup environment
|
# HealthChat: verify only — never provision. Upstream's
|
||||||
run: source ci_scripts/ci_common.sh && setup_github_actions_environment
|
# setup_github_actions_environment cannot run on this runner: it calls
|
||||||
|
# `sudo xcode-select -s /Applications/Xcode_26.5.0.app`, but the runner has no
|
||||||
|
# passwordless sudo and no such path (only /Applications/Xcode.app exists), and
|
||||||
|
# it calls `brew update && brew install ...` — novaloop-mac-studio-001 is the
|
||||||
|
# shared release machine, so a per-run `brew update` could upgrade xcodegen
|
||||||
|
# underneath the release pipeline. These tools are installed once by hand; CI
|
||||||
|
# only asserts they are present and fails loudly, naming the fix, if not.
|
||||||
|
- name: Verify environment
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
missing=""
|
||||||
|
formulae=""
|
||||||
|
for tool in xcodegen swiftlint swiftformat xcbeautify xcresultparser pkl git-lfs; do
|
||||||
|
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||||
|
missing="$missing $tool"
|
||||||
|
case "$tool" in
|
||||||
|
# Not in homebrew-core; only this tap ships it.
|
||||||
|
xcresultparser) formulae="$formulae a7ex/homebrew-formulae/xcresultparser" ;;
|
||||||
|
*) formulae="$formulae $tool" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
echo "::error::Missing on self-hosted runner:${missing}. Install by hand on novaloop-mac-studio-001, then re-run: brew install${formulae}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# The Xcode version is no longer pinned anywhere, so this is the only record
|
||||||
|
# of which Xcode built a given run.
|
||||||
|
xcodebuild -version
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: swift run -q tools ci unit-tests
|
# HealthChat: previews are skipped. PreviewTests renders a snapshot for every preview in the
|
||||||
|
# app and was the entire reason this job could not fit in an hour — it never once finished,
|
||||||
|
# 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
|
- name: Archive artifacts
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
@@ -44,7 +98,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
# Skip if not successful and in forks
|
# Skip if not successful and in forks
|
||||||
if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
# HealthChat: the existing guard only asks whether the PR comes FROM a
|
||||||
|
# fork, not whether this repo IS one, so it is true here and the step
|
||||||
|
# runs without a CODECOV_TOKEN — which fails the job via
|
||||||
|
# fail_ci_if_error. Gate on the upstream repo as post-release.yml does.
|
||||||
|
if: ${{ success() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
with:
|
with:
|
||||||
report_type: coverage
|
report_type: coverage
|
||||||
@@ -56,7 +114,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
# Skip if cancelled and in forks
|
# Skip if cancelled and in forks
|
||||||
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
# HealthChat: gated on the upstream repo for the same reason as the
|
||||||
|
# coverage step above — no CODECOV_TOKEN exists in this fork.
|
||||||
|
if: ${{ !cancelled() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||||
|
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout. Upstream is public, hence permissions: {} there.
|
||||||
|
actions: read # HealthChat: upload-sarif additionally needs this on private/internal repos; public upstream does not.
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|||||||
@@ -49,3 +49,10 @@ test_output
|
|||||||
# Only commit Pkl packages (there may be other caches in here).
|
# Only commit Pkl packages (there may be other caches in here).
|
||||||
Secrets/vendor/*
|
Secrets/vendor/*
|
||||||
!Secrets/vendor/package-2
|
!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
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ 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
|
## Project Structure
|
||||||
|
|
||||||
### Build System
|
### Build System
|
||||||
|
|||||||
@@ -9893,9 +9893,9 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CLASSIC_APP_DEEP_LINK_URL = "element://open";
|
CLASSIC_APP_DEEP_LINK_URL = "element://open";
|
||||||
CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector;
|
CLASSIC_APP_GROUP_IDENTIFIER = group.ch.healthchat;
|
||||||
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared";
|
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared";
|
||||||
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service";
|
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "ch.healthchat.element.encryption-manager-service";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
@@ -9968,9 +9968,9 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CLASSIC_APP_DEEP_LINK_URL = "element://open";
|
CLASSIC_APP_DEEP_LINK_URL = "element://open";
|
||||||
CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector;
|
CLASSIC_APP_GROUP_IDENTIFIER = group.ch.healthchat;
|
||||||
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared";
|
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).ch.healthchat.element.keychain.shared";
|
||||||
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service";
|
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "ch.healthchat.element.encryption-manager-service";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
|||||||
+8
-7
@@ -113,13 +113,14 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType
|
|||||||
|
|
||||||
private func login(classicAppAccount: ClassicAppAccount? = nil) async {
|
private func login(classicAppAccount: ClassicAppAccount? = nil) async {
|
||||||
if let classicAppAccount {
|
if let classicAppAccount {
|
||||||
if classicAppAccount.state.availableSecrets == .requiresBackup {
|
// HealthChat diverges from upstream here. Upstream sends `.requiresBackup` accounts to a sheet
|
||||||
state.bindings.showClassicAppBackupInstructions = true
|
// asking the user to set up key backup in Element Classic first. HealthChat ships with the same
|
||||||
} else {
|
// bundle id as the legacy app, so this update *replaces* it — there is no Classic app left to
|
||||||
await configureAccountProvider(classicAppAccount.serverName,
|
// open, and the sheet's only button resolves to nothing. Fall through to a normal sign-in
|
||||||
loginHint: "mxid:\(classicAppAccount.userID)",
|
// instead: the user re-verifies and loses only history that by definition has no backup.
|
||||||
fallbackHomeserverURL: classicAppAccount.homeserverURL)
|
await configureAccountProvider(classicAppAccount.serverName,
|
||||||
}
|
loginHint: "mxid:\(classicAppAccount.userID)",
|
||||||
|
fallbackHomeserverURL: classicAppAccount.homeserverURL)
|
||||||
} else if let serverName = state.serverName {
|
} else if let serverName = state.serverName {
|
||||||
await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint)
|
await configureAccountProvider(serverName, loginHint: provisioningParameters?.loginHint)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
||||||
|
<string>$(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -125,12 +125,17 @@ targets:
|
|||||||
com.apple.developer.usernotifications.communication: true
|
com.apple.developer.usernotifications.communication: true
|
||||||
com.apple.security.app-sandbox: true
|
com.apple.security.app-sandbox: true
|
||||||
com.apple.security.application-groups:
|
com.apple.security.application-groups:
|
||||||
# HealthChat: dropped $(CLASSIC_APP_GROUP_IDENTIFIER) (group.im.vector) — owned by Element's team, not provisionable here.
|
# 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.
|
||||||
- $(APP_GROUP_IDENTIFIER)
|
- $(APP_GROUP_IDENTIFIER)
|
||||||
com.apple.security.network.client: true
|
com.apple.security.network.client: true
|
||||||
keychain-access-groups:
|
keychain-access-groups:
|
||||||
# HealthChat: dropped $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER) (im.vector.app.*) — not provisionable here.
|
|
||||||
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
- $(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:
|
settings:
|
||||||
base:
|
base:
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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
@@ -0,0 +1,350 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# 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.
|
||||||
@@ -55,6 +55,16 @@ struct RunTests: AsyncParsableCommand {
|
|||||||
"com.apple.CoreSimulator.SimRuntime.iOS-\(runtime.replacingOccurrences(of: ".", with: "-"))"
|
"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 {
|
func run() async throws {
|
||||||
if let createName = createSimulatorName {
|
if let createName = createSimulatorName {
|
||||||
guard let createType = createSimulatorType else {
|
guard let createType = createSimulatorType else {
|
||||||
@@ -142,6 +152,10 @@ struct RunTests: AsyncParsableCommand {
|
|||||||
command += " -only-testing:\(scheme)/\(testName)"
|
command += " -only-testing:\(scheme)/\(testName)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for skippedTest in skippedTests {
|
||||||
|
command += " -skip-testing:\(scheme)/\(skippedTest)"
|
||||||
|
}
|
||||||
|
|
||||||
command += " | \(formatter)"
|
command += " | \(formatter)"
|
||||||
|
|
||||||
try await CI.run(.path("/bin/zsh"), ["-cu", command])
|
try await CI.run(.path("/bin/zsh"), ["-cu", command])
|
||||||
|
|||||||
@@ -238,8 +238,8 @@ final class AuthenticationStartScreenViewModelTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func classicAppAccountRequiresBackup() async throws {
|
func classicAppAccountRequiresBackupFallsThroughToLogin() async throws {
|
||||||
// Given a view model with a Classic app account that requires backup before signing in.
|
// Given a view model with a Classic app account that has no usable key backup.
|
||||||
let classicAppAccount = makeClassicAppAccount()
|
let classicAppAccount = makeClassicAppAccount()
|
||||||
await setupViewModel(classicAppAccount: classicAppAccount, availableSecrets: .requiresBackup)
|
await setupViewModel(classicAppAccount: classicAppAccount, availableSecrets: .requiresBackup)
|
||||||
guard case .welcomeBack(let account) = context.viewState.classicAppMode else {
|
guard case .welcomeBack(let account) = context.viewState.classicAppMode else {
|
||||||
@@ -248,27 +248,15 @@ final class AuthenticationStartScreenViewModelTests {
|
|||||||
}
|
}
|
||||||
#expect(account.state.availableSecrets == .requiresBackup)
|
#expect(account.state.availableSecrets == .requiresBackup)
|
||||||
|
|
||||||
// When continuing with the Classic account while backup is required.
|
// When continuing with the Classic account.
|
||||||
var deferred = deferFulfillment(context.observe(\.viewState.bindings.showClassicAppBackupInstructions)) { $0 }
|
let deferred = deferFulfillment(viewModel.actions) { $0.isLoginDirectlyWithOAuth }
|
||||||
context.send(viewAction: .continueWithClassic(classicAppAccount))
|
context.send(viewAction: .continueWithClassic(classicAppAccount))
|
||||||
|
|
||||||
// Then the backup instructions should be shown.
|
// 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.
|
||||||
try await deferred.fulfill()
|
try await deferred.fulfill()
|
||||||
|
#expect(context.viewState.bindings.showClassicAppBackupInstructions == false)
|
||||||
// When the user completes the backup in the Classic app and the app returns to the foreground.
|
#expect(client.urlForOauthOauthConfigurationPromptLoginHintDeviceIdAdditionalScopesReceivedArguments?.loginHint == "mxid:\(classicAppAccount.userID)")
|
||||||
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
|
// MARK: - Helpers
|
||||||
|
|||||||
@@ -11,3 +11,11 @@ settings:
|
|||||||
APP_GROUP_IDENTIFIER: group.ch.healthchat
|
APP_GROUP_IDENTIFIER: group.ch.healthchat
|
||||||
BASE_BUNDLE_IDENTIFIER: ch.healthchat.element
|
BASE_BUNDLE_IDENTIFIER: ch.healthchat.element
|
||||||
DEVELOPMENT_TEAM: 6G37K72W82
|
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
@@ -1,445 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
## 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.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# For more information about the Appfile, see:
|
||||||
|
# https://docs.fastlane.tools/advanced/#appfile
|
||||||
|
|
||||||
|
apple_id ENV["APPLE_ID"]
|
||||||
|
team_id ENV["TEAM_ID"]
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Autogenerated by fastlane
|
||||||
|
#
|
||||||
|
# Ensure this file is checked in to source control!
|
||||||
|
|
||||||
|
gem 'fastlane-plugin-xcodegen'
|
||||||
Reference in New Issue
Block a user