Skip to content

Add CI workflow - #1

Merged
gartnera merged 23 commits into
mainfrom
alexg/add-ci
Nov 7, 2026
Merged

gartnera merged 23 commits into
mainfrom
alexg/add-ci

Conversation

@gartnera

@gartnera gartnera commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Adds .github/workflows/ci.yaml, one workflow with a separate job for each check:

Job Runner What it runs
dart ubuntu dart format check, flutter analyze (package and example), flutter test
test-server ubuntu gofmt, go vet, go test for test_server/
swift-core macOS Starts the test server, then swift test on the Flutter-free ConnectNativeHttpCore
ios-integration macOS run_integration_tests.sh ios on a simulator: Dart suite, then native XCTest suite, as separate steps
android-integration ubuntu + KVM run_integration_tests.sh android on an API 34 emulator: Dart suite, then instrumented native suite
ci-ok ubuntu Depends on all of the above and fails unless every one succeeded. Mark this as the single required check.

The Android integration job also covers the example and Kotlin plugin build, so there's no separate build job. Flutter comes from .fvmrc via kuhnroyal/flutter-fvm-config-action, and Go from test_server/go.mod. On PRs, a new push cancels the in-progress run; runs on main always finish.

run_integration_tests.sh changes:

  • FLUTTER overrides the fvm flutter default, so CI can use the flutter on PATH.
  • Each run logs to build/integration_test_<platform>_<suites>.log, so the Dart and native runs don't overwrite each other. CI uploads the logs as artifacts.

Expect a few iterations on the simulator and emulator jobs; this PR is for that.

This PR was written by Claude.

🤖 Generated with Claude Code

gartnera and others added 23 commits November 6, 2026 14:38
One workflow, ci.yaml, with a job per check: Dart format/analyze/unit tests, the Go test server, the Swift core tests on macOS, the Android example build, and the iOS simulator and Android emulator integration tests, with the Dart and native bridge suites as separate steps. ci-ok depends on all of them, for use as the single required check.

run_integration_tests.sh takes FLUTTER to run without fvm, and writes one log per platform and suite.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
android-integration already builds the example and the plugin. Cancel superseded runs on main too, not just on PRs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Runs on main always finish, so every commit there gets a result.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A hung step now fails on its own name instead of using up the job's budget. The emulator gets a 5 minute boot timeout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The emulator exited at startup on a runner with 6.6 GB free (it needed 7.4 GB for userdata). Free preinstalled toolchains first and shrink the emulator disk to 4 GB.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Android: start the emulator ourselves at the top of the job and build the example and the plugin's instrumented tests while it boots, then run the Dart and native suites as separate steps. Cache the system image and Gradle. iOS: boot the simulator before setting up the toolchains.

run_integration_tests.sh: macOS's bash 3.2 treats an empty EXTRA_ARGS as unbound under set -u, and with the EXIT trap set the script then exited 0, so the iOS Dart suite never ran but passed.

DEBUG: a watchdog in the iOS native step reports what keeps the step running after its tests pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
avdmanager put the AVD outside ~/.android/avd on the runner, so the config edit failed. Set ANDROID_AVD_HOME for every tool. The native steps ran after the emulator setup failed, since !cancelled() ignores earlier failures.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
iOS: set up Go and Flutter and boot the simulator in one parallel group. Android: free disk space, enable KVM and restore the system image cache in parallel, then start the emulator and set up Java, Go and Flutter in parallel. The test suites stay sequential.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its first-boot data migration saturates the macOS runner's disk: in parallel, the Flutter SDK download fell from 36 MB/s to under 1 MB/s and setup-go timed out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the macOS runner the iOS native step hung after its tests passed: the test server outlived the script, re-parented to launchd, and held the log's tee open. The EXIT trap that should have stopped it was set inside `run | tee`'s subshell. Run in the main shell, logging through exec > >(tee), stop the server explicitly at the end of run as well as from the trap, and drop the debug watchdog that found this.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first few iOS Dart cases time out before the TLS ClientHello on every run. A second run shows whether that comes from the freshly booted simulator; the network log shows what the connections wait on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…back

Right after boot, the first minute of Dart-suite connections stalled before the TLS handshake, and a second run passed. Run the native XCTest suite first, which also exercises URLSession in the simulator.

URLSession races QUIC against TCP for each new connection and keeps whichever connects first; on the CI runner TCP sometimes wins, so the HTTP/3-only cases landed on HTTP/2. They now find a client whose connection negotiated HTTP/3 (checked with the test server's X-Proto), trying fresh clients for up to 8 s.

Keep recording the simulator's network log as a CI artifact; drop the debug rerun.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…r's QUIC

On the CI runner, URLSession sometimes connected to the local HTTP/3 origin over TCP: the server hadn't answered the QUIC Initial 230 ms after it was sent, while it answered TLS over TCP in 125 ms. Fallback is expected on real networks, but not on this path, so a new case fails when a fresh client's first call isn't HTTP/3.

test_server: let quic-go size the UDP socket's buffers (its 'Not a *net.UDPConn' warning), without exposing the socket in a way that bypasses Freeze, and write qlog traces to $QLOGDIR. CI uploads them with the simulator's network log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
iOS rebuilt everything on every run: cache Xcode's derived data (its simulator SDK module cache above all), the Flutter app's build directory and the core tests' build. The core tests' build moves to ios/connect_native_http/build_core_tests.sh, for a generic simulator destination, which CI runs alongside the toolchain setup and run_integration_tests.sh then finds up to date; it took 62-192 s of the native step, against 1.4 s of tests.

Android cached Gradle's dependencies but not its outputs: enable the build cache and keep ~/.gradle/caches/build-cache-1 between runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
flutter test once built and launched the app on CI, then waited 13 minutes without connecting to it, until the step timed out. run_integration_tests.sh now runs each command under a watchdog (macOS has no timeout(1)) that kills its whole process tree, so a hang fails in minutes and names the command. Test cases time out after 10 s (20 s for the HTTP/3 ones), and CI steps and jobs are sized to their observed durations.

Commit the project.pbxproj references Flutter's Swift Package Manager integration adds for the plugin and FlutterFramework, which it otherwise rewrites on every build. DEBUG: run the iOS Dart suite with -v.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The iOS job's native build failed with Permission denied: the script was committed as 644.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
On a fresh CI checkout build/ doesn't exist (the iOS build cache has never been saved, since every iOS job so far failed), so build_core_tests.sh's cd into it failed. set -e didn't stop it, because an assignment's status is its last command substitution's (basename), so the derived data path became /ios_core_tests and xcodebuild failed on the read-only root. run_integration_tests.sh creates build/ first, which is why it only failed when CI called the script directly.

Create the parent, and resolve it on a line of its own so a failed cd stops the script.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s
In run 42319973108's qlog, the server's first two QUIC connections (one per address family, from URLSession's Happy Eyeballs) took 86-159 ms from the client's Initial to the server's reply, against 2 ms for every later one. URLSession races QUIC against TCP and had connected over TCP by then, so the suite's first call went over HTTP/2; the client never sent another QUIC packet. Run 42316536743 shows the same: the first request was HTTP/2 and every later one HTTP/3.

The server now sends itself one request over HTTP/3 and one over HTTP/2, through loopback and verified against its CA, before it starts the admin endpoint that callers poll for readiness. It logs how long each took, which shows the cold-start cost on CI. The TCP listener is opened before serving, so the warm-up can't race it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s
It didn't help, and it ruled out the cause it was written for. In run 42324403392 the warm-up took 7 ms, and the first call of each suite still went over HTTP/2.

The simulator's network log shows why: when the app makes its first call, right after launch, the whole runner is CPU-starved. URLSession only started the QUIC connection 142 ms after its path was ready, TLS over TCP on loopback took 155 ms, and the server handled the QUIC Initial after the client had already picked TCP and cancelled QUIC. That is URLSession racing the two as designed, under load, not a slow QUIC path in the server or the library. The strict "first call to an HTTP/3 origin goes over HTTP/3" case, a later call, passed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s
…erbose

In run 42324403392, flutter test built and installed the example app on the emulator, then printed nothing for 8 minutes until the script's watchdog killed it; the server saw no request. It's the same symptom as the iOS launch hang. On iOS that's a known flutter_tools race (flutter/flutter#181771: it can miss the VM service URI in the simulator's log, fixed on master only), but the Android reader replays logcat from before it attaches, so that race doesn't apply. logcat and flutter's verbose output show whether the app crashed, never started, or announced a VM service flutter didn't see.

Raise the logcat buffer to 16M, which the emulator tries to do itself but fails while the device is still offline: it logs everything verbosely, and the default buffer wraps within seconds.

DEBUG: -v on the Android Dart suite.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s
…hang

In run 42325860453 the iOS Dart suite hung again, verbose this time: flutter test launched the app and logged "Waiting for VM Service port to be available..." for 6 minutes. That's flutter/flutter#181771: flutter test finds the app's VM service in the simulator's log through `log stream`, which drops events logged before it prints its first line, so the URL is sometimes missed and flutter waits forever.

Its fix, flutter/flutter#193142, is on master only. The iOS job now applies its lib/ change, fitted to 3.47.7, to the SDK and rebuilds the tool. The step fails once the patch stops applying, i.e. when .fvmrc moves, which is when to check whether the new release has the fix and drop it.

The Android Dart suite passed in that run, with logcat recorded; its hang in run 42324403392 is still unexplained.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s
Run 42327069006 reported "already applied" on its first run with the patch, and flutter --version didn't rebuild the tool: the SDK went unpatched, and the iOS suite passed without the fix. macOS's patch takes a reverse of a patch that isn't applied as a forward one, so `patch -R --dry-run` succeeds either way. Check for a line only the patch adds instead, and fail if it's still missing after patching.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BqmpXX3jjKH85A4Dvw1r1s
@gartnera
gartnera merged commit f8f6df3 into main Nov 7, 2026
6 checks passed
@gartnera
gartnera deleted the alexg/add-ci branch November 7, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants