From 20bf36986338bd1cb212d33a7949c07c9f32f528 Mon Sep 17 00:00:00 2001 From: AidenJohnston Date: Sat, 26 Sep 2026 18:48:30 -0500 Subject: [PATCH] ci(ios): cut pull request runs from ~25 min to ~12 min A run spent 2:40 booting the Simulator with nothing else running, and 10:00 in one test, the every-state accessibility audit (65 app launches). With 5 macOS jobs allowed at once, those long runs also queued other pull requests for up to 22 minutes. - Boot the Simulator in the background right after checkout, alongside swift test and the device build. - Skip the audit test on pull requests unless labelled full-ui; pushes to t3/ios-mvf and main, and manual runs, still run it. - Skip drafts (run on ready_for_review) and Markdown-only changes. - Turn off the index store in CI builds. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/labels.json | 5 +++ .github/workflows/ios.yml | 86 ++++++++++++++++++++++++++++++--------- CONTRIBUTING.md | 4 +- 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/.github/labels.json b/.github/labels.json index d972796e3..bf7807164 100644 --- a/.github/labels.json +++ b/.github/labels.json @@ -24,6 +24,11 @@ "color": "b60205", "description": "Not ready to merge. State what must change before it is ready." }, + { + "name": "full-ui", + "color": "5319e7", + "description": "iOS CI also runs the every-state accessibility audit (about 10 more minutes)." + }, { "name": "size:XS", "color": "0e8a16", diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 81d4edefd..c48739365 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -2,40 +2,85 @@ name: iOS on: push: - branches: [main] + branches: [main, t3/ios-mvf] paths: - "ios/**" + - "!ios/**/*.md" - ".github/workflows/ios.yml" pull_request: + # ready_for_review: drafts are skipped below, so run once a draft is marked ready. + # labeled: adding full-ui reruns the pull request with the accessibility audit. + types: [opened, synchronize, reopened, ready_for_review, labeled] paths: - "ios/**" + - "!ios/**/*.md" - ".github/workflows/ios.yml" + # Runs every test, the audit included, on any branch. + workflow_dispatch: permissions: contents: read concurrency: - group: ios-${{ github.ref }} + # Adding any label other than full-ui gets a group of its own, so it cannot cancel a real run. + group: ios-${{ github.ref }}${{ github.event.action == 'labeled' && github.event.label.name != 'full-ui' && format('-{0}', github.run_id) || '' }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: ios-build: name: iOS build + # A free account runs 5 macOS jobs at once, shared by every open pull request, so drafts and + # labels other than full-ui don't take a slot. + if: >- + github.event_name != 'pull_request' || + (!github.event.pull_request.draft && + (github.event.action != 'labeled' || github.event.label.name == 'full-ui')) # macos-26 (arm64) is the newest GA macOS image. The xcode-27 image is still a preview. runs-on: macos-26 timeout-minutes: 45 env: # Newest stable Xcode on macos-26 per images/macos/macos-26-arm64-Readme.md (image 20260907). DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer + # iPhone 17 on iOS 26.5 ships with Xcode 26.6 on macos-26 (images/macos/macos-26-arm64-Readme.md). + SIM_NAME: iPhone 17 + SIM_OS: "26.5" # Homebrew only installs the latest XcodeGen. The release zip gives an exact version, # and the drift check below needs the same version the committed project was made with. XCODEGEN_VERSION: 2.46.0 XCODEGEN_SHA256: 4d9e34b62172d645eed6457cac13fc222569974098ef4ee9c3368bedf0196806 + # The every-state accessibility audit relaunches the app 65 times (about 10 min). Pull + # requests skip it unless labelled full-ui; pushes and manual runs always include it. + FULL_UI: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-ui') }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - name: Show toolchain + run: | + if [ ! -d "$DEVELOPER_DIR" ]; then + echo "::error::$DEVELOPER_DIR is not on this runner image. Pick an installed Xcode from the image readme." + exit 1 + fi + xcodebuild -version + swift --version + + # Booting takes 2 to 3 minutes and needs nothing from the build, so it runs alongside the + # steps below; the UI test step waits for it. + - name: Boot the Simulator in the background + run: | + runtime="com.apple.CoreSimulator.SimRuntime.iOS-${SIM_OS//./-}" + udid="$(xcrun simctl list devices available -j \ + | jq -r --arg runtime "$runtime" --arg name "$SIM_NAME" \ + '(.devices[$runtime] // [])[] | select(.name == $name) | .udid' | head -n 1)" + if [ -z "$udid" ]; then + echo "::error::No $SIM_NAME on iOS $SIM_OS on this runner image. Pick one from the image readme." + xcrun simctl list devices available + exit 1 + fi + echo "SIM_UDID=$udid" >> "$GITHUB_ENV" + nohup xcrun simctl bootstatus "$udid" -b > "$RUNNER_TEMP/simulator-boot.log" 2>&1 & + # The package's drift tests compare the vendored schemas with the server's and the capture # packet's own copies. Before those branches merge they exist only as branches, which the # shallow checkout leaves out; without them the tests report as skipped, not passed. @@ -46,15 +91,6 @@ jobs: +refs/heads/t3/packet:refs/remotes/origin/t3/packet \ || echo "::warning::t3/server or t3/packet is gone; the drift tests read the merged tree or skip" - - name: Show toolchain - run: | - if [ ! -d "$DEVELOPER_DIR" ]; then - echo "::error::$DEVELOPER_DIR is not on this runner image. Pick an installed Xcode from the image readme." - exit 1 - fi - xcodebuild -version - swift --version - - name: Install XcodeGen run: | curl -fsSL -o "$RUNNER_TEMP/xcodegen.zip" \ @@ -83,20 +119,32 @@ jobs: # The capture logic is a plain Swift package; its tests run on macOS without a Simulator. run: swift test --package-path ios/HouseScanKit -Xswiftc -warnings-as-errors + # CI builds once from clean, so the index Xcode keeps for editing is wasted work. - name: Build run: >- xcodebuild -project ios/HouseScan.xcodeproj -scheme HouseScan -configuration Debug - -destination "generic/platform=iOS" CODE_SIGNING_ALLOWED=NO build + -destination "generic/platform=iOS" CODE_SIGNING_ALLOWED=NO COMPILER_INDEX_STORE_ENABLE=NO build + + - name: Wait for the Simulator + timeout-minutes: 10 + run: | + xcrun simctl bootstatus "$SIM_UDID" -b + echo "Booted $SIM_NAME on iOS $SIM_OS ($SIM_UDID)." - name: UI test the whole flow in the Simulator # Replays the committed synthetic wall (ios/HouseScanUITests/Fixtures/synthetic-wall) with - # -autopilot and the bundled sample result. iPhone 17 on iOS 26.5 ships with Xcode 26.6 on - # macos-26 (images/macos/macos-26-arm64-Readme.md). - run: >- - xcodebuild -project ios/HouseScan.xcodeproj -scheme HouseScan -configuration Debug - -destination "platform=iOS Simulator,name=iPhone 17,OS=26.5" - -resultBundlePath "$RUNNER_TEMP/HouseScanUITests.xcresult" - -only-testing:HouseScanUITests test + # -autopilot and the bundled sample result. + run: | + skip=() + if [ "$FULL_UI" != "true" ]; then + skip=(-skip-testing:HouseScanUITests/ScreenStatesUITests/testEveryStatePassesTheAccessibilityAudit) + echo "::notice::Skipped the every-state accessibility audit (about 10 min). Add the full-ui label to run it on this pull request." + fi + xcodebuild -project ios/HouseScan.xcodeproj -scheme HouseScan -configuration Debug \ + -destination "id=$SIM_UDID" \ + -resultBundlePath "$RUNNER_TEMP/HouseScanUITests.xcresult" \ + -only-testing:HouseScanUITests "${skip[@]}" \ + COMPILER_INDEX_STORE_ENABLE=NO test - name: Keep UI test screenshots if: always() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e2e1ceb9..20f705eb8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,10 +28,12 @@ The `size:*` label describes the effective diff. It is a review signal, not a me | --- | --- | --- | | Server checks | `server/` changes | `make server` | | Web checks | `web/` changes | `make web` | -| iOS build | `ios/` changes | `make ios` | +| iOS build | `ios/` changes outside Markdown, on non-draft pull requests | `make ios` | | Sync label definitions | `.github/labels.json` changes on `main` | none | | Label PR size | a pull request opens or updates | none | +The iOS UI tests skip the every-state accessibility audit on pull requests, because it adds about 10 minutes and macOS runners are scarce. Add the `full-ui` label when a pull request changes screens or copy; pushes to `t3/ios-mvf` and `main` always run it. + `make check` runs the three local suites. They need uv, Node 24 with pnpm, and Xcode 26 or newer; each directory's README has details. No check is required by branch rules yet. Don't call one required until the rules require its status. Keep workflows that run pull-request code away from production credentials and destructive external systems. A green CI run is evidence for the checks it ran, not proof that a capture works on a real house.