Harden CI workflows for Node 24 and Xcode#78
Merged
Conversation
…ailures)
The Xcode Swift test testBestMoveFromStartPositionIsUciMove intermittently failed
on CI with "Move was nil!" (~51s timeout). Root cause was a startup race: the
`isready`/`readyok` handshake could be dropped because ChessKitEngine's
responseStream is hot — a response emitted before `for await` begins iterating is
lost — and NNUE `setoption`s were sent before `uciok` (when the engine may silently
drop them), so `readyok` never arrived within the 30s timeout on a contended
3-core Debug CI runner.
Fix restructures the SharedStockfishCore init handshake into a strict sequence:
1. start engine + get stream
2. START iterating the stream BEFORE sending any command (so no response is missed)
3. send `uci`, await `uciok` via a CheckedContinuation (async-native; blocking a
DispatchSemaphore from async code is a Swift 6 language-mode error)
4. send NNUE EvalFile/EvalFileSmall setoptions + Skill Level
5. send `isready` (readyok awaited lazily by the first runSearch via readySemaphore)
Also bumps sharedReadyTimeout 30s → 90s: a cold start on contended CI can spend
30-40s loading NNUE before acknowledging readyok.
Verified locally: testBestMoveFromStartPositionIsUciMove now passes in ~2s
(was ~51s timeout); all 4 Swift tests pass; zero Swift warnings. The retry
logic in getBestMove/evaluate remains as a safety net.
The previous CheckedContinuation approach (commit c319fca) crashed the test process on CI ~10s into testBestMoveFromStartPositionIsUciMove — xcodebuild reported "Restarting after unexpected exit, crash, or test timeout". The crash was from the continuation being abandoned (when the stream/engine tore down before uciok arrived) — CheckedContinuation traps in that case. Redesign: drive the ENTIRE uci→uciok→setoptions→isready→readyok handshake inline from the single consumer `for await response in stream` loop. This: - eliminates cross-task signaling entirely (no continuation, no second Task) - is race-free by construction (the loop is single-threaded over the stream) - guarantees commands are sent in exact order and no response is missed (the original root cause: the consumer started iterating after isready was sent, so readyok was dropped) Once the handshake completes (readyok received), the same loop routes runtime responses (info/bestmove) to handleRuntime(). The readySemaphore remains for runSearch's sync waitUntilReady() (called off the main thread, never from async). Verified locally on a clean simulator: all 4 Swift tests pass; the first (cold-start) test takes ~26s for NNUE load but completes with a valid move.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Updates the CI workflows for current GitHub Actions runtimes and hardens the Android and Apple test jobs that were producing warnings or failures on PR #78. Also fixes an iOS Stockfish startup race that caused the Xcode Swift tests to return nil moves/scores on CI.
Why
GitHub Actions warns when Node 20-backed JavaScript actions are forced onto Node 24. Android tests also warned about a stale emulator-runner input and a report artifact path that does not match the KMP connected test layout. Apple jobs used
macos-latest, which GitHub is migrating to macOS 26 beginning June 15, 2026.The Xcode job also exposed an iOS Stockfish readiness race: the wrapper could read
Engine.responseStreambefore ChessKitEngine had created it, exit the detached startup task, and leavegetBestMove/evaluatewaiting for readiness until they returned nil.Changes
reactivecircus/android-emulator-runnertov2.38.0so the Android emulator action itself runs on Node 24.boot-timeoutinput withemulator-boot-timeout.macos-15to avoid themacos-latestmacOS 26 migration warning.OS=latest,arch=arm64and disable parallel testing for the Swift test run.Validation
.github/workflows/*.ymlgit diff --check --cachedboot-timeout,macos-latest,androidTests/connected/debug/index.html, and the old emulator runner tagStockfishChessEngineTests/testBestMoveFromStartPositionIsUciMoveandStockfishChessEngineTests/testEvaluateStartPositionIsRoughlyBalancedon an arm64 simulator