-
Notifications
You must be signed in to change notification settings - Fork 0
352 lines (334 loc) · 17.4 KB
/
Copy pathci.yml
File metadata and controls
352 lines (334 loc) · 17.4 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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
macos:
name: macOS (${{ matrix.mode }})
runs-on: macos-15
timeout-minutes: 15
# The executor-drain is the unconditional default on this runner (macOS 15,
# Swift 6 — see `_makeTestExecutorBox`); there is no flag and no wall-clock
# comparison row (that lived in pre-removal history). BOTH modes are now
# REQUIRED (no `continue-on-error`):
# • SERIAL — the primary deterministic correctness gate (caught the OR-path
# race fixed in 497c2ab under serialized execution).
# • PARALLEL — validates the framework's parallel-safety claim, a real
# differentiator vs. e.g. TCA's enforced `@MainActor`. It was informational
# while a small `waitUntil`-based tail flaked on the small runners; both
# causes are now fixed (design note Updates 22–24): `testObservedStream`'s
# never-true wait fails catchably so `withKnownIssue` absorbs it, and
# `testSharedDependency` awaits its shared-dep observations reactively up
# front instead of via teardown timing. Verified green across repeated CI
# runs, so it now blocks merges too.
strategy:
fail-fast: false
matrix:
include:
- mode: parallel
flag: --parallel
- mode: serial
flag: --no-parallel
steps:
- uses: actions/checkout@v6
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest
- name: Build and test (${{ matrix.mode }})
# `--parallel` validates the framework's parallel-safety claim — a real
# differentiator vs. e.g. TCA's enforced `@MainActor`. `--no-parallel` is
# the deterministic regression gate (caught the OR-path race in 497c2ab).
# `fail-fast: false` so one mode's flake doesn't cancel the other's signal.
#
# `SWIFT_MODEL_TIMEOUT_SCALE=3` multiplies all test-infrastructure budgets
# (expect/settle 5 s → 15 s; cleanup settle 25 s → 75 s; trait cap 30 s →
# 90 s; meta-test bounds scale too). The small CI runners stall under
# saturation; locally tests use scale=1 (default) for fast feedback.
#
# Benchmarks live in `SwiftModelBenchmarkTests` (skipped from CI; run via
# `swift test --filter SwiftModelBenchmarkTests`).
env:
SWIFT_MODEL_TIMEOUT_SCALE: "3"
run: swift test ${{ matrix.flag }} --skip SwiftModelBenchmarkTests
tsan:
name: macOS (TSan)
runs-on: macos-15
# ThreadSanitizer gate over the full parallel suite. Added after the
# 2026-07-02 concurrency audit: the suite is TSan-clean (the audit's 25
# warnings / 5 distinct races are fixed), so any new report is a regression.
#
# Design notes:
# • macOS only. Swift TSan works on Linux too, but Linux CI already needs
# the `scripts/ci-test` IPC-noise wrapper; keep the sanitizer signal on
# the platform where it's cleanest. Parallel mode — cross-test
# scheduling diversity is what surfaces races.
# • `--skip testObservedStreamWithModelAccessingObservable`: the one
# documented-unsupported test (plain `@Observable` interop) that races
# BY DESIGN in its own test code — see "Known load-sensitive tests" in
# Docs/Contributing/TestInfrastructure.md. Everything else must be clean.
# • `SWIFT_MODEL_TIMEOUT_SCALE=6` (not 3): TSan slows execution 5–15×,
# so wait budgets need more headroom than the plain jobs. The first CI
# run of this job caught `withTestTimeout_cancelsBodyThroughWaitPrimitive`
# flaking because its cancel-propagation margin was a FIXED 5 s (the
# timer→cancelAll→resume chain measured > 5 s wall-clock under TSan on
# the saturated runner); that margin now scales with this env var like
# the file's other bounds, so raising the scale is safe if ever needed.
# • TSan reports do NOT fail `swift test`'s exit code (no
# `halt_on_error`); the follow-up step scans the log and fails on any
# `WARNING: ThreadSanitizer`, printing the full reports. Running to
# completion (rather than halt-on-first) surfaces ALL races in one run.
timeout-minutes: 45
steps:
- uses: actions/checkout@v6
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest
- name: Build and test under ThreadSanitizer (parallel)
env:
SWIFT_MODEL_TIMEOUT_SCALE: "6"
run: |
set -o pipefail
swift test --parallel \
--skip SwiftModelBenchmarkTests \
--skip testObservedStreamWithModelAccessingObservable \
--sanitize=thread 2>&1 | tee tsan.log
- name: Fail on ThreadSanitizer reports
# Separate step so a test failure above and a race report here are
# distinguishable in the job UI. Prints every full report (WARNING …
# ================== blocks) before failing.
run: |
count=$(grep -c 'WARNING: ThreadSanitizer' tsan.log || true)
if [ "${count}" -gt 0 ]; then
echo "::error::ThreadSanitizer reported ${count} warning(s)"
awk '/WARNING: ThreadSanitizer/,/^==================$/' tsan.log
exit 1
fi
echo "ThreadSanitizer: clean (0 warnings)"
linux:
name: Linux (${{ matrix.mode }})
runs-on: ubuntu-latest
timeout-minutes: 12
# See the macOS job: the drive is the unconditional default (Swift 6.3
# container). BOTH modes are now REQUIRED — serial is the deterministic gate,
# and parallel (formerly informational while the `waitUntil` tail flaked on
# the 2-vCPU container) is now green after the Update 22–24 fixes, so it
# blocks merges too.
container:
image: swift:6.3.0
strategy:
fail-fast: false
matrix:
include:
- mode: parallel
flag: --parallel
- mode: serial
flag: --no-parallel
steps:
- uses: actions/checkout@v6
- name: Cache Swift build artifacts
uses: actions/cache@v5
with:
path: .build
# `Package.resolved` isn't committed (no `hashFiles` input there), so
# we hash `Package.swift` instead — that's what determines the
# resolved dependency graph. Without this, every job hashed to the
# same key and a stale `.build/workspace-state.json` from a previous
# resolve (e.g. an older `swift-custom-dump` revision with a
# different trait declaration) survived across runs and produced
# spurious trait-resolution errors. The `v2-` prefix is a manual
# cache-bust — bump it whenever a stale-cache failure recurs.
# `restore-keys` is intentionally narrowed to the Package.swift hash
# too so partial-prefix matches can't pull in a workspace state from
# an older manifest. With this scheme, every Package.swift change
# forces a full re-resolve (the build is incremental anyway).
# Mode is part of the key so parallel/serial don't fight over
# build artifacts (different test ordering can trigger different
# incremental rebuild patterns).
key: ${{ runner.os }}-swift-6.3-${{ matrix.mode }}-v2-${{ hashFiles('Package.swift') }}
restore-keys: |
${{ runner.os }}-swift-6.3-${{ matrix.mode }}-v2-${{ hashFiles('Package.swift') }}
- name: Build and test (${{ matrix.mode }})
# See the macOS job for the dual-mode rationale and
# `SWIFT_MODEL_TIMEOUT_SCALE` justification.
#
# Linux goes through `scripts/ci-test` rather than calling `swift test`
# directly. On Linux, swift-syntax's compiler-plugin message handler
# intermittently logs `Internal Error: DecodingError … Corrupted JSON …
# unexpected end of file` during macro expansion (a truncated/EOF frame
# on the compiler ↔ macro-plugin IPC pipe). It is emitted on essentially
# every Linux build — including passing ones — but can occasionally make
# `swift test` exit non-zero even though the build compiled and EVERY
# test passed. That is exactly how `Linux (parallel)` flaked on
# run 28446231009 (`Test run with 764 tests … passed`, yet job failed).
# An upstream toolchain artifact, not a SwiftModel bug. The wrapper
# treats "build compiled + test run passed + zero real failures" as
# success and still fails hard on any genuine test/build failure — it
# does NOT retry or mask real failures. macOS is unaffected (zero
# occurrences), so it keeps calling `swift test` directly.
env:
SWIFT_MODEL_TIMEOUT_SCALE: "3"
run: scripts/ci-test ${{ matrix.flag }} --skip SwiftModelBenchmarkTests
android:
name: Android (aarch64)
# Compile-only cross-compile check for aarch64-unknown-linux-android28.
# No test execution on Android — see the comment block below for why.
#
# History:
# • `13961d6` switched this job to `skiptools/swift-android-action@v2`,
# which builds + pushes test binaries to an emulator and runs them.
# That worked at the time but consistently fails on this branch
# against Swift 6.3 with errors of the form:
# error: Failed opening
# `.build/<triple>/debug/index/store/v5/units/<File>.swift.o-<hash>`
# No such file or directory
# emitted while generating `all-discovered-tests.swift`. The Swift
# 6.3 cross-compiler writes index units to the HOST store
# (`x86_64-unknown-linux-gnu`), but SwiftPM's package-level
# test-discovery looks for them in the TARGET store
# (`x86_64-unknown-linux-android28`) — they're never found.
# • The manual workaround below (seed the target store from the host
# store before cross-compiling) is what was working pre-`13961d6`.
# We've restored it. Once Swift's cross-compiler is fixed to write
# units to the target store directly, this whole compile-only job
# can be replaced again by `swift-android-action` for real test
# execution on the emulator.
#
# Run on Linux: canImport(ObjectiveC/AppKit/UIKit) is false on Linux,
# which correctly matches the Android target and avoids false-positive
# compilation errors that occur when cross-compiling from macOS (where
# those checks evaluate against the macOS host instead of the Android
# target).
runs-on: ubuntu-latest
timeout-minutes: 10
container:
image: swift:6.3.0
steps:
- uses: actions/checkout@v6
- name: Install Android NDK r27d and Swift Android SDK
run: |
apt-get update -qq
apt-get install -qq -y unzip wget 2>/dev/null
wget -q "https://dl.google.com/android/repository/android-ndk-r27d-linux.zip" -O /tmp/ndk.zip
unzip -q /tmp/ndk.zip -d /opt
export ANDROID_NDK_HOME=/opt/android-ndk-r27d
echo "ANDROID_NDK_HOME=$ANDROID_NDK_HOME" >> "$GITHUB_ENV"
swift sdk install \
"https://download.swift.org/swift-6.3-release/android-sdk/swift-6.3-RELEASE/swift-6.3-RELEASE_android.artifactbundle.tar.gz" \
--checksum 2f2942c4bcea7965a08665206212c66991dabe23725aeec7c4365fc91acad088
# swift sdk install uses FileManager.homeDirectoryForCurrentUser (getpwuid)
# on Linux, which returns /root regardless of the $HOME env var. GitHub
# Actions containers set HOME=/github/home, but the bundle always lands
# in /root/.swiftpm/swift-sdks/ when running as the root container user.
SETUP_SCRIPT=$(find /root/.swiftpm/swift-sdks -name "setup-android-sdk.sh" 2>/dev/null | head -1)
if [ -z "$SETUP_SCRIPT" ]; then
echo "setup-android-sdk.sh not found. Bundle contents:"; find /root/.swiftpm | head -20; exit 1
fi
bash "$SETUP_SCRIPT"
- name: Build Linux tests (seed index store for Android cross-compilation)
# Workaround for a Swift 6.3 cross-compilation bug: the cross-compiler
# writes index units to the HOST store instead of the Android store.
# Building for Linux first seeds those units so that SPM's test-discovery
# step can find them when cross-compiling for Android (next step copies
# them to the Android index store path).
run: swift build --build-tests
- name: Seed Android index store from Linux build
run: |
LINUX_STORE=".build/x86_64-unknown-linux-gnu/debug/index/store/v5"
ANDROID_STORE=".build/aarch64-unknown-linux-android28/debug/index/store/v5"
mkdir -p "$ANDROID_STORE/units" "$ANDROID_STORE/records"
cp "$LINUX_STORE/units/"* "$ANDROID_STORE/units/"
for subdir in "$LINUX_STORE/records/"*/; do
name=$(basename "$subdir")
mkdir -p "$ANDROID_STORE/records/$name"
cp "$subdir"* "$ANDROID_STORE/records/$name/"
done
- name: Build SwiftModelTests for Android (aarch64)
# ANDROID_NDK_ROOT must be unset: it overrides ANDROID_NDK_HOME and
# breaks the SDK bundle's NDK path resolution.
run: |
unset ANDROID_NDK_ROOT
swift build \
--swift-sdk aarch64-unknown-linux-android28 \
--build-tests
wasm:
name: WASM (wasm32-unknown-wasip1)
# Run on Linux: same reasoning as Android — host-side canImport checks
# (ObjectiveC, AppKit, UIKit) evaluate against the container OS, which
# correctly reflects the WASM target's behaviour.
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: swift:6.3.0
# Both levers are set job-wide so every `swift` invocation evaluates the
# same manifest configuration (a step that omitted one would force SwiftPM
# to re-evaluate manifests and rebuild).
#
# SWIFTPM_TARGET_WASI — ours: trims the package to `SwiftModel`,
# `SwiftModelMacros` and `SwiftModelTests`. It was introduced *because
# of* the dynamic-product problem below, and `OMIT_DYNAMIC_TEST_SUPPORT`
# now solves that at the root — but the lever is still required, for a
# second reason that only surfaced once this job started running
# `--build-tests`: that builds the whole package, including the
# `SwiftModelBenchmarks` executable, whose harness is built on
# `DispatchTime` / `DispatchQueue.concurrentPerform` and does not
# compile for WASI. Don't drop this lever without also handling that.
#
# OMIT_DYNAMIC_TEST_SUPPORT — xctest-dynamic-overlay's, added in 1.11.0
# via pointfreeco/swift-issue-reporting#183. Demotes the
# `IssueReportingTestSupport` product from `.dynamic` to automatic
# (static) linkage. WASI has no shared-library support, and SwiftPM
# materialises that product into any test build plan where it's
# reachable in the resolved graph — even when consumer-side target deps
# platform-condition it out — so before 1.11.0 the test-executable link
# failed unconditionally and this job could only compile-check
# individual targets.
env:
SWIFTPM_TARGET_WASI: "1"
OMIT_DYNAMIC_TEST_SUPPORT: "1"
steps:
- uses: actions/checkout@v6
- name: Install Swift WASM SDK
run: |
swift sdk install \
"https://download.swift.org/swift-6.3-release/wasm-sdk/swift-6.3-RELEASE/swift-6.3-RELEASE_wasm.artifactbundle.tar.gz" \
--checksum 9fa4016ee632c7e9e906608ec3b55cf13dfc4dff44e47574c5af58064dc33fd9
- name: Build SwiftModel for WASM (compile check)
# The library on its own, with no test scaffolding in the graph — this
# is what an actual WASM consumer builds. Kept as a separate step so a
# library-level WASM break is distinguishable at a glance from a
# test-only one.
run: |
swift build \
--swift-sdk swift-6.3-RELEASE_wasm \
--target SwiftModel
- name: Build test bundle for WASM (compile + link)
# `--build-tests` (rather than `--target SwiftModelTests`) is the point
# of this step: it links the test executable, which is strictly more
# than a compile check and is exactly what the dynamic-product problem
# used to block. See the job-level `env:` block above.
run: |
swift build --build-tests \
--swift-sdk swift-6.3-RELEASE_wasm
- name: Install xz (unpacks the wasmtime release tarball)
# The swift:6.3.0 container image doesn't ship xz, and the setup
# action below extracts a .tar.xz.
run: apt-get update -qq && apt-get install -y -qq xz-utils
- name: Install wasmtime
uses: bytecodealliance/actions/wasmtime/setup@v1
with:
version: "49.0.1"
- name: Run WASM smoke under wasmtime
# The test bundle above can't run on WASI yet: `GlobalTickScheduler`
# (the deadline source behind `expect` / `settle` / `waitUntil`) is
# GCD-backed and would need a WASI-native path first. A plain
# executable runs fine, so `Tests/WASMSmoke` exercises at runtime the
# code paths that have broken on wasm32 while compiling cleanly — e.g.
# the over-aligned key path index bug that made
# `LocalStorage<UInt64>` trap (see `OverAlignedKeyPathIndexTests`).
run: scripts/wasm-smoke
env:
SWIFT_WASM_SDK: swift-6.3-RELEASE_wasm