Skip to content

GH-5125 LocalGate: every CI suite on one box through Bobcat's Gate - #5246

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-5125-local-gate
Oct 28, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
gh-5125-local-gate

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5125.

./build.sh LocalGate runs what the CI workflows run (tests.yml, dotnet.yml, http.yml, fsharp.yml, command-line.yml, bobcat-samples.yml) on one box, through Bobcat's Gate (Bobcat.Supervisor 1.0.0). It builds once, runs every test suite as a unit of one gate (longest first, one track per Docker service), then the gates that aren't test hosts.

What it does

  1. Starts or reuses every compose service the selected suites need, without waiting for them.
  2. Builds once, Release, while the containers boot. That's one dotnet build per framework over a generated .slnx of the selected test projects (artifacts/local-gate/).
  3. Waits for the services' readiness probes (AwaitDockerServices). Then it runs 62 suites as units of one gate under one worker budget (default half the cores). Each unit's supervisor is configured exactly as its CI target's is (configureSupervisor, factored out of runSupervised). Each unit leaves its ledger as artifacts/test-ledger/LocalGate.*.json.
  4. LocalGate then runs the extras as the ordinary targets they are: CICommandLineSmoke, CIAotSmoke, BobcatSamples, then CICodegenTest and CICodegenDrift. Drift goes last because it regenerates the tree that codegen-test wipes.

./build.sh LocalGateSuites runs the suites alone; --gate-only <name>... runs a slice; --gate-workers N sets the budget.

Isolation: the tracks

A track is held by one unit at a time. What collides on a shared server is names, not the server:

  • Postgres / SQL Server. A suite that only uses the database its connection string names gets its own database, wolverine_gate_<project>. It's recreated when the unit starts and handed over as ConnectionStrings__postgres / ConnectionStrings__sqlserver, which Servers.cs reads first. That suite holds no track.
    • Suites that create sibling databases under fixed names (tenant1-4, db1-3, players, things, …) run against the default database as in CI and hold the postgresql / sqlserver track: MartenTests, PostgresqlTests, PersistenceTests, both EF suites, PolecatTests.
    • SqlServerTests keeps its four wolverine_w* lanes.
  • Brokers and emulators: one track per compose service.
  • Fixed ports and heavy emulators:
    • mqtt-broker-ports: both MQTT suites host a broker on 1883/1884.
    • cosmos-emulator: a Testcontainers emulator per suite.
    • ravendb.
  • Everything else is limited by the worker budget only.

Also in this PR

  • CICommandLineSmoke (build/CommandLineSmoke.cs): command-line.yml's describe-handlers and event-model checks, moved out of inline shell into a target. The workflow now calls it, so CI and the gate run the same checks.
  • The F# suites from fsharp.yml are gate units: supervised here, dotnet test in that workflow, which is unchanged.
  • VerifyLocalGateCoverage (now part of ./build.sh ci) fails in two cases:
    • A test project in wolverine.slnx is in neither the table in build/LocalGate.cs nor its NotGated list.
    • build/gates.json, which ./build.sh LocalGateManifest generates from the same table, is stale.
  • Release by default: Configuration is Release whenever LocalGate or LocalGateSuites is invoked, unless --configuration is given, so the extras build like CI too. Everywhere else it is unchanged.
  • Ordering and proceed-after-failure:
    • CICodegenTest gets After(...) and ProceedAfterFailure; CICodegenDrift gets After(CICodegenTest).
    • The three Bobcat sample targets get ProceedAfterFailure, so one red extra doesn't skip the rest.
    • In CI each of these runs alone, so nothing changes there.
  • Docs: .claude/docs/test_gates.md and CLAUDE.md.

Not gated (in no workflow)

19 test projects run in no CI workflow, so they're listed in NotGated with their reasons:

  • the four *.LeaderElection suites for Postgres, SQL Server, MySQL and Oracle;
  • SignalR, HealthChecks, BackPressure, Metrics, Diagnostics and PolicyTests;
  • SlowTests (manual workflow) and ChaosTesting;
  • several sample test projects.

That they run nowhere is worth a separate look.

Measured

./build.sh LocalGate on an 18-core M-series Mac (128 GB), default 9-worker budget, first run, so no recorded durations yet:

  • Green, 43m52s in all.
    • Build: 33s.
    • Suites: 40m03s, against 3h09m one after another.
    • Extras: 3m16s.
  • All 62 units passed. Five suites had tests that passed on retry, which are reported as flaky, not clean:
    • EfCoreTests: 4 of the 5 are one class, serverless_with_outbox_local_messages_5046_on_sqlserver. It ran while SqlServerTests had 4 lanes busy on the same server.
    • OracleTests: 2.
    • SqlServerTests, AmazonSqs and Pubsub: 1 each.
  • The long poles:
    • The postgresql track chain, about 40m: PostgresqlTests 10m, MartenTests 14m, EfCoreTests 6m, EfCoreTests.MultiTenancy 7m, PersistenceTests 3m.
    • Wolverine.AzureServiceBus.Tests, 27m on one worker. CI shards it three ways.
  • The next run orders units longest first from .bobcat/gate-durations.json. That puts ASB first, so the postgresql chain sets the wall clock.

Known gaps

  • With .nuke/parameters.json's net9.0, CI's EF Core 10 job (CIEfCoreNet10) is covered only by --framework all / --framework net10.0.
  • The sibling-database suites serialize on the postgresql / sqlserver tracks. Moving their fixed names under the suite's database (the way LaneDatabases does for SqlServerTests) would let them overlap.

🤖 Generated with Claude Code

./build.sh LocalGate builds once (Release, one generated .slnx per framework), runs the
62 test projects the CI workflows run as units of one Bobcat gate -- longest first,
under one worker budget, one track per Docker service, a database of its own for each
suite that allows it -- then the gates that are not test hosts: CICommandLineSmoke,
CIAotSmoke, BobcatSamples, CICodegenTest, CICodegenDrift.

- LocalGateSuites runs the suites alone; --gate-only and --gate-workers narrow it.
- configureSupervisor/workerFactoryFor factored out of runSupervised, so a gate unit is
  configured exactly as its CI target is.
- CICommandLineSmoke: command-line.yml's describe-handlers and event-model checks as a
  target; the workflow now calls it.
- VerifyLocalGateCoverage (in ./build.sh ci): every test project in wolverine.slnx is a
  gate suite or in NotGated with a reason, and build/gates.json is current.
- Configuration defaults to Release when LocalGate/LocalGateSuites is invoked.

Measured: green, 43m52s in all; suites 40m03s against 3h09m one after another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 8b0d297 into main Oct 28, 2026
87 of 88 checks passed
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.

LocalGate: run the full CI gate set locally through Bobcat's Gate, build once, longest first, tracks per Docker service

1 participant