-
Notifications
You must be signed in to change notification settings - Fork 0
170 lines (162 loc) · 8.56 KB
/
Copy pathtuist-apps.yml
File metadata and controls
170 lines (162 loc) · 8.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
name: Tuist Apps
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions: {}
jobs:
apps:
name: ${{ matrix.app }} (${{ matrix.platform }})
runs-on: macos-26
strategy:
fail-fast: false
matrix:
include:
- { app: NodeApp, platform: macos, action: test }
- { app: KernelApp, platform: macos, action: test }
- { app: NodeApp, platform: ios, action: test }
- { app: KernelApp, platform: ios, action: test }
# Roadmap — uncomment as each platform is brought up. `action` is a
# per-row knob: drop a flaky iOS-simulator `test` to `build` here
# without restructuring the matrix. New platforms also need a
# `-destination` in the "Resolve destination" step below.
# - { app: NodeApp, platform: visionos, action: test }
# - { app: KernelApp, platform: visionos, action: test }
# tvOS carries KernelApp only: the Bitcoin product's execvp() is
# __TVOS_PROHIBITED, so NodeApp cannot build on tvOS (mirrors the
# per-scheme split in apple-builds.yml's tvOS job).
# - { app: KernelApp, platform: tvos, action: build }
permissions:
contents: read
env:
DEVELOPER_DIR: "/Applications/Xcode_26.4.app/Contents/Developer"
WORKSPACE: Projects/Bitcoin.xcworkspace
DERIVED_DATA: .derivedData
steps:
- uses: actions/checkout@v6
# Generate the Xcode workspace from the Tuist manifest. Kept separate so a
# generation failure (e.g. an SPM version conflict in Projects/Project.swift)
# is isolated in the log.
#
# We generate with Tuist but build/test with xcodebuild DIRECTLY (not
# `tuist xcodebuild`): the `swift package tuist …` plugin can't forward
# xcodebuild's single-dash flags, because SwiftPM's own CLI parser reads
# `-workspace`/`-scheme` as short-flag clusters and the `-c` inside grabs
# the value (and `--` leaks through to xcodebuild). See `.github/AGENTS.md`.
- name: Generate workspace
run: swift package --disable-sandbox tuist generate -p Projects/ --no-open
# macOS is a fixed destination; for the iOS Simulator we resolve an
# available iPhone by UDID at runtime, which is robust against
# simulator-name churn across Xcode versions. Add cases here when
# uncommenting the visionOS/tvOS matrix rows above.
- name: Resolve destination
env:
PLATFORM: ${{ matrix.platform }}
run: |
case "$PLATFORM" in
macos)
echo "DESTINATION=platform=macOS" >> "$GITHUB_ENV" ;;
ios)
UDID=$(xcrun simctl list devices available | grep -E 'iPhone 1[6-9]' | grep -oE '[0-9A-Fa-f-]{36}' | head -1)
[ -z "$UDID" ] && UDID=$(xcrun simctl list devices available | grep 'iPhone' | grep -oE '[0-9A-Fa-f-]{36}' | head -1)
if [ -z "$UDID" ]; then echo "::error::no available iPhone simulator on the runner"; exit 1; fi
echo "DESTINATION=platform=iOS Simulator,id=$UDID" >> "$GITHUB_ENV" ;;
visionos)
UDID=$(xcrun simctl list devices available | grep 'Apple Vision' | grep -oE '[0-9A-Fa-f-]{36}' | head -1)
if [ -z "$UDID" ]; then echo "::error::no available visionOS simulator on the runner"; exit 1; fi
echo "DESTINATION=platform=visionOS Simulator,id=$UDID" >> "$GITHUB_ENV" ;;
tvos)
echo "DESTINATION=generic/platform=tvOS" >> "$GITHUB_ENV" ;;
*)
echo "::error::unhandled platform '$PLATFORM' — add a -destination case"; exit 1 ;;
esac
# Compile the app (and, for `test` rows, its test bundle) once. Signing is
# disabled: CI runners have no DEVELOPMENT_TEAM (supplied locally by the
# gitignored Resources/Project/Local.xcconfig), and these unit tests need no
# signed/entitled binary. The validation skips quiet the dev-plugin
# (tuist/subtree/docc) and macro prompts the package pulls on untagged refs.
- name: Build ${{ matrix.app }} (${{ matrix.platform }})
env:
SCHEME: ${{ matrix.app }}
ACTION: ${{ matrix.action }}
run: |
XCACTION=build
if [ "$ACTION" = "test" ]; then XCACTION=build-for-testing; fi
xcodebuild "$XCACTION" \
-skipMacroValidation -skipPackagePluginValidation \
-workspace "$WORKSPACE" -scheme "$SCHEME" \
-destination "$DESTINATION" -derivedDataPath "$DERIVED_DATA" \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO
# Run the pre-built tests WITHOUT recompiling — the point of the
# build-for-testing split: the iOS retry below re-runs only this step.
#
# iOS retry loop: the simulator test host intermittently segfaults at launch
# inside Apple's own UIApplicationMain prep (EXC_BAD_ACCESS in
# CFBundleGetIdentifier / _UIKitUserDefaults → NSUserDefaults init), aborting
# the bundle before our tests connect ("Early unexpected exit … test runner
# crashed before establishing connection"). The crash report has zero frames
# from our targets — an Apple/CoreSimulator launch race (~5-10% per launch
# locally, higher under CI load). xcodebuild does not relaunch the host and
# --retry-count only retries tests that ran, so we re-invoke
# test-without-building (no rebuild — fast). The retry is GATED on that crash
# signature: any other non-zero exit (a real test/build failure) fails fast
# and is never masked. macOS is single-attempt.
#
# The app test bundles are Swift Testing suites — results print as `◇`/`✔`
# lines and failures gate via the exit code. xcodebuild's legacy "Executed
# 0 tests" summary counts only XCTest cases (we have none); it does NOT mean
# tests were skipped.
- name: Test ${{ matrix.app }} (${{ matrix.platform }})
if: matrix.action == 'test'
env:
SCHEME: ${{ matrix.app }}
PLATFORM: ${{ matrix.platform }}
run: |
MAX=1
if [ "$PLATFORM" = "ios" ]; then MAX=5; fi
LOG="${RUNNER_TEMP:-/tmp}/twb-$SCHEME-$PLATFORM.log"
RESULTS="${RUNNER_TEMP:-/tmp}/xcresults"
mkdir -p "$RESULTS"
attempt=1
while true; do
echo "::group::test-without-building $SCHEME ($PLATFORM) — attempt $attempt/$MAX"
set +e
xcodebuild test-without-building \
-skipMacroValidation -skipPackagePluginValidation \
-workspace "$WORKSPACE" -scheme "$SCHEME" \
-destination "$DESTINATION" -derivedDataPath "$DERIVED_DATA" \
-resultBundlePath "$RESULTS/$SCHEME-$PLATFORM-attempt-$attempt.xcresult" \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO 2>&1 | tee "$LOG"
rc=${PIPESTATUS[0]}
set -e
echo "::endgroup::"
if [ "$rc" -eq 0 ]; then break; fi
if ! grep -qE "Early unexpected exit|test runner crashed before establishing connection" "$LOG"; then
echo "::error::$SCHEME ($PLATFORM) failed — not the launch-crash flake; not retrying"
exit "$rc"
fi
if [ "$attempt" -ge "$MAX" ]; then
echo "::error::$SCHEME ($PLATFORM) hit the simulator launch-crash flake $MAX times"
exit 1
fi
echo "Simulator launch-crash flake — re-running tests without rebuild (attempt $((attempt + 1))/$MAX)"
attempt=$((attempt + 1))
done
# Xcode's structured record of the run. Without it the only evidence a
# failure leaves is this step's log, which flushes in batches — the
# 2026-08-20 KernelApp (ios) failure printed at 07:35:35 an event that
# actually happened at 07:35:33, which destroyed the timing needed to see
# which test groups overlapped. The `if:` is REQUIRED: a step with no
# condition is skipped once an earlier step failed, i.e. exactly when the
# bundle is worth having. One bundle per attempt (the retry loop above
# would otherwise collide — xcodebuild refuses an existing bundle path),
# and one artifact name per matrix row so concurrent rows don't clash.
- name: Upload test results (${{ matrix.app }}, ${{ matrix.platform }})
if: failure() && matrix.action == 'test'
uses: actions/upload-artifact@v7
with:
name: xcresult-${{ matrix.app }}-${{ matrix.platform }}
path: ${{ runner.temp }}/xcresults
retention-days: 14
if-no-files-found: ignore