From 1ba98a545101d756b0e1e1b5baefe86bfd721e14 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Apr 2026 02:24:50 +0000 Subject: [PATCH 1/8] ci: route all workflows to self-hosted Linux runners Repo hit the private-repo free-minutes limit, so ubuntu-latest jobs fail instantly on billing. Route every workflow to the org's self-hosted pool via the auto-applied [self-hosted, Linux] labels. https://claude.ai/code/session_01TjofiprA2cKgSYqzgzMScn --- .github/workflows/admin-approval.yml | 2 +- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/claude-agent.yml | 2 +- .github/workflows/claude-review.yml | 2 +- .github/workflows/dependabot-automerge.yml | 2 +- .github/workflows/label.yml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/project-orchestrator.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/triage.yml | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/admin-approval.yml b/.github/workflows/admin-approval.yml index 8706d7bb..d5d59cb0 100644 --- a/.github/workflows/admin-approval.yml +++ b/.github/workflows/admin-approval.yml @@ -32,7 +32,7 @@ concurrency: jobs: admin-approval: name: Admin approval - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - name: Check for admin approval env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ef6fc57..7cf27e52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ permissions: jobs: check: name: Check - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -34,7 +34,7 @@ jobs: lint: name: Lint - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -54,7 +54,7 @@ jobs: test: name: Test - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -83,7 +83,7 @@ jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -96,7 +96,7 @@ jobs: integration: name: Integration Tests - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] needs: [test] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -113,7 +113,7 @@ jobs: # Vitest unit tests for the TypeScript SDK. Fast (~30s) and runs on # every PR — no path filter so a deps bump or workflow tweak still # exercises the suite. - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -146,7 +146,7 @@ jobs: # Node to 22 which provides it — but keeping the flag while the # raw-SQL race is still unfixed). Drop once follow-up lands. if: github.event_name == 'push' || github.event.pull_request.draft == false - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] needs: [build] continue-on-error: true steps: diff --git a/.github/workflows/claude-agent.yml b/.github/workflows/claude-agent.yml index cd90a2a3..f35fd55a 100644 --- a/.github/workflows/claude-agent.yml +++ b/.github/workflows/claude-agent.yml @@ -36,7 +36,7 @@ concurrency: jobs: claude: name: Run Claude - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] # Gate: must be a trusted actor AND either mention @claude or apply # the `agent` label. The author_association path differs per event # so each branch is explicit. diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index cd15e902..ea8d0fd6 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -37,7 +37,7 @@ jobs: github.event.pull_request.draft == false && github.actor != 'dependabot[bot]' && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR","CONTRIBUTOR"]'), github.event.pull_request.author_association) - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] permissions: contents: read pull-requests: write diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 59c02085..4c875a98 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -29,7 +29,7 @@ permissions: jobs: auto-merge: if: github.actor == 'dependabot[bot]' - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - name: Fetch Dependabot metadata id: meta diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index 38d49a08..cfcdd2d7 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -26,7 +26,7 @@ concurrency: jobs: label: name: Apply labels - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index fdffb4ef..13698870 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -34,7 +34,7 @@ concurrency: jobs: validate: name: Validate - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - name: Check Conventional Commits format id: check diff --git a/.github/workflows/project-orchestrator.yml b/.github/workflows/project-orchestrator.yml index 8c367b28..71415498 100644 --- a/.github/workflows/project-orchestrator.yml +++ b/.github/workflows/project-orchestrator.yml @@ -94,7 +94,7 @@ env: jobs: orchestrate: - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - name: Guard — require PROJECT_BOARD_TOKEN env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0cf675cd..5e53ea26 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ permissions: jobs: release: name: Release - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 5f710b23..1121915f 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -26,7 +26,7 @@ concurrency: jobs: classify: name: Classify and label - runs-on: ubuntu-latest + runs-on: [self-hosted, Linux] # Skip re-triggering on our own label edits. if: github.actor != 'github-actions[bot]' && github.event.issue.pull_request == null steps: From 00095807019f137f4056c39fc339c1b1ea05a403 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Fri, 24 Apr 2026 07:57:50 -0400 Subject: [PATCH 2/8] fix(ci): serialize Test job on self-hosted + deterministic cache test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four runners sharing one VM's disk was thrashing `make coverage`: - `TestDLQStats_EmptyWhenNoStream` / `TestDLQStats_ReturnsCorrectCounts` hit `create stream: context deadline exceeded` inside `NewEmbedded`. The JetStream 5s default API timeout blows when `-race` overhead + `SyncAlways: true` fsyncs compete with three parallel test runs on the same disk. Keeping `-race` and `SyncAlways` — serialize the job instead with a static concurrency group so only one Test runs at a time across all PRs/pushes. Other CI jobs still parallelize. - `TestLocalCache_Overwrite` got `v1` back when it expected `v2`. The 10ms post-Set sleeps weren't enough for Ristretto's async admission goroutine under load. Swap to `c.Wait()` (already used by the sibling `TestLocalCache_ExpiredKey`) so admission is deterministic. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 8 ++++++++ internal/cache/local_test.go | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cf27e52..16f8b158 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,14 @@ jobs: test: name: Test runs-on: [self-hosted, Linux] + # Serialize across PRs/pushes on self-hosted: `make coverage` runs + # with `-race` and the embedded NATS uses `SyncAlways: true`, so + # parallel Test jobs on the shared 4-runner VM thrash disk I/O + # and flake JetStream stream creation + async-cache admission. + # Other jobs still parallelize; only Test queues. + concurrency: + group: ci-test-self-hosted + cancel-in-progress: false steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/internal/cache/local_test.go b/internal/cache/local_test.go index cce96b62..24d986fc 100644 --- a/internal/cache/local_test.go +++ b/internal/cache/local_test.go @@ -68,9 +68,9 @@ func TestLocalCache_Overwrite(t *testing.T) { ctx := context.Background() _ = c.Set(ctx, "key", []byte("v1"), 10*time.Second) - time.Sleep(10 * time.Millisecond) + c.Wait() _ = c.Set(ctx, "key", []byte("v2"), 10*time.Second) - time.Sleep(10 * time.Millisecond) + c.Wait() val, _, err := c.Get(ctx, "key") assert.NoError(t, err) From 5817e2f7dd46ba6497ac849cc83f679ea6cf62fb Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Fri, 24 Apr 2026 08:17:24 -0400 Subject: [PATCH 3/8] fix(ci): serialize go test packages in coverage target `make coverage` used `go test`'s default package parallelism, so even with the Test job serialized at the CI level, 8 packages were still fsyncing embedded NATS in parallel inside a single job. That shared- disk contention was enough to blow JetStream's 5s stream-creation timeout (TestDLQStats_EmptyWhenNoStream), even when no other job on the VM was active. `-p 1` serializes package execution within the coverage run. Keeps `-race` and SyncAlways untouched. Adds ~2-3 min to the Test job on either CI or local runs; use `make test` for fast local iteration. Co-Authored-By: Claude Opus 4.7 (1M context) --- Makefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ae061663..ec6ca313 100644 --- a/Makefile +++ b/Makefile @@ -178,7 +178,12 @@ test-all: ## Unit + integration tests coverage: ## Unit test coverage → tmp/coverage/ and summary @mkdir -p tmp/coverage - @$(GOTESTSUM) --format $(GOTESTSUM_FMT) -- -tags="$(TAGS)" ./internal/... -race -coverprofile=tmp/coverage/coverage.txt -covermode=atomic $(ARGS) + # -p 1 serializes package execution. CI runs this on a shared 4-runner + # VM where packages fsyncing embedded NATS (`SyncAlways: true`) in + # parallel saturates disk I/O and flakes JetStream stream creation. + # Serial execution costs ~2-3 min but eliminates the contention. + # Local runs pay the same cost; use `make test` for fast iteration. + @$(GOTESTSUM) --format $(GOTESTSUM_FMT) -- -tags="$(TAGS)" ./internal/... -p 1 -race -coverprofile=tmp/coverage/coverage.txt -covermode=atomic $(ARGS) @go tool cover -html=tmp/coverage/coverage.txt -o tmp/coverage/coverage.html @echo "$(GREEN)==> Coverage Summary:$(RESET)" @go tool cover -func=tmp/coverage/coverage.txt | tail -n 1 | awk '{print " Total Coverage: $(CYAN)" $$3 "$(RESET)"}' From c6c7b247996f6f4e7deb315c4b0a7932535b05e8 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Fri, 24 Apr 2026 09:26:15 -0400 Subject: [PATCH 4/8] =?UTF-8?q?revert(ci):=20`-p=201`=20in=20coverage=20ta?= =?UTF-8?q?rget=20=E2=80=94=20didn't=20fix=20the=20VM=20flake?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hypothesis was that `go test`'s default package-parallelism was contending on disk inside the Test job. Local reproduction on a dev machine (Apple Silicon, NVMe) showed 4 concurrent copies of `make coverage` all pass in ~18 s each and zero failures — so package parallelism isn't the issue. On the CI VM `TestDLQStats_*` hangs for 150-193 s before timing out even with `-p 1` and the Test job already serialized via `concurrency:`, which means the stall is inside the VM (disk/fsync/zombie-process), not inside `go test`'s scheduler. Backing out `-p 1` to restore the ~3 min of Test time it costs. VM diagnosis next. Co-Authored-By: Claude Opus 4.7 (1M context) --- Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index ec6ca313..ae061663 100644 --- a/Makefile +++ b/Makefile @@ -178,12 +178,7 @@ test-all: ## Unit + integration tests coverage: ## Unit test coverage → tmp/coverage/ and summary @mkdir -p tmp/coverage - # -p 1 serializes package execution. CI runs this on a shared 4-runner - # VM where packages fsyncing embedded NATS (`SyncAlways: true`) in - # parallel saturates disk I/O and flakes JetStream stream creation. - # Serial execution costs ~2-3 min but eliminates the contention. - # Local runs pay the same cost; use `make test` for fast iteration. - @$(GOTESTSUM) --format $(GOTESTSUM_FMT) -- -tags="$(TAGS)" ./internal/... -p 1 -race -coverprofile=tmp/coverage/coverage.txt -covermode=atomic $(ARGS) + @$(GOTESTSUM) --format $(GOTESTSUM_FMT) -- -tags="$(TAGS)" ./internal/... -race -coverprofile=tmp/coverage/coverage.txt -covermode=atomic $(ARGS) @go tool cover -html=tmp/coverage/coverage.txt -o tmp/coverage/coverage.html @echo "$(GREEN)==> Coverage Summary:$(RESET)" @go tool cover -func=tmp/coverage/coverage.txt | tail -n 1 | awk '{print " Total Coverage: $(CYAN)" $$3 "$(RESET)"}' From e5dd7b4f03b46ddc1e079f3579074de1cb02219a Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Fri, 24 Apr 2026 09:26:16 -0400 Subject: [PATCH 5/8] docs(agents): mandate local-first validation before pushing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codify the workflow that surfaced during PR #77's CI-flake debug: validate locally with `make ci` / `make coverage` before pushing, and treat local-pass / CI-fail as an environment mismatch (not a test bug) until proven otherwise. Adds a new MANDATORY "Local-First Validation" section to AGENTS.md covering: the pre-push checklist, the diagnostic order-of-operations when local and CI disagree (reproduce → reproduce-under-load → touch the runner, in that order), and explicit instructions for delegating to subagents. Mirrors a shorter pointer into copilot-instructions.md rule #8. Reason: PR #77's DLQ-test flakes were iterated on via four CI pushes before running the same tests locally — which would have showed they pass in milliseconds, reframing the whole investigation as a VM problem rather than a test problem. The rule is written to stop that pattern recurring with future agents on this repo. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/copilot-instructions.md | 1 + AGENTS.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 78c06efa..4141b9dd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -19,3 +19,4 @@ For detailed project context, architecture, code conventions, and common tasks, 5. **Verify before finishing** — before completing any task, search docs for identifiers you touched (field names, env vars, endpoints, struct names) and fix any stale references. 6. **Every new function must have tests** — use table-driven tests, shared mocks from `internal/testutil/`, and aim for 80%+ coverage. Run `make lint` and `make test` before considering work complete. 7. **Use testutil helpers** — use `MockPublisher`, `MockCache`, `MockDeduplicator`, `MockSubscriber` from `testutil/mocks.go` instead of creating ad-hoc mocks. Use `testutil.MakeJWT()` for auth tests. Use `policy.NewMemoryStore(p)` for policy tests. Use `pipes.NewMemoryStore(queries...)` for pipes tests. +8. **Validate locally before pushing** — run `make ci` (or at minimum `make coverage`) in your sandbox and confirm it passes before opening or updating a PR. The repo runs on a shared self-hosted runner pool with finite throughput and bills AI-reviewer credits per push; a speculative commit to "see what CI says" costs real minutes and dollars. If a test passes locally but flakes on CI, investigate the runner environment before patching the test — see the "Local-First Validation" section in [AGENTS.md](../AGENTS.md) for full guidance. diff --git a/AGENTS.md b/AGENTS.md index 85888622..a4306431 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,6 +112,37 @@ Dev tools (`gotestsum`, `gofumpt`, `goimports`) are pinned in `go.mod` via nativ - **Every new function should have corresponding test cases.** Run `make lint` and `make test` before considering work complete. - **E2E tests via SDK**: The TypeScript SDK is the primary E2E test harness. Tests in `tests/sdk/` exercise the full pipeline (ingest → ClickHouse → query) and simultaneously validate backend behavior and SDK correctness. Use `make test-e2e` to run, or `make test-e2e-dev` for watch mode. Add new E2E scenarios as `tests/sdk/*.test.ts` files using helpers from `tests/sdk/helpers.ts`. +## Local-First Validation (MANDATORY) + +**Validate locally before pushing. Do not use CI as your first feedback loop.** The repo runs on a shared 4-runner self-hosted VM with finite throughput and bills AI-reviewer (Claude, Gemini, Copilot) credits on every push. A speculative "let's see what CI says" commit costs real minutes and real dollars and is visible to the entire team as churn. Every push should represent a change you have locally verified to pass the same gates CI will run. + +### Before every push + +Run the CI-equivalent locally: + +```bash +make ci # Full parity with CI: tidy + fmt + lint + vulncheck + build + tests +make coverage # Matches the CI `Test` job (race detector; -p 1 in CI for package serialization) +``` + +If `make ci` passes, your commit has crossed the same gates CI will run. If it fails, fix it before pushing — don't rely on CI to surface issues that took seconds to catch locally. + +For workflow-only changes where `make ci` isn't relevant, at minimum run `actionlint .github/workflows/*.yml` and read through your YAML diff line-by-line before pushing; CI's own billing makes "push and see" for workflow-file iteration especially wasteful. + +### If local passes but CI fails + +**Treat this as an environment mismatch, not a test bug, until proven otherwise.** Tests that pass on a dev machine in milliseconds but time out on the self-hosted VM point to runner-side problems (I/O pressure, zombie processes, disk contention, shared-VM fsync storms) — not to flaky test code. Investigate the runner before changing tests or production code. Masking environment issues with longer timeouts or retries tends to compound: today's 5s bump becomes tomorrow's 30s bump becomes next week's unbounded wait, and the underlying runner problem keeps slowly degrading. + +Order of operations before patching tests for "CI flakiness": + +1. **Reproduce the reported failure locally first.** `go test -race -run TestFoo ./...` on your machine. If it fails locally, you have a real test bug; fix it with deterministic primitives (use `c.Wait()` not `time.Sleep`, use `require.Eventually` not `time.Sleep` then assert, use channel sync not goroutine scheduling assumptions). +2. **If it passes locally, try to reproduce under load.** Run 4 concurrent copies of `make coverage` to simulate the VM's shared-runner contention. If that still passes, the problem is the VM — not the test. +3. **Only then touch the runner.** SSH in, check `iostat -x 2`, `pgrep -af nats-server`, `df -h`, `du -sh /opt/github/action-runner-*/_work`. Environment fixes (cleanup crons, tmpfs for test temp dirs, slower runner count, faster disk) stay scoped to the runner and don't pollute the codebase. + +### When delegating to another agent + +If you hand work to a subagent or another Claude session, tell them explicitly: *"Run locally first. Do not push to CI until `make ci` passes on your checkout."* Agents default to "commit and let CI run" because it looks like progress; in this repo that default is expensive. Override it at delegation time. + ## Review Response (MANDATORY) **Every review comment on a PR gets a substantive reply, and every conversation gets resolved before merge. This applies equally to human reviewers and AI reviewers (Copilot, Gemini Code Assist, claude-review, future bots). The `main branch protection` ruleset enforces `required_review_thread_resolution: true`, so unresolved threads literally block merge.** From 7d78185add00d7a5f7a10603182d62ff62d09b57 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Mon, 27 Apr 2026 16:35:53 -0400 Subject: [PATCH 6/8] ci: migrate workflows to ARC runners + shared BuildKit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap `runs-on: [self-hosted, Linux]` → `[wave-rf-runners]` across all 10 workflow files. ARC's RunnerSet exposes its own label rather than the auto-applied Linux/self-hosted pair the previous self-hosted VM runners used. ci.yml Test: drop the static `ci-test-self-hosted` concurrency group. That was a workaround for github-vm's shared-zvol fsync contention; ARC's per-pod isolation eliminates the contention, and keeping the group would now globally serialize Test across all PRs. ci.yml E2E: pre-build the wavehouse image via `docker buildx build --load` before running tests. With BUILDX_BUILDER preset on the runner pod, the build routes to the shared remote BuildKit (cache shared across runners and repos) and loads the result back into the DinD sidecar's local daemon. Setting WAVEHOUSE_IMAGE then makes tests/compose.yaml use the loaded image instead of triggering its own `build:` directive — explicit per the header comment in that compose file. release.yml: label change only. Multi-arch is the BuildKit VM's concern with remote builds, and BUILDX_BUILDER is preset on the runner — no setup-qemu / setup-buildx needed at the workflow layer. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/admin-approval.yml | 2 +- .github/workflows/ci.yml | 37 +++++++++++++--------- .github/workflows/claude-agent.yml | 2 +- .github/workflows/claude-review.yml | 2 +- .github/workflows/dependabot-automerge.yml | 2 +- .github/workflows/label.yml | 2 +- .github/workflows/pr-title.yml | 2 +- .github/workflows/project-orchestrator.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/triage.yml | 2 +- 10 files changed, 31 insertions(+), 24 deletions(-) diff --git a/.github/workflows/admin-approval.yml b/.github/workflows/admin-approval.yml index d5d59cb0..7f6ce155 100644 --- a/.github/workflows/admin-approval.yml +++ b/.github/workflows/admin-approval.yml @@ -32,7 +32,7 @@ concurrency: jobs: admin-approval: name: Admin approval - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - name: Check for admin approval env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16f8b158..e8fcf063 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ permissions: jobs: check: name: Check - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -34,7 +34,7 @@ jobs: lint: name: Lint - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -54,15 +54,7 @@ jobs: test: name: Test - runs-on: [self-hosted, Linux] - # Serialize across PRs/pushes on self-hosted: `make coverage` runs - # with `-race` and the embedded NATS uses `SyncAlways: true`, so - # parallel Test jobs on the shared 4-runner VM thrash disk I/O - # and flake JetStream stream creation + async-cache admission. - # Other jobs still parallelize; only Test queues. - concurrency: - group: ci-test-self-hosted - cancel-in-progress: false + runs-on: [wave-rf-runners] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -91,7 +83,7 @@ jobs: build: name: Build - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -104,7 +96,7 @@ jobs: integration: name: Integration Tests - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] needs: [test] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -121,7 +113,7 @@ jobs: # Vitest unit tests for the TypeScript SDK. Fast (~30s) and runs on # every PR — no path filter so a deps bump or workflow tweak still # exercises the suite. - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -154,7 +146,7 @@ jobs: # Node to 22 which provides it — but keeping the flag while the # raw-SQL race is still unfixed). Drop once follow-up lands. if: github.event_name == 'push' || github.event.pull_request.draft == false - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] needs: [build] continue-on-error: true steps: @@ -166,5 +158,20 @@ jobs: cache: "npm" cache-dependency-path: tests/sdk/package-lock.json + # Build the wavehouse image once via the shared remote BuildKit + # and `--load` it into the runner pod's Docker daemon (DinD + # sidecar) so the docker compose stack started by setup.ts uses + # the prebuilt image instead of re-building from source. The + # BUILDX_BUILDER env var is preset on the runner pod, so buildx + # transparently routes to the shared builder + cache. + - name: Build wavehouse image (remote BuildKit, --load) + run: docker buildx build --load -t wavehouse:e2e -f deployments/Dockerfile . + - name: Run E2E tests + env: + # Setting WAVEHOUSE_IMAGE makes tests/compose.yaml use the + # prebuilt image (loaded by the previous step) instead of + # invoking its own `build:` directive. See header comment in + # tests/compose.yaml. + WAVEHOUSE_IMAGE: wavehouse:e2e run: make test-e2e diff --git a/.github/workflows/claude-agent.yml b/.github/workflows/claude-agent.yml index f35fd55a..8583ecf4 100644 --- a/.github/workflows/claude-agent.yml +++ b/.github/workflows/claude-agent.yml @@ -36,7 +36,7 @@ concurrency: jobs: claude: name: Run Claude - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] # Gate: must be a trusted actor AND either mention @claude or apply # the `agent` label. The author_association path differs per event # so each branch is explicit. diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index ea8d0fd6..35d9f573 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -37,7 +37,7 @@ jobs: github.event.pull_request.draft == false && github.actor != 'dependabot[bot]' && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR","CONTRIBUTOR"]'), github.event.pull_request.author_association) - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] permissions: contents: read pull-requests: write diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 4c875a98..7e052b95 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -29,7 +29,7 @@ permissions: jobs: auto-merge: if: github.actor == 'dependabot[bot]' - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - name: Fetch Dependabot metadata id: meta diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index cfcdd2d7..7720437a 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -26,7 +26,7 @@ concurrency: jobs: label: name: Apply labels - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 13698870..b2fcdbda 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -34,7 +34,7 @@ concurrency: jobs: validate: name: Validate - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - name: Check Conventional Commits format id: check diff --git a/.github/workflows/project-orchestrator.yml b/.github/workflows/project-orchestrator.yml index 71415498..bf5415d2 100644 --- a/.github/workflows/project-orchestrator.yml +++ b/.github/workflows/project-orchestrator.yml @@ -94,7 +94,7 @@ env: jobs: orchestrate: - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - name: Guard — require PROJECT_BOARD_TOKEN env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e53ea26..8d76d81d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ permissions: jobs: release: name: Release - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 1121915f..46a126ff 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -26,7 +26,7 @@ concurrency: jobs: classify: name: Classify and label - runs-on: [self-hosted, Linux] + runs-on: [wave-rf-runners] # Skip re-triggering on our own label edits. if: github.actor != 'github-actions[bot]' && github.event.issue.pull_request == null steps: From 97eb39c2b5464d3eb5a2eb26640fd7b10f4f6bc7 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Mon, 27 Apr 2026 20:43:25 -0400 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20address=20Copilot=20R2=20=E2=80=94?= =?UTF-8?q?=20TestLocalCache=5FOverwrite=20+=20actionlint=20wording?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - internal/cache/local_test.go: assert NoError on both `c.Set` calls in TestLocalCache_Overwrite (was discarding the error). A failing Set would have produced a misleading downstream assertion failure rather than the actual cause. - AGENTS.md: reword the actionlint guidance per Copilot's suggestion. The repo doesn't pin or wrap `actionlint` (no make target, not in go.mod tools), so framing it as an optional local-dev tool rather than a mandated step matches reality. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 2 +- internal/cache/local_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a4306431..b40b05b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,7 +127,7 @@ make coverage # Matches the CI `Test` job (race detector; -p 1 in CI for packa If `make ci` passes, your commit has crossed the same gates CI will run. If it fails, fix it before pushing — don't rely on CI to surface issues that took seconds to catch locally. -For workflow-only changes where `make ci` isn't relevant, at minimum run `actionlint .github/workflows/*.yml` and read through your YAML diff line-by-line before pushing; CI's own billing makes "push and see" for workflow-file iteration especially wasteful. +For workflow-only changes where `make ci` isn't relevant, manually read through your YAML diff line-by-line before pushing. If you already have `actionlint` installed locally, also run `actionlint .github/workflows/*.yml`; CI's own billing makes "push and see" for workflow-file iteration especially wasteful. ### If local passes but CI fails diff --git a/internal/cache/local_test.go b/internal/cache/local_test.go index 24d986fc..137a74c5 100644 --- a/internal/cache/local_test.go +++ b/internal/cache/local_test.go @@ -67,9 +67,9 @@ func TestLocalCache_Overwrite(t *testing.T) { defer func() { _ = c.Close() }() ctx := context.Background() - _ = c.Set(ctx, "key", []byte("v1"), 10*time.Second) + require.NoError(t, c.Set(ctx, "key", []byte("v1"), 10*time.Second)) c.Wait() - _ = c.Set(ctx, "key", []byte("v2"), 10*time.Second) + require.NoError(t, c.Set(ctx, "key", []byte("v2"), 10*time.Second)) c.Wait() val, _, err := c.Get(ctx, "key") From 88489e065b766b8af4101aebd3ba2cd21fb09526 Mon Sep 17 00:00:00 2001 From: Eric Andrechek Date: Mon, 27 Apr 2026 20:53:57 -0400 Subject: [PATCH 8/8] fix(ci): trigger CI on `ready_for_review` so e2e runs on draft flip Default `pull_request` event types are `[opened, synchronize, reopened]` and don't include `ready_for_review`. The e2e job is gated on `github.event.pull_request.draft == false`, so when a PR is opened as draft and later flipped ready, the existing CI run's event payload still says draft=true and e2e silently skips. Adding `ready_for_review` to the event filter ensures the full suite (including e2e) runs at the moment the PR becomes review- ready, without needing to push an empty commit to force a fresh trigger. Hit on this exact PR: e2e was missed on commit 97eb39c because the push happened while the PR was still draft, and flipping ready didn't re-fire CI. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8fcf063..b5d08735 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,13 @@ on: push: branches: [main] pull_request: + # Default `pull_request` types are [opened, synchronize, reopened] + # which do NOT include `ready_for_review`. Without it, flipping a + # PR from draft → ready doesn't re-trigger CI, so the e2e job + # (gated on `draft == false`) silently skips on the existing run. + # Adding `ready_for_review` ensures the full suite runs at the + # moment the PR becomes review-ready. + types: [opened, synchronize, reopened, ready_for_review] branches: [main] permissions: