diff --git a/docs/superpowers/plans/2026-07-15-healthchat-appstore-release.md b/docs/superpowers/plans/2026-07-15-healthchat-appstore-release.md index b2d0b1b70..ef4b3cf27 100644 --- a/docs/superpowers/plans/2026-07-15-healthchat-appstore-release.md +++ b/docs/superpowers/plans/2026-07-15-healthchat-appstore-release.md @@ -28,9 +28,9 @@ | `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 | | `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 | -| `Gemfile`, `Gemfile.lock`, `Brewfile` | Toolchain pinning | 2 | +| `Gemfile`, `Gemfile.lock` | Toolchain pinning | 2 | | `HEALTHCHAT_CHANGES.md` | Release-notes source the workflow greps | 2 | | `.github/workflows/release-healthchat.yml` | Tag-triggered build + TestFlight upload | 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 **Files:** -- Create: `Gemfile`, `Brewfile`, `fastlane/Pluginfile`, `fastlane/Appfile` +- Create: `Gemfile`, `fastlane/Pluginfile`, `fastlane/Appfile` - 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** ```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. -- [ ] **Step 4: Create `Brewfile`** - -```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`** +- [ ] **Step 4: Create `fastlane/Appfile`** ```ruby # 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. -- [ ] **Step 6: Extend `.gitignore`** +- [ ] **Step 5: Extend `.gitignore`** 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. -- [ ] **Step 7: Resolve gems and commit the lock** +- [ ] **Step 6: Resolve gems and commit the lock** Run: `bundle install` 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. -- [ ] **Step 8: Commit** +- [ ] **Step 7: Commit** ```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" ``` @@ -437,16 +431,11 @@ git commit -m "Add fastlane configuration for the HealthChat App Store build" ```ruby 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 - #### 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 #### 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** -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` 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** @@ -776,9 +763,6 @@ jobs: restore-keys: | ${{ runner.os }}-gems- - - name: Brew bundle - run: brew bundle - - name: Bundle install run: | 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`. -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, macOS, ARM64]`) shared with the legacy fork, not registered per-repo. -- **Xcode 26.4**, selectable via `xcodes` (pinned in `fastlane/Fastfile:before_all`). - **Bumping the pin requires installing the new Xcode on the runner first.** - Otherwise `xcodes` starts a multi-GB authenticated download that no CI secret can - satisfy, and the run hangs in `before_all`. -- **xcodegen** on PATH — installed manually once via `brew install xcodegen`; - deliberately **not** in `Brewfile`, so `brew bundle` will not churn it. -- **Homebrew**, **Ruby** + **bundler**, **Git** on PATH. +- **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`. -If `brew bundle` installs packages on every run, brew state has drifted from -`Brewfile` — investigate rather than ignore. +**A release records no Xcode provenance.** With the pin gone, the runner's current +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) @@ -1194,7 +1199,7 @@ place, and these are what stop existing users being silently logged out. ## Known branch divergence on rebase `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 upstream has started to overlap; keep `--ours` and investigate. @@ -1216,9 +1221,9 @@ Run: ```bash 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 -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** diff --git a/docs/superpowers/specs/2026-07-15-healthchat-appstore-release-design.md b/docs/superpowers/specs/2026-07-15-healthchat-appstore-release-design.md index 28d067aed..85f30d25a 100644 --- a/docs/superpowers/specs/2026-07-15-healthchat-appstore-release-design.md +++ b/docs/superpowers/specs/2026-07-15-healthchat-appstore-release-design.md @@ -168,12 +168,11 @@ element-ios. Here that class of conflict does not exist — which is why this is | 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/.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 ()` 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. | @@ -262,7 +261,8 @@ Changes from the legacy: - 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`). +- **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`. - 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. @@ -278,16 +278,38 @@ the real requirement.** `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`. +**No Xcode pin.** Measured on the runner today (user `novaloop`, arm64, macOS 26.5.2): exactly **one** Xcode, +`/Applications/Xcode.app` — **26.6, build `17F113`**, `xcode-select`ed and the only version `xcodes` lists. No +versioned `Xcode_*.app` siblings exist. -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). +An earlier draft said to pin `"26.4"` because "the runner has it and the legacy pipeline proves it works". +**Both halves were wrong.** The runner does not have 26.4 and never will; a pin naming an absent version does +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 @@ -343,7 +365,7 @@ per its own "keep current" rule this is a new fact about the repo, so it belongs | 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` | +| PR2 | Part 2 — fastlane, workflow, Gemfile(.lock), .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. @@ -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. | | **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. | +| **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 `group.ch.healthchat` UserDefaults suite has a verified-empty key intersection. Captured as a standing hazard