Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 82 additions & 15 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,26 +73,39 @@ commands:
mise exec -- ruby -I Tools/Tests -e 'Dir["Tools/Tests/**/*_test.rb"].sort.each { |file| require File.expand_path(file) }'
./snapshot-shards check

validate_runner_environment:
validate_build_environment:
steps:
- run:
name: Validate runner environment
name: Validate build environment
command: |
echo "CPU: $(sysctl -n machdep.cpu.brand_string)"
echo "Logical CPUs: $(sysctl -n hw.logicalcpu)"
echo "Memory bytes: $(sysctl -n hw.memsize)"
echo "Architecture: $(uname -m)"
sw_vers
xcodebuild -version
echo "Available simulator runtimes:"
xcrun simctl list runtimes available

expected_xcode_build="$(< .xcode-build-version)"
xcode_build="$(xcodebuild -version | awk '/Build version/{print $3}')"
if [[ "$xcode_build" != "$expected_xcode_build" ]]; then
echo "Expected Xcode build $expected_xcode_build, found $xcode_build" >&2
exit 1
fi

echo "Checkout: $(git rev-parse HEAD)"
echo "Origin: $(git remote get-url origin)"
if git config --local --get-regexp 'credential|extraheader' >/tmp/git-credential-config.txt 2>/dev/null; then
echo "Unexpected persisted Git credential configuration:" >&2
sed -E 's/(authorization:).*/\1 [REDACTED]/I' /tmp/git-credential-config.txt >&2
exit 1
fi

validate_simulator_environment:
steps:
- run:
name: Validate simulator environment
command: |
echo "Available simulator runtimes:"
xcrun simctl list runtimes available
if ! xcrun simctl list runtimes available | grep -Eq '^iOS 27(\.0)? '; then
echo "An available iOS 27 simulator runtime is required" >&2
exit 1
Expand All @@ -102,18 +115,21 @@ commands:
echo "Selected simulator: $selected_simulator"
xcrun simctl list devices available | grep "$selected_simulator"

echo "Checkout: $(git rev-parse HEAD)"
echo "Origin: $(git remote get-url origin)"
if git config --local --get-regexp 'credential|extraheader' >/tmp/git-credential-config.txt 2>/dev/null; then
echo "Unexpected persisted Git credential configuration:" >&2
sed -E 's/(authorization:).*/\1 [REDACTED]/I' /tmp/git-credential-config.txt >&2
exit 1
fi
validate_runner_environment:
steps:
- validate_build_environment
- validate_simulator_environment

prepare_builder:
parameters:
test_workdir:
type: string
run_ci_helper_tests:
type: boolean
default: true
require_simulator:
type: boolean
default: true
steps:
- checkout_without_lfs
- restore_cache:
Expand All @@ -139,7 +155,10 @@ commands:
- ~/.local/share/mise/installs
- configure_test_workdir:
test_workdir: << parameters.test_workdir >>
- test_ci_helpers
- when:
condition: << parameters.run_ci_helper_tests >>
steps:
- test_ci_helpers
- run:
name: Generate project and resolve Swift packages
command: |
Expand All @@ -162,7 +181,11 @@ commands:
key: swiftpm-artifacts-v2-{{ arch }}-{{ checksum "Package.resolved" }}
paths:
- ~/Library/Caches/org.swift.swiftpm/artifacts
- validate_runner_environment
- validate_build_environment
- when:
condition: << parameters.require_simulator >>
steps:
- validate_simulator_environment

prepare_worker:
parameters:
Expand All @@ -178,13 +201,16 @@ commands:

run_with_timeout:
parameters:
step_name:
type: string
default: Run tests
seconds:
type: integer
command:
type: string
steps:
- run:
name: Run tests
name: << parameters.step_name >>
no_output_timeout: 45m
command: |
ruby -e '
Expand Down Expand Up @@ -316,6 +342,41 @@ jobs:
test_workdir: test-output-ios
artifact_name: test-ios

build-where-audiences:
executor: m4-pro-medium
parallelism: 2
steps:
- prepare_builder:
test_workdir: test-output-audience-build
run_ci_helper_tests: false
require_simulator: false
- run:
name: Select Where audience
command: |
case "$CIRCLE_NODE_INDEX" in
0) scheme="Where Beta" ;;
1) scheme="Where App Store" ;;
*)
echo "No Where audience for shard $CIRCLE_NODE_INDEX" >&2
exit 1
;;
esac
printf 'export WHERE_AUDIENCE_SCHEME=%q\n' "$scheme" >> "$BASH_ENV"
echo "Selected scheme: $scheme"
- run_with_timeout:
step_name: Build selected Where audience
seconds: 2700
command: >-
set -o pipefail;
mise exec -- tuist xcodebuild build
-workspace Stuff.xcworkspace
-scheme "$WHERE_AUDIENCE_SCHEME"
-destination "generic/platform=iOS Simulator"
2>&1 | tee "$TEST_WORKDIR/build.log"
- collect_diagnostics:
test_workdir: test-output-audience-build
artifact_name: where-audience-build

snapshot:
executor: m4-pro-medium
parallelism: 4
Expand Down Expand Up @@ -392,6 +453,12 @@ workflows:
name: Build & Test (iOS)
requires:
- Build iOS Tests
# Each release audience needs a complete compile. Separate workers keep
# two serial release builds out of the workflow's critical path.
- build-where-audiences:
name: Build Where Audiences
requires:
- Build iOS Tests
- snapshot:
name: Snapshot Tests (iOS)
requires:
Expand Down
36 changes: 27 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,26 @@ bootstrap, and process orchestration. In particular,

`./icons` is the single command for the Where app's alternate icons (see
`./icons --help`). It keeps both asset catalogs and the picker's
`AppIcons.json` manifest in sync. It stages and validates the whole change
before replacing anything and supports `--dry-run`. Never hand-edit those
files. Never add icon Swift. Run `./ide --no-open` after adding one.
`AppIcons.json` manifest in sync — never hand-edit those or add icon Swift.
It stages and validates the whole change before replacing anything and supports
`--dry-run`. Never hand-edit those files. Never add icon Swift. Run
`./ide --no-open` after adding one. Icon-set names are independent of
primary/alternate status: each Where audience selects its primary icon in
[`Project.swift`](Project.swift), and every other set remains selectable as an
alternate. Change an audience's primary before asking `./icons` to remove that
asset; the command refuses to delete any configured primary.

### Where build audiences

The Where host targets have three explicit schemes — **Where Development**
(`Debug`), **Where Beta** (`Beta`), and **Where App Store** (`Release`) — whose
audience descriptors live in [`Project.swift`](Project.swift). Keep bundle IDs,
App Group, display name, primary icon, configuration, and the matching
`WHERE_DEVELOPMENT` / `WHERE_BETA` / `WHERE_APP_STORE` compiler condition in
that one descriptor. These custom conditions belong only to the app, widget,
and share-extension targets; package targets receive audience-dependent values
by injection. Development uses its own bundle family and local-only store;
Beta and App Store share the production bundle family and CloudKit store.

### Version and build metadata

Expand Down Expand Up @@ -176,7 +193,7 @@ triage). **Always-on** rules every edit must honor stay in `AGENTS.md` or
`AGENTS.md` says what it is and how it can be used.
- Add SPM library targets in `Package.swift` and wire apps/tests in `Project.swift` (see existing `unitTests` helper. Native-macOS test bundles are declared directly, like `LedgerCoreTests`, since that helper hosts iOS bundles in StuffTestHost). A new module also ships a root `README.md` and `AGENTS.md` — see [Per-module docs](#per-module-docs).
- **CI schemes**: CI runs explicit shared schemes rather than the autogenerated `Stuff-Workspace` scheme. **Stuff-iOS-Tests** covers the iOS bundles. **Ledger-macOS-Tests** (the Ledger app + `LedgerCoreTests`) runs in its own `test-macos` job. The workspace mixes iOS targets with the native-macOS **Ledger** ones. No single xcodebuild destination can build both. Add a new test bundle to the matching scheme in `Project.swift`. If you do not, CI will not run it.
- **CircleCI build handoff**: CircleCI builds both iOS schemes sequentially on one `m4pro.large` builder. The unit worker and parallel snapshot workers attach its products and run without compilation. Each worker validates the build manifest before it starts a simulator. `./snapshot-shards` owns three planned suite assignments and one intake shard. A new suite runs on the intake shard until rebalancing adds it to the plan. Each active snapshot worker must execute exactly its assigned suites. The default-off `manual_full_gate` pipeline parameter runs this same clean-VM gate on an explicitly selected stacked branch tip; ordinary pushes and pull requests do not set it.
- **CircleCI build handoff**: CircleCI builds both iOS test schemes sequentially on one `m4pro.large` builder. The unit worker and parallel snapshot workers attach its products and run without compilation. Each worker validates the build manifest before it starts a simulator. Two `m4pro.medium` build-only shards compile **Where Beta** and **Where App Store** after the shared builder completes. `./snapshot-shards` owns three planned suite assignments and one intake shard. A new suite runs on the intake shard until rebalancing adds it to the plan. Each active snapshot worker must execute exactly its assigned suites. The default-off `manual_full_gate` pipeline parameter runs this same clean-VM gate on an explicitly selected stacked branch tip; ordinary pushes and pull requests do not set it.
- **Image snapshots are the exception: one bundle per module, one shared scheme.** Each module owning image references has its own `*SnapshotTests` target over its `SnapshotTests/` folder. All are listed in the single shared **StuffSnapshotTests** scheme and its dedicated CI `snapshot` job. Snapshots are slow and LFS-backed. They are **out of** `Stuff-iOS-Tests`. References under any `__Snapshots__/` directory are Git LFS (`.gitattributes`. The CI job hydrates them explicitly). Framework halves: `Shared/SnapshotKit` (shippable matrix + previews) and `Shared/SnapshotKitTesting` (test-only pipeline, whose own regression bundle **SnapshotKitTestingTests** pixel-probes without LFS and runs in `Stuff-iOS-Tests`).
- **A new image suite gets a target, not a scheme.** Add the `*SnapshotTests` target. List only `SnapshotKitTesting` in `extraPackageProducts`. Add it to the `StuffSnapshotTests` scheme's build and test lists. Never add a scheme or CI job of its own. An image bundle links only what its module needs (the Periscope and Inspector suites don't build against WhereUI at all). References follow the sources automatically via `#filePath`.
- **Separate snapshot bundles are safe because each `.xctest` gets its own `StuffTestHost` process** (measured on Xcode 27 — `ProcessInfo.processIdentifier` probes. Details in the snapshot-bundle comment in [`Project.swift`](Project.swift)). Each bundle statically embeds its own copy of `SnapshotKitTesting`'s capture state. Two copies in one process corrupt each other. Tripwire: if a toolchain ever shares one host process across bundles, re-measure before adding another image bundle.
Expand Down Expand Up @@ -664,10 +681,11 @@ one: ShellCheck, the retained Python and Ruby tool suites, SwiftFormat, the SF
Symbol lint, the String Catalog lint, and the attribution check. CircleCI
([`.circleci/config.yml`](.circleci/config.yml)) builds `build-ios-tests` once
and attaches its products to `test-ios` and a four-way-sharded `snapshot` job,
which moved there in PR #237 and gained the build handoff in PR #276. CircleCI
passes `--skip-architecture` so Bumper does not run twice. Do not read either file as
the whole of CI. PRs with `NO-CI` in their title skip both CI systems. Use this
marker only for prototypes and exploratory work. The marker does not skip
pushes to `main` or manual CircleCI full-gate runs. See the
plus two build-only shards for **Where Beta** and **Where App Store**. The iOS
tests moved there in PR #237 and gained the build handoff in PR #276. CircleCI
passes `--skip-architecture` so Bumper does not run twice. Do not read either
file as the whole of CI. PRs with `NO-CI` in their title skip both CI systems.
Use this marker only for prototypes and exploratory work. The marker does not
skip pushes to `main` or manual CircleCI full-gate runs. See the
[`running-tests`](.agents/skills/running-tests/SKILL.md) skill for simulator
setup and the full validation recipe.
Loading
Loading