Correct the release plan and design for the runner's measured state

Both documents told the reader to pin Xcode 26.4 and to install it via a
Brewfile. A probe of novaloop-mac-studio-001 refutes the premise: it carries
exactly one Xcode, 26.6, globally selected, with no versioned siblings and no
26.4 anywhere. The design justified the pin with "the runner has it and the
legacy pipeline proves it works" — both halves are false, and a pin naming an
absent version does not fail fast: xcodes starts a multi-GB authenticated
download that no CI secret can satisfy, so before_all hangs.

Record the no-pin decision and its cost — whatever Xcode the runner has is what
ships, unrecorded — rather than only deleting the wrong instruction, so nobody
reintroduces the pin from the same reasoning. Drop the Brewfile with it: its only
purpose was installing xcodes for that pin, and brew has no business running on a
shared release machine where it could upgrade xcodegen mid-release.

Add the runner's real constraints (no passwordless sudo, single Xcode, five
missing tools installed by hand) to the design, since they are why upstream's
setup_github_actions_environment cannot run here.

Two new risks. The legacy element-ios pipeline pins the same absent 26.4, so it
is very likely broken right now — not this fork's problem to fix, but the human
needs to know before the next legacy release. And with no pin, an Xcode upgrade
on the runner silently changes the compiler for every subsequent release.

The plan's embedded Fastfile and skill now match the branches that implement
them byte-for-byte, so the plan cannot drift from the artefacts it describes.
This commit is contained in:
2026-07-16 00:00:26 +02:00
committed by Sedat Qaja
parent ae2b32217b
commit d7a93dddea
2 changed files with 86 additions and 57 deletions
@@ -28,9 +28,9 @@
| `ElementX/SupportingFiles/target.yml` | Entitlement arrays — regains the classic keychain group | 1 | | `ElementX/SupportingFiles/target.yml` | Entitlement arrays — regains the classic keychain group | 1 |
| `ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift` | Start screen logic — `.requiresBackup` redirect | 1 | | `ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift` | Start screen logic — `.requiresBackup` redirect | 1 |
| `UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift` | Encodes the redirect behaviour | 1 | | `UnitTests/Sources/AuthenticationStartScreenViewModelTests.swift` | Encodes the redirect behaviour | 1 |
| `fastlane/Fastfile` | `before_all` + `release_testflight` + private build/signing lanes | 2 | | `fastlane/Fastfile` | `release_testflight` + private build/signing lanes | 2 |
| `fastlane/Appfile`, `fastlane/.env.default`, `fastlane/Pluginfile` | fastlane configuration | 2 | | `fastlane/Appfile`, `fastlane/.env.default`, `fastlane/Pluginfile` | fastlane configuration | 2 |
| `Gemfile`, `Gemfile.lock`, `Brewfile` | Toolchain pinning | 2 | | `Gemfile`, `Gemfile.lock` | Toolchain pinning | 2 |
| `HEALTHCHAT_CHANGES.md` | Release-notes source the workflow greps | 2 | | `HEALTHCHAT_CHANGES.md` | Release-notes source the workflow greps | 2 |
| `.github/workflows/release-healthchat.yml` | Tag-triggered build + TestFlight upload | 2 | | `.github/workflows/release-healthchat.yml` | Tag-triggered build + TestFlight upload | 2 |
| `.gitignore` | Excludes `out/` and `provisioning_profiles/` | 2 | | `.gitignore` | Excludes `out/` and `provisioning_profiles/` | 2 |
@@ -279,9 +279,11 @@ Leave the description template for the developer. Exactly one `pr-` label or `Da
### Task 4: Add the toolchain files ### Task 4: Add the toolchain files
**Files:** **Files:**
- Create: `Gemfile`, `Brewfile`, `fastlane/Pluginfile`, `fastlane/Appfile` - Create: `Gemfile`, `fastlane/Pluginfile`, `fastlane/Appfile`
- Modify: `.gitignore` - Modify: `.gitignore`
**No `Brewfile`.** Nothing in this pipeline installs tools. The runner's toolchain is provisioned once by hand (Task 9 Step 5): `novaloop-mac-studio-001` is shared with the legacy release pipeline, so a per-run `brew` could upgrade `xcodegen` underneath a running release. Its only other purpose was installing `xcodes` for a version pin that no longer exists (Task 6).
- [ ] **Step 1: Branch** - [ ] **Step 1: Branch**
```bash ```bash
@@ -314,15 +316,7 @@ gem 'fastlane-plugin-xcodegen'
Only one plugin. The legacy also carries `fastlane-plugin-versioning`, but nothing in its Fastfile calls it — its version lanes are raw `File.read`/`gsub!`. Do not copy dead weight forward. Only one plugin. The legacy also carries `fastlane-plugin-versioning`, but nothing in its Fastfile calls it — its version lanes are raw `File.read`/`gsub!`. Do not copy dead weight forward.
- [ ] **Step 4: Create `Brewfile`** - [ ] **Step 4: Create `fastlane/Appfile`**
```ruby
brew "xcodesorg/made/xcodes"
```
`xcodegen` is deliberately **absent**: it is installed on the runner once by hand, so `brew bundle` cannot churn it on every run. The legacy documents the same convention.
- [ ] **Step 5: Create `fastlane/Appfile`**
```ruby ```ruby
# For more information about the Appfile, see: # For more information about the Appfile, see:
@@ -334,7 +328,7 @@ team_id ENV["TEAM_ID"]
`APPLE_ID` is a machine-level shell variable on the runner, not a repo secret. `APPLE_ID` is a machine-level shell variable on the runner, not a repo secret.
- [ ] **Step 6: Extend `.gitignore`** - [ ] **Step 5: Extend `.gitignore`**
Append: Append:
@@ -346,17 +340,17 @@ provisioning_profiles/
`build` (`.gitignore:35`) and `fastlane/report.xml` (`:28`) are already covered upstream. `provisioning_profiles/` matters most: it holds downloaded signing material and must never be committed. `build` (`.gitignore:35`) and `fastlane/report.xml` (`:28`) are already covered upstream. `provisioning_profiles/` matters most: it holds downloaded signing material and must never be committed.
- [ ] **Step 7: Resolve gems and commit the lock** - [ ] **Step 6: Resolve gems and commit the lock**
Run: `bundle install` Run: `bundle install`
Expected: creates `Gemfile.lock`. Expected: creates `Gemfile.lock`.
Commit the lock — the workflow's gem cache keys on `hashFiles('**/Gemfile.lock')`. Without it the key never changes, fastlane silently re-resolves, and a breaking fastlane release can change the pipeline between two runs of the same tag. Commit the lock — the workflow's gem cache keys on `hashFiles('**/Gemfile.lock')`. Without it the key never changes, fastlane silently re-resolves, and a breaking fastlane release can change the pipeline between two runs of the same tag.
- [ ] **Step 8: Commit** - [ ] **Step 7: Commit**
```bash ```bash
git add Gemfile Gemfile.lock Brewfile fastlane/Pluginfile fastlane/Appfile .gitignore git add Gemfile Gemfile.lock fastlane/Pluginfile fastlane/Appfile .gitignore
git commit -m "Add the fastlane toolchain for HealthChat releases" git commit -m "Add the fastlane toolchain for HealthChat releases"
``` ```
@@ -437,16 +431,11 @@ git commit -m "Add fastlane configuration for the HealthChat App Store build"
```ruby ```ruby
default_platform :ios 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 platform :ios do
#### Pre ####
before_all do
# 26.4 is what novaloop-mac-studio-001 carries and what the legacy pipeline builds with.
# Bumping this requires installing the new version on the runner FIRST — otherwise xcodes
# attempts a multi-GB authenticated download, which no CI secret can satisfy, and hangs here.
xcodes(version: "26.4", select_for_current_build_only: true)
end
#### Public #### #### Public ####
desc "Builds the App Store ipa and uploads it to TestFlight (internal testers only)" desc "Builds the App Store ipa and uploads it to TestFlight (internal testers only)"
@@ -603,12 +592,10 @@ Expected: lists `ios release_testflight` with its description. A Ruby syntax err
- [ ] **Step 3: Verify the version guard rejects a marker version** - [ ] **Step 3: Verify the version guard rejects a marker version**
Run: `brew bundle` first if you have never run it here — this step needs the `xcodes` CLI, for the reason below.
Run: `bundle exec fastlane release_testflight marketing_version:26.07.3-rc1` Run: `bundle exec fastlane release_testflight marketing_version:26.07.3-rc1`
Expected: fails with `'marketing_version' must be 1-3 dot-separated integers`, before any build or signing work. Expected: fails with `'marketing_version' must be 1-3 dot-separated integers`, before any build or signing work.
**On ordering.** fastlane's `before_all` always runs before the lane body, so `xcodes` selects the Xcode version *first* and the guard fires immediately after — not before. That still saves the ~30 minutes of signing the guard exists to prevent, which is the point, but it means this step needs the `xcodes` CLI present (it is in `Brewfile`). Without it the lane dies at line 10 with `Invalid default value for binary_path` — a missing-tool error, not a guard failure. Do not restructure the Fastfile to make the guard literally run first; `before_all` pinning the toolchain is the convention the legacy pipeline uses too. **On ordering.** With no `before_all`, the guard is the first thing the lane does — nothing runs ahead of it, so this step needs no Xcode selection and no tool beyond fastlane itself. That is the whole point: it costs seconds instead of the ~30 minutes of signing it exists to prevent. Do not reintroduce a `before_all` to pin the toolchain; see Task 9 Step 5 for why the runner's Xcode is a property of the machine.
- [ ] **Step 4: Commit** - [ ] **Step 4: Commit**
@@ -776,9 +763,6 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-gems- ${{ runner.os }}-gems-
- name: Brew bundle
run: brew bundle
- name: Bundle install - name: Bundle install
run: | run: |
bundle config path vendor/bundle bundle config path vendor/bundle
@@ -876,7 +860,13 @@ Then restore the previous account: `gh auth switch --user joel-poyet_GALGH`.
`novaloop-mac-studio-001` is an **org-level** runner (`[self-hosted, macOS, ARM64]`, online). Confirm the org runner group's visibility includes element-x-ios — two `Default` groups exist, one `all` and one `selected`. `novaloop-mac-studio-001` is an **org-level** runner (`[self-hosted, macOS, ARM64]`, online). Confirm the org runner group's visibility includes element-x-ios — two `Default` groups exist, one `all` and one `selected`.
On the runner itself, confirm: `xcodegen` on PATH (`brew install xcodegen`, deliberately not in `Brewfile`), Xcode **26.4** present and selectable via `xcodes`, Homebrew, Ruby + bundler, and `APPLE_ID` exported as a machine-level shell variable. On the runner itself, confirm the toolchain is installed **by hand** — CI never provisions it. Measured today on `novaloop-mac-studio-001` (user `novaloop`, arm64, macOS 26.5.2):
- **Xcode:** one install, `/Applications/Xcode.app` (**26.6**, build `17F113`), globally selected via `xcode-select`; no versioned `Xcode_*.app` siblings. Nothing pins it — the build uses whatever `xcode-select` points at, so upgrading it silently changes the compiler every subsequent release is built with. Treat that upgrade as a release-affecting change.
- **Present:** `xcodes`, `xcodegen`, `git-lfs`, `brew`, `swift`, rbenv `ruby`/`bundle`/`gem`.
- **Missing:** `swiftlint`, `swiftformat`, `pkl`, `xcresultparser`, `xcbeautify`. The unit-test workflow asserts all five; this release lane requires none (its SwiftLint/SwiftFormat build phases are `if which …` guarded and degrade to a warning). The human installs them once: `brew install swiftlint swiftformat pkl xcbeautify a7ex/homebrew-formulae/xcresultparser` (`xcresultparser` is not in homebrew-core).
- **No passwordless sudo.** Nothing in CI may call `sudo`.
- Also confirm `APPLE_ID` is exported as a machine-level shell variable — the `Appfile` reads it and no secret carries it.
--- ---
@@ -1084,20 +1074,35 @@ invisible to the other account.
Runs on `novaloop-mac-studio-001` — an **org-level** runner (`[self-hosted, Runs on `novaloop-mac-studio-001` — an **org-level** runner (`[self-hosted,
macOS, ARM64]`) shared with the legacy fork, not registered per-repo. macOS, ARM64]`) shared with the legacy fork, not registered per-repo.
- **Xcode 26.4**, selectable via `xcodes` (pinned in `fastlane/Fastfile:before_all`). - **One Xcode, globally selected.** A single install at `/Applications/Xcode.app`
**Bumping the pin requires installing the new Xcode on the runner first.** (**26.6**, build `17F113`, today) — no versioned `Xcode_*.app` siblings.
Otherwise `xcodes` starts a multi-GB authenticated download that no CI secret can **Nothing pins it.** The Fastfile builds with whatever `xcode-select` points at, so
satisfy, and the run hangs in `before_all`. **whatever the runner has is what ships**. Upgrading the runner's Xcode silently
- **xcodegen** on PATH — installed manually once via `brew install xcodegen`; changes the compiler every subsequent release is built with — no tag, no diff, no
deliberately **not** in `Brewfile`, so `brew bundle` will not churn it. review. Treat that upgrade as a release-affecting change.
- **Homebrew**, **Ruby** + **bundler**, **Git** on PATH. - **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 - **`APPLE_ID`** exported as a machine-level shell variable — the `Appfile` reads
it and it is not carried in any secret. it and it is not carried in any secret.
- Network access to `developer.apple.com`, `api.appstoreconnect.apple.com`, - Network access to `developer.apple.com`, `api.appstoreconnect.apple.com`,
`github.com`, `rubygems.org`. `github.com`, `rubygems.org`.
If `brew bundle` installs packages on every run, brew state has drifted from **A release records no Xcode provenance.** With the pin gone, the runner's current
`Brewfile` — investigate rather than ignore. state is the only fact about which compiler built a shipped build, and
`release-healthchat.yml` does not log it — `unit-tests.yml` runs `xcodebuild
-version`, the release path does not. Nothing in a release run's logs or artifacts
will tell you afterwards; read `xcodebuild -version` on the runner while it still
matches.
## Renewing the Apple Distribution certificate (annual) ## Renewing the Apple Distribution certificate (annual)
@@ -1194,7 +1199,7 @@ place, and these are what stop existing users being silently logged out.
## Known branch divergence on rebase ## Known branch divergence on rebase
`fastlane/`, `.github/workflows/release-healthchat.yml`, `HEALTHCHAT_CHANGES.md`, `fastlane/`, `.github/workflows/release-healthchat.yml`, `HEALTHCHAT_CHANGES.md`,
`Brewfile`, `Gemfile` and this skill are **fork-only** — upstream element-x-ios has `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 no `fastlane/` directory at all. A rebase conflict in any of them means something
upstream has started to overlap; keep `--ours` and investigate. upstream has started to overlap; keep `--ours` and investigate.
@@ -1216,9 +1221,9 @@ Run:
```bash ```bash
grep -n "MARKETING_VERSION" project.yml | head -1 # section "Version rules" cites :56 grep -n "MARKETING_VERSION" project.yml | head -1 # section "Version rules" cites :56
grep -c "CLASSIC_APP" Variants/HealthChat/healthchat.yml # migration contract: must be 3 grep -c "CLASSIC_APP" Variants/HealthChat/healthchat.yml # migration contract: must be 3
grep -n "xcodes(version:" fastlane/Fastfile # runner section cites the pin grep -rn "xcodes(version:\|before_all" fastlane/Fastfile # runner section says nothing pins Xcode
``` ```
Expected: `MARKETING_VERSION` at `project.yml:56`; three `CLASSIC_APP_*` entries in the variant; the Fastfile pin matches the `26.4` the skill documents. A skill that misquotes the repo is worse than no skill. Expected: `MARKETING_VERSION` at `project.yml:56`; three `CLASSIC_APP_*` entries in the variant; **no match** for the pin — a pin's return would contradict the runner section. A skill that misquotes the repo is worse than no skill.
- [ ] **Step 4: Verify the skill is discoverable** - [ ] **Step 4: Verify the skill is discoverable**
@@ -168,12 +168,11 @@ element-ios. Here that class of conflict does not exist — which is why this is
| File | Purpose | | File | Purpose |
|---|---| |---|---|
| `fastlane/Fastfile` | `before_all` (Xcode pin) + public `release_testflight` + private helpers | | `fastlane/Fastfile` | Public `release_testflight` + private helpers. **No `before_all`, no Xcode pin** — see runner notes. |
| `fastlane/Appfile` | `apple_id ENV["APPLE_ID"]`, `team_id ENV["TEAM_ID"]` | | `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/.env.default` | Project/target/bundle-id/profile configuration (full key list below) |
| `fastlane/Pluginfile` | **`fastlane-plugin-xcodegen` only** | | `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. | | `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. | | `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. | | `.gitignore` | Add `out/` and **`provisioning_profiles/`** (signing material — security-relevant). `build` (`:35`) and `fastlane/report.xml` (`:28`) are already covered upstream. |
@@ -262,7 +261,8 @@ Changes from the legacy:
- Drop the CocoaPods cache and `rm -rf Pods/MatrixSDK`. - 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. - 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. - Re-evaluate `fetch-depth: 0` (legacy `:48`); keep only if something needs history.
- Keep the explicit `brew bundle` step (legacy `:115`). - **Drop the `brew bundle` step** (legacy `:115`) — there is no `Brewfile`, and the runner is provisioned by
hand. See runner notes.
- `bundle exec fastlane release_testflight`; artifacts `out/ElementX.ipa`, `out/ElementX.app.dSYM.zip`. - `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 - 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. an *empty match*, not an absent file — under `set -euo pipefail` a missing file aborts the job.
@@ -278,16 +278,38 @@ the real requirement.**
`runs-on: [self-hosted, macOS]`. Confirm the org runner group's visibility includes element-x-ios (two `runs-on: [self-hosted, macOS]`. Confirm the org runner group's visibility includes element-x-ios (two
`Default` groups exist, one `all`, one `selected`). `Default` groups exist, one `all`, one `selected`).
**Xcode pin: use `"26.4"`.** The runner has it and the legacy pipeline proves it works. **No Xcode pin.** Measured on the runner today (user `novaloop`, arm64, macOS 26.5.2): exactly **one** Xcode,
`ci_scripts/ci_common.sh:34` (`sudo xcode-select -s /Applications/Xcode_26.5.0.app`) is an **Xcode Cloud image `/Applications/Xcode.app` — **26.6, build `17F113`**, `xcode-select`ed and the only version `xcodes` lists. No
path, not an `xcodes` version identifier**, and is not evidence the runner has 26.5. If element-x turns out to versioned `Xcode_*.app` siblings exist.
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` An earlier draft said to pin `"26.4"` because "the runner has it and the legacy pipeline proves it works".
so `brew bundle` cannot churn it), Homebrew, Ruby + bundler, and `APPLE_ID` exported as a machine-level shell **Both halves were wrong.** The runner does not have 26.4 and never will; a pin naming an absent version does
variable (the `Appfile` reads it; it is not repo-carried). not fail fast — `xcodes` starts a multi-GB authenticated download needing `XCODES_USERNAME`/`XCODES_PASSWORD`,
which none of the five secrets supply, so `before_all` hangs and takes the release with it. Since the runner
carries a single globally-selected Xcode, whatever `xcode-select` points at *is* the build toolchain, and a pin
could only ever disagree with reality. **Drop it entirely**; the Xcode version becomes a property of the
machine, changed by provisioning it rather than by editing a tracked file. The reasoning about
`ci_scripts/ci_common.sh:34` (`sudo xcode-select -s /Applications/Xcode_26.5.0.app`) still stands — it is an
Xcode Cloud image path, not an `xcodes` version identifier, and says nothing about this runner.
The cost of dropping it: **whatever the runner has is what ships**, and no release run records which Xcode
built it (`release-healthchat.yml` does not log `xcodebuild -version`; `unit-tests.yml` does). Upgrading the
runner's Xcode is therefore a release-affecting change with no tag, diff or review. Accepted — the alternative
pins a version that does not exist. Captured in the skill.
**The runner is provisioned by hand, never by CI.** Upstream's `setup_github_actions_environment`
(`ci_scripts/ci_common.sh`) cannot run here: it calls `sudo xcode-select -s /Applications/Xcode_26.5.0.app` —
there is **no passwordless sudo** and no such path — and `brew update && brew install …`, which on this
*shared release machine* could upgrade `xcodegen` underneath a running release. CI must **verify and fail
loudly**, naming the fix, instead.
Present today: `xcodes`, `xcodegen`, `git-lfs`, `brew`, `swift`, rbenv `ruby`/`bundle`/`gem`, and `APPLE_ID`
exported as a machine-level shell variable (the `Appfile` reads it; it is not repo-carried).
**Missing today** — the human installs these once, before the test workflow can pass: `swiftlint`,
`swiftformat`, `pkl`, `xcresultparser`, `xcbeautify`. Only `xcresultparser` is outside homebrew-core
(`a7ex/homebrew-formulae/xcresultparser`). The unit-test job asserts all of them. The release lane requires
none: its SwiftLint/SwiftFormat build phases are `if which …` guarded and degrade to a build warning when the
tool is absent (`ElementX/SupportingFiles/target.yml:202-214`), and nothing in the build reads `pkl`.
### Secrets to copy ### Secrets to copy
@@ -343,7 +365,7 @@ per its own "keep current" rule this is a new fact about the repo, so it belongs
| PR | Contents | Label | | PR | Contents | Label |
|---|---|---| |---|---|---|
| PR1 | Part 1 — `CLASSIC_APP_*`, entitlement, `.requiresBackup` edit, regenerated pbxproj | `pr-bugfix` | | 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` | | PR2 | Part 2 — fastlane, workflow, Gemfile(.lock), .gitignore, HEALTHCHAT_CHANGES.md | `pr-build` |
| PR3 | Part 3 — skill + AGENTS.md delegation | `pr-doc` | | 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. AGENTS.md's 500-addition guidance argues for keeping these separate rather than combining PR2 and PR3.
@@ -424,6 +446,8 @@ upload; review proves nothing extra about the pipeline.
| **O4** | **Runner group visibility** — confirm `novaloop-mac-studio-001` is reachable from element-x-ios. | | **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. | | **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. | | **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. |
| **O7** | **The legacy element-ios pipeline is probably broken right now — unverified, and worth verifying.** It pins `xcodes(version: "26.4")` and runs on this same runner, which carries only **26.6**. Its last green run was 2026-04-15, so the machine plausibly had 26.4 then and lost it to an upgrade since. If so, the next legacy release does not fail — it **hangs** in `before_all` on an authenticated multi-GB download, and only a timeout ends it. **This fork no longer depends on that pipeline** (D3's "the legacy pipeline is proven" means *was* proven, on a toolchain the machine no longer has), so this is not a blocker here. Confirm the pin against `Novaloop-AG/element-ios` and de-pin it there before its next release. Out of scope for these three PRs — raise it. |
| **O8** | **Whatever Xcode the runner has is what ships**, and no release run records it. The pin is gone (see Runner), so upgrading `/Applications/Xcode.app` silently changes the compiler for every subsequent release, with no tag, diff or review. Mitigated only by documentation in the skill. Adding `xcodebuild -version` to `release-healthchat.yml` would give provenance cheaply — worth doing. |
**Closed:** the app-group collision question — container paths are disjoint, and the shared **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 `group.ch.healthchat` UserDefaults suite has a verified-empty key intersection. Captured as a standing hazard