mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-09 16:22:55 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1779cdd6a5 | |||
| e01f44cd87 | |||
| 4b3cd2c1d0 | |||
| f741adfbf4 | |||
| 6b81aeef3f | |||
| 2bd8865d4e |
@@ -77,6 +77,13 @@ jobs:
|
|||||||
# on either runner. Unit tests alone take ~16 min here. Upstream skips them the same way in
|
# 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
|
# unit-tests-enterprise.yml. Cost: preview snapshot regressions are not caught in CI, so
|
||||||
# re-record and check them locally when changing UI.
|
# 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
|
run: swift run -q tools ci unit-tests --skip-previews
|
||||||
|
|
||||||
- name: Archive artifacts
|
- name: Archive artifacts
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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 |
|
||||||
@@ -253,7 +253,7 @@ Build the branch and install it **without deleting the legacy app** — same bun
|
|||||||
- [ ] **Step 5: Observe**
|
- [ ] **Step 5: Observe**
|
||||||
|
|
||||||
Expected: the app finds the existing account and signs in without a manual login.
|
Expected: the app finds the existing account and signs in without a manual login.
|
||||||
Watch the log for `ClassicAppManagerError` and for `"skipping loadAccounts"` — the latter means Step 1's trap caught you and the run is **void, not failed**.
|
Watch the log for `ClassicAppManagerError`. Two *different* lines end in `"skipping loadAccounts"` (`AuthenticationService.swift:52` and `:57`) and they mean opposite things — never match the bare substring. `"Already signed in to Element X before, skipping loadAccounts"` means Step 1's trap caught you and the run is **void, not failed**. `"Classic App not configured, skipping loadAccounts"` 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.
|
||||||
|
|
||||||
- [ ] **Step 6: Exercise the `.requiresBackup` path if you can reach it**
|
- [ ] **Step 6: Exercise the `.requiresBackup` path if you can reach it**
|
||||||
|
|
||||||
@@ -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**
|
||||||
|
|
||||||
@@ -718,12 +705,21 @@ jobs:
|
|||||||
submodules: false
|
submodules: false
|
||||||
persist-credentials: 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
|
- name: Resolve marketing version
|
||||||
id: 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: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||||
VERSION="${{ github.event.inputs.version }}"
|
VERSION="$INPUT_VERSION"
|
||||||
else
|
else
|
||||||
VERSION="${GITHUB_REF_NAME#healthchat-v}"
|
VERSION="${GITHUB_REF_NAME#healthchat-v}"
|
||||||
fi
|
fi
|
||||||
@@ -776,9 +772,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 +869,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 +1083,36 @@ 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's only Xcode provenance is its own log.** `release-healthchat.yml` runs
|
||||||
`Brewfile` — investigate rather than ignore.
|
`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)
|
## Renewing the Apple Distribution certificate (annual)
|
||||||
|
|
||||||
@@ -1188,13 +1203,19 @@ place, and these are what stop existing users being silently logged out.
|
|||||||
- **`hasSignedInBefore` voids migration testing.** It lives in that shared suite,
|
- **`hasSignedInBefore` voids migration testing.** It lives in that shared suite,
|
||||||
survives app deletion, and short-circuits `loadAccounts()`. Any device that has
|
survives app deletion, and short-circuits `loadAccounts()`. Any device that has
|
||||||
run an element-x HealthChat build shows a plain login screen *while migration
|
run an element-x HealthChat build shows a plain login screen *while migration
|
||||||
works perfectly*. Delete the app before testing; `"skipping loadAccounts"` in the
|
works perfectly*. Test on a device that has never run an element-x HealthChat
|
||||||
log means the run is **void, not failed**.
|
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
|
## 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 +1237,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,9 @@ 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.** 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. `release-healthchat.yml` records it via the `Record toolchain` step (`xcodebuild -version`) before every build, so each run logs the compiler that produced it — but that log line is the only record, and it detects the change after the fact rather than preventing it. Treat a runner Xcode upgrade as a release-affecting change. |
|
||||||
|
| **O9** | **A pull request can execute arbitrary code on the release machine.** `unit-tests.yml` runs on `[self-hosted, macOS]` under a `pull_request` trigger (`.github/workflows/unit-tests.yml:9,20`), so any PR's build scripts run on `novaloop-mac-studio-001` — the same persistent runner, keychain and signing identities the release pipeline uses. **Accepted because the repo is INTERNAL**, which narrows PR authors to org members. The trade-off is invisible to CI: zizmor reports it (`self-hosted-runner`, medium) **only at `--persona=auditor`**, and the default persona CI runs suppresses it — so no required check will ever warn. If org membership stops being a sufficient trust boundary: restrict the trigger to non-fork branches, or move back to GitHub-hosted `macos-26` with a raised `timeout-minutes`. |
|
||||||
|
|
||||||
**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
|
||||||
|
|||||||
Reference in New Issue
Block a user