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
121 changes: 81 additions & 40 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,23 +163,26 @@ jobs:
# swift test/build hang on macOS runners (see run_tests matrix note), and
# the bootstrap targets are mac-host-gated so ubuntu never sees them.
#
# Layers:
# Layers, one job each (fanned out 2026-09-02: serial, they ran seventy
# minutes — fast suite, skeletons, then four generated-app runs back to
# back; none feeds another, so the wall clock is now the longest leg):
# 1. Fast suite (config/tokens/emitter/consistency tests; skeletons
# self-skip without the env gate).
# 2. Walking skeletons: emit each shape, run its verification doors.
# TEST_RUNNER_ prefix forwards the gate to the test process.
# 3. Generation matrix: generated UI tests on real destinations — the
# automated BIG TEST. The least-proven leg (design §7): signing and
# simulator behavior on hosted runners may need iteration.
bootstrap_generation_matrix:
name: Scaffolder fast suite + skeletons + generated-app tests
# automated BIG TEST, one leg per app shape × destination. The
# least-proven leg (design §7): signing and simulator behavior on
# hosted runners may need iteration.
bootstrap_fast_suite:
name: Scaffolder fast suite
runs-on: macos-latest
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Install xcodegen
run: brew install xcodegen
Expand All @@ -204,6 +207,22 @@ jobs:
-scheme FOSUtilities-Package -destination "platform=macOS,arch=arm64,name=My Mac" \
-only-testing:FOSMVVMBootstrapTests test

bootstrap_skeletons:
name: Scaffolder walking skeletons
runs-on: macos-latest
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- uses: actions/checkout@v4

- name: Install xcodegen
run: brew install xcodegen

- name: Skip macro fingerprint validation
run: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES

- name: Walking skeletons
env:
TEST_RUNNER_FOSMVVM_BOOTSTRAP_SKELETONS: "1"
Expand All @@ -215,29 +234,62 @@ jobs:
-scheme FOSUtilities-Package -destination "platform=macOS,arch=arm64,name=My Mac" \
-only-testing:FOSMVVMBootstrapTests/IntegrationTests test

- name: Generate app shapes for the matrix
# One leg per (app shape × destination). Each leg builds the CLI and
# scaffolds its own app: a few minutes of build per leg buys running the
# four legs side by side instead of one after another.
bootstrap_generated_app_tests:
name: Generated UI tests (${{ matrix.app }}, ${{ matrix.destination }})
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
app: [LocalDemo, ServerDemo]
destination: [iOS Simulator, macOS]
include:
- app: LocalDemo
shape: localOnly
bundle_id_root: com.example.localdemo
- app: ServerDemo
shape: clientServer
bundle_id_root: com.example.serverdemo
- destination: iOS Simulator
xcode_destination: "platform=iOS Simulator,name=iPhone 17"
signing: "CODE_SIGNING_ALLOWED=NO"
- destination: macOS
xcode_destination: "platform=macOS,arch=arm64,name=My Mac"
signing: "CODE_SIGN_IDENTITY=- DEVELOPMENT_TEAM= CODE_SIGN_STYLE=Manual"
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- uses: actions/checkout@v4

- name: Install xcodegen
run: brew install xcodegen

- name: Skip macro fingerprint validation
run: defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES

- name: Build the scaffolder
run: |
set -o pipefail
xcrun xcodebuild -IDEClonedSourcePackagesDirPathOverride="$PWD/.dependencies" \
-skipMacroValidation -skipPackagePluginValidation \
-derivedDataPath "$PWD/.derivedData" \
-scheme fosmvvm-bootstrap -destination "platform=macOS,arch=arm64,name=My Mac" build

- name: Generate ${{ matrix.app }}
run: |
set -o pipefail
BIN="$PWD/.derivedData/Build/Products/Debug/fosmvvm-bootstrap"
if [ ! -x "$BIN" ]; then
xcrun xcodebuild -IDEClonedSourcePackagesDirPathOverride="$PWD/.dependencies" \
-skipMacroValidation -skipPackagePluginValidation \
-derivedDataPath "$PWD/.derivedData" \
-scheme fosmvvm-bootstrap -destination "platform=macOS,arch=arm64,name=My Mac" build
fi
mkdir -p "$RUNNER_TEMP/matrix"
cat > "$RUNNER_TEMP/local.json" <<'CONFIG'
{ "projectName": "LocalDemo", "shape": "localOnly",
"platforms": { "macOS": "14.0", "iOS": "17.0" },
"bundleIdRoot": "com.example.localdemo", "teamId": "ABCDE12345" }
CONFIG
cat > "$RUNNER_TEMP/server.json" <<'CONFIG'
{ "projectName": "ServerDemo", "shape": "clientServer",
cat > "$RUNNER_TEMP/app.json" <<CONFIG
{ "projectName": "${{ matrix.app }}", "shape": "${{ matrix.shape }}",
"platforms": { "macOS": "14.0", "iOS": "17.0" },
"bundleIdRoot": "com.example.serverdemo", "teamId": "ABCDE12345" }
"bundleIdRoot": "${{ matrix.bundle_id_root }}", "teamId": "ABCDE12345" }
CONFIG
"$BIN" new --config "$RUNNER_TEMP/local.json" --output "$RUNNER_TEMP/matrix/LocalDemo"
"$BIN" new --config "$RUNNER_TEMP/server.json" --output "$RUNNER_TEMP/matrix/ServerDemo"
"$BIN" new --config "$RUNNER_TEMP/app.json" --output "$RUNNER_TEMP/matrix/${{ matrix.app }}"

# CI simulators are ALWAYS the cold case: fresh boot, first install —
# exactly the measured first-run tap-flake condition (2026-08-22).
Expand All @@ -246,27 +298,16 @@ jobs:
# failure still fails both iterations. Durable fix is FOSTestingUI's
# first-interaction settle (follow-on) — then retries are belt-and-braces.
- name: Pre-boot iOS simulator
if: matrix.destination == 'iOS Simulator'
run: |
UDID=$(xcrun simctl list devices available | grep "iPhone 17 (" | head -1 | grep -oE "[0-9A-F-]{36}")
xcrun simctl boot "$UDID" || true
xcrun simctl bootstatus "$UDID" -b || true

- name: Generated UI tests (iOS Simulator)
run: |
set -o pipefail
for APP in LocalDemo ServerDemo; do
xcrun xcodebuild -project "$RUNNER_TEMP/matrix/$APP/$APP.xcodeproj" \
-scheme "$APP" -destination "platform=iOS Simulator,name=iPhone 17" \
-retry-tests-on-failure -test-iterations 2 \
CODE_SIGNING_ALLOWED=NO test
done

- name: Generated UI tests (macOS)
- name: Generated UI tests
run: |
set -o pipefail
for APP in LocalDemo ServerDemo; do
xcrun xcodebuild -project "$RUNNER_TEMP/matrix/$APP/$APP.xcodeproj" \
-scheme "$APP" -destination "platform=macOS,arch=arm64,name=My Mac" \
-retry-tests-on-failure -test-iterations 2 \
CODE_SIGN_IDENTITY=- DEVELOPMENT_TEAM= CODE_SIGN_STYLE=Manual test
done
xcrun xcodebuild -project "$RUNNER_TEMP/matrix/${{ matrix.app }}/${{ matrix.app }}.xcodeproj" \
-scheme "${{ matrix.app }}" -destination "${{ matrix.xcode_destination }}" \
-retry-tests-on-failure -test-iterations 2 \
${{ matrix.signing }} test
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **Doctor R4's testing-product remedy names both halves** — removing
`FOSTesting` from a non-test target is only half the fix; the finding now also
says to link the testing product directly on each test target that uses it,
and why the one-doorway rule does not apply there. Surfaced by the first
customer doctor run, where "remove the link" alone would have broken the
test bundles.
- **The scaffolder CI job fans out** — the serial seventy-minute job (fast suite,
walking skeletons, four generated-app UI-test runs) is now three jobs, the last
a four-leg matrix over app shape × destination. Each leg builds the CLI and
scaffolds its own app, so nothing waits on anything and the wall clock is the
longest leg.

## [0.15.1] - 2026-09-02

### Added
Expand Down
2 changes: 1 addition & 1 deletion Sources/FOSMVVMBootstrap/Doctor/ProjectRule+Linkage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ extension ProjectRule {
severity: .error,
target: target.name,
summary: "links \(testing.joined(separator: ", ")), which is a testing product, into a non-test target.",
remedy: "Remove the link. Testing products belong on test targets only — linked here they ride into the shipping app."
remedy: "Remove the link here, and link \(testing.joined(separator: ", ")) directly on each test target that uses it. Testing products belong on test targets only — linked here they ride into the shipping app — and they do not go through SPMLibraries: test-only types are never shared across targets, so the one-doorway rule does not apply to them."
)
}
}
Expand Down
Loading