Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
115aeb4
test(distributed): share suite containers, isolate specs by database
mudler Aug 31, 2026
1974bc1
test(distributed): stop leaking admin pools when database setup fails
mudler Aug 31, 2026
f0fa4a7
test(distributed): wait for the log subscriber instead of racing it
mudler Aug 31, 2026
53639c4
test(distributed): scope the log-subscriber wait and mark the race it…
mudler Aug 31, 2026
3257fc5
ci(distributed): run the distributed e2e suite on PRs
mudler Aug 31, 2026
2d37ee1
ci(distributed): widen the trigger and drop the mid-suite image pull
mudler Aug 31, 2026
c0af66a
test(distributed): add a process-level cluster harness
mudler Aug 31, 2026
a7847b8
test(distributed): make the cluster harness survive a restart
mudler Aug 31, 2026
53cd640
test(distributed): add admin session helper to the cluster harness
mudler Aug 31, 2026
23a2bd5
test(distributed): give each frontend its own data dir and one pinned…
mudler Aug 31, 2026
ce3f360
test(distributed): add kill and restart primitives to the cluster har…
mudler Aug 31, 2026
fc0fce8
test(distributed): correct the failure-primitive comments and guard t…
mudler Aug 31, 2026
1f241fb
test(distributed): prove the cluster harness with a two-replica baseline
mudler Aug 31, 2026
2e4c731
test(distributed): fail rather than skip the cluster specs in CI
mudler Aug 31, 2026
737eb6c
test(distributed): require the cluster binaries by default under CI
mudler Aug 31, 2026
875ff33
test(distributed): cover replica death, rolling restart and racing re…
mudler Aug 31, 2026
58232a3
test(distributed): prove health checking was alive during the failove…
mudler Aug 31, 2026
6620aa0
ci(distributed): run the process-level cluster suite
mudler Aug 31, 2026
73e7ef8
ci(distributed): fail the cluster job when it selects no specs
mudler Aug 31, 2026
b0e01e9
docs: document the distributed and cluster e2e suites
mudler Aug 31, 2026
2c314d6
docs: correct the flake-attempts semantics and fill the review gaps
mudler Aug 31, 2026
c5796d4
test(distributed): correct the claims the e2e comments make
mudler Aug 31, 2026
b13ebea
docs(e2e): correct three claims in the distributed e2e comments
mudler Aug 31, 2026
a305444
feat(cluster): record frontend replicas in a shared instances table
mudler Aug 31, 2026
6ef642f
fix(cluster): stamp liveness on the database clock and refuse undiala…
mudler Aug 31, 2026
6ce3eda
feat(cluster): add the bidirectional splice used by the relay and tunnel
mudler Aug 31, 2026
77e2041
fix(cluster): treat a yamux teardown in Splice as a normal ending
mudler Aug 31, 2026
8ebc241
fix(cluster): report a dead yamux session instead of swallowing it
mudler Aug 31, 2026
e957ff1
feat(cluster): accept authenticated peer links on /api/cluster/peer
mudler Aug 31, 2026
5847f6e
fix(cluster): stop reading a bare EOF as a clean ending
mudler Aug 31, 2026
78958be
fix(cluster): own the peer auth prefix in auth, and pin what the spec…
mudler Aug 31, 2026
e7aac9b
feat(cluster): dial and pool yamux links to peer replicas
mudler Sep 1, 2026
d73730b
refactor(cluster): make the cluster service a leaf and blame the call…
mudler Sep 1, 2026
d928851
feat(cluster): fence worker-connection ownership with a monotonic epoch
mudler Sep 1, 2026
7f1599e
fix(cluster): draw connection epochs from a sequence so none is ever …
mudler Sep 1, 2026
667ce1d
fix(cluster): keep the connection schema migratable on SQLite
mudler Sep 1, 2026
aca383d
feat(cluster): give phase 1 a call site, and prove it against real re…
mudler Sep 1, 2026
e26d556
fix(cluster): hold the guarantees phase 1's comments were claiming
mudler Sep 1, 2026
0d13056
fix(cluster): share one lock order, and correct the phase 1 comments
mudler Sep 1, 2026
5e2938e
feat(cluster): resolve tunnel ownership against a live owner
mudler Sep 1, 2026
e477791
fix(cluster): make the ownership comments say only what holds
mudler Sep 1, 2026
f3ba1f6
feat(cluster): hold worker tunnels, and re-claim them after a sweep
mudler Sep 1, 2026
63ed55a
fix(cluster): make a claim and its record indivisible per node
mudler Sep 1, 2026
62476e5
fix(cluster): keep a session close out of the per-node claim gate
mudler Sep 1, 2026
6e55092
feat(cluster): open the door a worker dials its tunnel through
mudler Sep 1, 2026
48ece89
fix(cluster): harden the worker tunnel, and stop starting a database …
mudler Sep 1, 2026
a816bf9
fix(testutil): stop the shared-database change from disarming two reg…
mudler Sep 1, 2026
5b7d65e
fix(testutil): clear the maintenance timeouts at connection startup
mudler Sep 1, 2026
29a2020
feat(worker): dial, hold and serve the tunnel, on a credential of its…
mudler Sep 1, 2026
5108be2
fix(worker): spec the tunnel's routing table, which was the SSRF boun…
mudler Sep 1, 2026
3b6d32c
fix(worker): make the tunnel credential's node-type gate actually str…
mudler Sep 1, 2026
cce914b
feat(cluster): relay a peer's stream onto a worker tunnel held here
mudler Sep 1, 2026
1036f56
fix(cluster): express the splice policy once, and pin the relay's bud…
mudler Sep 1, 2026
75953d9
feat(cluster): reach every worker through its tunnel, never its address
mudler Sep 1, 2026
a8ac2af
fix(cluster): make "no route" a condition of its own, and let it out …
mudler Sep 1, 2026
b4d8e23
fix(grpc): let the transport answer through the wrappers, not only pa…
mudler Sep 1, 2026
b8d47cc
test(nodes): make the joiner spec deterministic, and gate the wrapper…
mudler Sep 1, 2026
ed9a4b6
docs(grpc): withdraw the lint-cost claim, and make the one nolint tid…
mudler Sep 1, 2026
1cf847f
feat(distributed): stop workers listening, and stop them advertising
mudler Sep 1, 2026
3338d7b
fix(distributed): refuse a worker that cannot tunnel, and say why it …
mudler Sep 1, 2026
0683fb1
test(distributed): prove the worker tunnel end to end, under real inf…
mudler Sep 1, 2026
0dc6ebd
fix(cluster): stop blaming a peer for the caller's own expired deadline
mudler Sep 1, 2026
d26263f
fix(distributed): let a worker's own refusal be evidence about its ba…
mudler Sep 2, 2026
c19ed5a
fix(cluster): stop a late request frame reading as the worker's verdict
mudler Sep 2, 2026
6b712e7
fix(cluster): keep the refusal vocabulary in one table
mudler Sep 2, 2026
49e7744
feat(cluster): record a departure instead of erasing the connection
mudler Sep 2, 2026
d99f7ff
fix(cluster): pin the departure retention to the loop that applies it
mudler Sep 2, 2026
6b4ce58
feat(cluster): answer presence with four values on the database clock
mudler Sep 2, 2026
6e50f06
fix(cluster): pin the derived retention to the sweep that applies it
mudler Sep 2, 2026
49b3d22
feat(worker): serve the control plane over the tunnel, not over NATS
mudler Sep 2, 2026
f9d0d4c
fix(worker): pin the rune cut, answer unload honestly, drop the dead …
mudler Sep 2, 2026
44f12b2
feat(distributed): call the worker's control routes instead of the bus
mudler Sep 2, 2026
5880f4e
test(distributed): pin the no-demotion rule at every call site it is …
mudler Sep 2, 2026
7fc617c
feat(distributed): serve file staging over the worker tunnel
mudler Sep 2, 2026
4c3e0de
test(distributed): pin the unreadable-request rule at all three file …
mudler Sep 2, 2026
93af914
feat(distributed): read worker absence from the database, not from a …
mudler Sep 2, 2026
749cc7a
fix(distributed): let absence be decided by something, at all three c…
mudler Sep 3, 2026
dd9aff5
feat(distributed): take the backend worker off NATS entirely
mudler Sep 3, 2026
6717856
test(distributed): prove phase 3 end to end, under real processes
mudler Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .agents/building-and-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,23 @@ Rules (both gates):
- **Don't weaken the gate:** never hand-lower a baseline or widen a tolerance to turn a red gate green. The ratchet only moves up.
- If a change drops coverage, **add tests** (sort `coverage-summary.json` by line% ascending to find untested code) rather than editing the baseline. When coverage legitimately rises, commit the regenerated baseline (`make test-coverage-baseline` / `test-ui-coverage-baseline`).
- The Go gate is **strict — no tolerance**; `covermode=atomic` keeps it deterministic. The UI gate keeps a small tolerance only because its e2e coverage isn't.

## Distributed-mode test suites

Two suites cover distributed mode (frontend replicas, worker nodes, PostgreSQL, NATS), split by a Ginkgo label:

- `make test-e2e-distributed` runs `Distributed && !VLLMMultinode && !Cluster` over `./tests/e2e/distributed` recursively. Services are wired directly into the test binary. ~240 specs, ~75s.
- `make test-e2e-cluster` runs `Cluster` and spawns real `local-ai` child processes through the `tests/e2e/distributed/cluster` helper package. 6 specs, about 8m30s measured over three consecutive runs (509.1s / 509.8s / 512.3s, so 8m29s to 8m32s).

Both jobs live in `.github/workflows/tests-e2e-distributed.yml`, with `timeout-minutes: 45` each. They trigger on pull requests *and* on every push to `master`; the `paths-ignore` filter (see [.agents/ci-caching.md](ci-caching.md)) sits on the pull-request trigger only, so a master push always runs both. They are advisory only because `master` carries no branch protection, which is a repository setting and not a YAML key: `continue-on-error: true` would flip the run's *conclusion* to success and hide the failure, so it is not used.

- **Containers are suite-scoped, not spec-scoped.** `SetupInfra` used to start a PostgreSQL (~10s) and a NATS (~3.5s) per spec. Across the 213 specs behind it that was roughly **48 minutes of pure container startup per run**, which is why this suite was never in CI. (213 rather than the ~240 above: the larger number is everything the label filter selects, the smaller one is just the specs that call `SetupInfra`.) Containers now start once in `BeforeSuite` and each spec gets its own database via `CREATE DATABASE` (~67ms), which is what the `dbName` argument was always describing. Adding a spec needs no change: call `SetupInfra("some-name")` as before, the name is a prefix and a counter keeps it unique.
- **Consequence for new specs:** the NATS bus is now *shared* within a Ginkgo process, so a wildcard subscriber can observe another spec's traffic. Filter assertions on an identifier your spec owns (a node ID, a job ID) instead of counting everything on `jobs.*.progress`, and verify the spec with `--randomize-all`.
- **`BeforeSuite`, not `SynchronizedBeforeSuite`.** Under `ginkgo -p` each process then gets its own container pair, keeping NATS subjects isolated per process. A single shared NATS across parallel processes would let specs on different processes see each other's messages on the same subject.
- **The label split.** The 8 argument-validation specs under `tests/e2e/distributed/cluster/` carry `Label("Distributed")` only, on purpose: they need no binary, no PostgreSQL and no NATS, so they belong in the fast job. That is why `test-e2e-distributed` keeps `-r` (it must reach the subpackage) and `test-e2e-cluster` deliberately does **not** (the subpackage is out of its scope).
- **`--fail-on-empty` is load-bearing on both targets.** Ginkgo exits 0 when a label filter selects nothing, so without it a refactor that renames or drops `Label("Cluster")` leaves the target reporting "Test Suite Passed" having started no cluster at all. `LOCALAI_E2E_REQUIRE_BINARIES` does not cover this case: it only fires inside a spec that is actually running.
- **The binary gate.** `localAIBinary()` and `mockBackendBinary()` **fail** rather than skip when `CI` is set, or when `LOCALAI_E2E_REQUIRE_BINARIES` is truthy; `LOCALAI_E2E_REQUIRE_BINARIES=0` (also `off`, `no`, `n`, `disabled`, and anything `strconv.ParseBool` reads as false) forces skipping even under CI. **Any value that parses as neither reads as ON**, not off: setting the variable to something meaningless means someone meant to turn the gate on, and reading it as false would quietly restore the silent skip the flag exists to remove. The whole polarity is deliberate, because in CI a skipped cluster spec is indistinguishable from a passing one: Ginkgo exits 0 on skips. Locally a missing binary still just skips, since `CI` is unset in an ordinary shell.
- **Flake budget: no retries at all.** `--flake-attempts` is *total attempts*, not retries (ginkgo v2.29.0 `internal/group.go` sets `maxAttempts = FlakeAttempts` and loops `attempt < maxAttempts`; the flag's own usage string reads "0 - failed tests are not retried"). `DISTRIBUTED_TEST_FLAKES` defaults to **1**, so each spec runs once and a failure is a failure, and `test-e2e-cluster` pins `--flake-attempts 1` outright rather than reading the variable. The repo-wide `TEST_FLAKES=5` means up to five attempts, so up to four retries. These suites exist to surface nondeterminism, and a retry converts exactly that signal into a green run. Raise it locally when bisecting something unrelated, not in the Makefile.
- **Coverage:** `tests/e2e/distributed` is excluded from the coverage roots (`COVERAGE_E2E_ROOTS = ./tests/e2e`, run non-recursively), and so is the `cluster` helper package beneath it. Neither suite moves the baseline, so production code that these suites are the only cover for reads as **uncovered**. Unit tests for such code belong under `./core/...` with `testutil.SetupTestDB()`.
- **The cluster job builds against a stubbed React UI.** `core/http/react-ui/dist` is gitignored and built by Node, so the workflow writes a one-line `index.html` there to satisfy the `//go:embed react-ui/dist/*` in `core/http/app.go` and skips a full Node and Vite install. That holds only while the suite drives the HTTP API and never the UI, which has its own e2e suite. A spec that ever asserts on a UI asset would pass locally, where a real `dist/` exists, and be served the stub in CI: if you write one, the stub step has to go and the real build come back.
- **Do not shorten the cluster suite's waits.** Three of its six specs sit at ~167s each because they wait out a 60s staleness threshold plus a 15s health-check tick. That wait is what stops the assertions from passing before the system could have reacted, which was a real false green earlier on. If the job has to get faster, the levers are CI concurrency or making the thresholds configurable, not shorter waits.
4 changes: 2 additions & 2 deletions .agents/ci-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ This is worth more than it looks. Measured over the week to 2026-07-30, **97% of

The volume is real: 13 gallery-only PRs merged that week with 10 open at once, and 78 of the 137 PRs opened were bot-generated.

`paths-ignore` on the PR trigger of `image-pr.yml` (7 jobs), `build-test.yaml` (3), `lint.yml` (2) and `tests-e2e.yml` (1) drops 13 of those 20. The excluded set:
`paths-ignore` on the PR trigger of `image-pr.yml` (7 jobs), `build-test.yaml` (3), `lint.yml` (2) and `tests-e2e.yml` (1) drops 13 of those 20, measured before `tests-e2e-distributed.yml` (2 jobs) landed. That workflow carries the same exclusion set for the same reason: its dependency graph is 99 packages, so an allowlist of paths would silently stop guarding the moment code moved, while a diff confined to the paths below provably cannot reach it. The excluded set:

| Path | Why no image or Go build can see it |
|---|---|
Expand Down Expand Up @@ -192,7 +192,7 @@ What still runs, and why it has to:
Two properties this relies on:

- `paths-ignore` skips a run only when **every** changed file matches, so a PR touching the gallery *and* Go code still runs everything. That is what makes the exclusion safe rather than a hole.
- `master` carries no branch protection and no rulesets, so a skipped workflow reports no status and nothing waits on it. If required status checks are ever introduced, these four entries must be excluded from the required set or PRs will hang on "Expected — Waiting for status to be reported".
- `master` carries no branch protection and no rulesets, so a skipped workflow reports no status and nothing waits on it. If required status checks are ever introduced, these five entries must be excluded from the required set or PRs will hang on "Expected — Waiting for status to be reported".

### `image.yml` on master push is gated too, by a job rather than a path filter

Expand Down
204 changes: 204 additions & 0 deletions .github/workflows/tests-e2e-distributed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
---
name: 'E2E Distributed Tests'

on:
pull_request:
# The suite's dependency graph is 99 packages, so an allowlist of paths
# silently stops guarding the moment code moves. At ~75s the job is cheap
# enough to run unless the diff is confined to paths it provably cannot
# reach. See .agents/ci-caching.md.
paths-ignore:
- 'gallery/**'
- 'docs/**'
- 'examples/**'
- '**/*.md'
push:
branches:
- master

concurrency:
group: ci-tests-e2e-distributed-${{ github.event.pull_request.number || github.sha }}-${{ github.repository }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
tests-e2e-distributed:
runs-on: ubuntu-latest
# Advisory because it is deliberately not in branch protection, so a failure
# is a visible red X rather than a blocked merge. Promoting it to a required
# check is a repository-settings change, to be made once it has a track
# record; a heavy suite made required on day one gets disabled instead of
# fixed.
timeout-minutes: 45
steps:
- name: Clone
uses: actions/checkout@v7
with:
submodules: true
- name: Configure apt mirror on runner
uses: ./.github/actions/configure-apt-mirror
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.26.0'
cache: false
- name: Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libopus-dev
- name: Proto Dependencies
run: |
curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v26.1/protoc-26.1-linux-x86_64.zip -o protoc.zip && \
unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
rm protoc.zip
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
PATH="$PATH:$HOME/go/bin" make protogen-go
- name: Pre-pull test images
# Pulling here rather than inside the suite keeps container-start timing
# out of the spec timeouts and makes a registry outage read as a
# setup failure instead of a test failure. These two are the only images
# the suite needs once the testcontainers reaper is disabled below.
run: |
docker pull postgres:16-alpine
docker pull nats:2-alpine
- name: Distributed E2E
# TESTCONTAINERS_RYUK_DISABLED keeps the pre-pull above meaningful. The
# reaper exists to clean up leaked containers on a long-lived host, but
# this runner is ephemeral and every container dies with the VM. Leaving
# it enabled would pull a third, unpinned image (testcontainers/ryuk)
# from Docker Hub mid-suite: exactly the registry dependency the
# pre-pull step exists to remove.
env:
TESTCONTAINERS_RYUK_DISABLED: "true"
run: |
PATH="$PATH:$HOME/go/bin" make test-e2e-distributed
- name: Setup tmate session if tests fail
if: ${{ failure() }}
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
connect-timeout-seconds: 180
limit-access-to-actor: true

tests-e2e-cluster:
runs-on: ubuntu-latest
# Advisory for the same reason as the job above: master has no branch
# protection, so a failure here is a visible red X rather than a blocked
# merge. That is a repository-settings property, not a YAML key. The key
# that looks like it says "advisory" instead flips the run's conclusion to
# success, which hides the failure rather than flagging it, so it appears in
# none of this repo's workflows and must not be added here.
#
# Separate job from tests-e2e-distributed so the fast in-process suite is
# not held behind a Go build of local-ai. Serial on purpose: each Ginkgo
# process would get its own PostgreSQL and NATS container and each spec
# spawns two or three local-ai children, so --procs on an unmeasured runner
# is a change to make with numbers, not by default.
#
# The two timeouts bound different things and are not alternatives. Ginkgo's
# --timeout=20m bounds the SUITE only; this job timeout must additionally
# cover setup, which here is the larger and more variable half: submodule
# checkout, apt, protoc plus two go installs plus protogen-go, a cold-cache
# module download (cache: false), a full go build of ./cmd/local-ai, and a
# separate ginkgo test compile. That build alone is ~316s of CPU, so on a
# 4-vCPU runner setup is realistically 8-12 minutes.
#
# 45 minutes therefore, matching the sibling job. A tighter number does not
# make a hang fail faster, it just moves the kill from Ginkgo, which prints
# which spec hung, to the runner, which prints nothing: a red job with no
# evidence, which is how a suite gets disabled rather than fixed.
#
# The suite itself is about 8m30s over three consecutive runs (509.1s /
# 509.8s / 512.3s, so 8m29s to 8m32s) on a developer box, and will be slower
# here. Three specs sit at ~167s each because they wait out a 60s staleness
# threshold plus a 15s health-check tick (HealthCheckInterval, in
# core/config/distributed_config.go; core/services/nodes/health.go runs the
# ticker on the unexported checkInterval, not one of the reconcilers). Do
# not shorten those windows to make this job faster: the wait is what stops
# the assertions from passing before the system could have reacted, which
# was a real false green earlier on.
timeout-minutes: 45
steps:
- name: Clone
uses: actions/checkout@v7
with:
submodules: true
- name: Configure apt mirror on runner
uses: ./.github/actions/configure-apt-mirror
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.26.0'
cache: false
- name: Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libopus-dev
- name: Proto Dependencies
run: |
curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v26.1/protoc-26.1-linux-x86_64.zip -o protoc.zip && \
unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
rm protoc.zip
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
PATH="$PATH:$HOME/go/bin" make protogen-go
- name: Stub the embedded React UI
# core/http/react-ui/dist is gitignored and built by Node, but this
# suite drives the HTTP API and never the UI, which has its own e2e
# suite. A single index.html satisfies the //go:embed react-ui/dist/*
# in core/http/app.go, so the job skips a full Node and Vite install.
# If a cluster spec ever asserts on a UI asset, this step must go and
# the real build come back: a developer box has a real dist/, so such a
# spec would pass locally and fail only here, or worse be served the
# stub and pass in both places.
run: |
mkdir -p core/http/react-ui/dist
printf '<!doctype html><title>stub</title>\n' > core/http/react-ui/dist/index.html
- name: Build local-ai
# Not `make build`: that target pulls in the React UI build. The specs
# exec this binary directly via LOCALAI_E2E_BINARY.
run: |
PATH="$PATH:$HOME/go/bin" go build -o local-ai ./cmd/local-ai
- name: Pre-pull test images
# Same reasoning as the job above: pulling here keeps container-start
# timing out of the spec timeouts and makes a registry outage read as a
# setup failure rather than a test failure.
run: |
docker pull postgres:16-alpine
docker pull nats:2-alpine
- name: Cluster E2E
env:
LOCALAI_E2E_BINARY: ${{ github.workspace }}/local-ai
# Must live under the workspace so the upload step below can reach it.
# The harness defaults to GinkgoT().TempDir(), which lands under
# TMPDIR and would leave the artifact glob matching nothing.
LOCALAI_E2E_LOG_DIR: ${{ github.workspace }}/cluster-logs
# Belt and braces: the harness already fails rather than skips when CI
# is set, and GitHub Actions always sets CI. Stating it here means a
# future edit to that default cannot silently turn this job into one
# that passes without ever starting a cluster, since a skipped cluster
# spec is indistinguishable from a passing one.
LOCALAI_E2E_REQUIRE_BINARIES: "true"
# See the job above: the runner is ephemeral, so the reaper buys
# nothing and would pull a third, unpinned Docker Hub image mid-suite.
TESTCONTAINERS_RYUK_DISABLED: "true"
run: |
PATH="$PATH:$HOME/go/bin" make test-e2e-cluster
- name: Upload process logs
# The per-process logs are the only way to read a cluster failure: the
# Ginkgo output says which assertion failed, not what the four child
# processes were doing. Without this a red job is undebuggable.
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: cluster-process-logs
path: cluster-logs/**/*.log
if-no-files-found: ignore
retention-days: 7
- name: Setup tmate session if tests fail
if: ${{ failure() }}
uses: mxschmitt/action-tmate@v3.23
with:
detached: true
connect-timeout-seconds: 180
limit-access-to-actor: true
22 changes: 22 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@ linters:
- staticcheck
enable:
- forbidigo
# gocritic is enabled for ONE checker: ruleguard, which runs the rules in
# hack/lint/. Every other gocritic check is off (disable-all below), so
# this adds no style noise; it is here purely as the gate that catches a
# gRPC backend wrapper written without Unwrap. See
# hack/lint/backend_wrappers.go for why that cannot be a compile-time
# assertion.
- gocritic
settings:
gocritic:
disable-all: true
enabled-checks:
- ruleguard
settings:
ruleguard:
failOn: all
rules: '${base-path}/hack/lint/backend_wrappers.go'
forbidigo:
forbid:
- pattern: '^t\.Errorf$'
Expand Down Expand Up @@ -126,3 +141,10 @@ linters:
- path: ^backend/go/whisper/sources/
text: 'http\.(DefaultClient|Get|Post|PostForm|Head)'
linters: [forbidigo]
# Test doubles embed grpc.Backend to inherit the interface's method set
# over a NIL value; they decorate nothing, hold no inner client, and have
# no transport answer to forward. The rule targets production wrappers,
# which is where swallowing that answer deletes replica rows.
# gocritic here is only the backend-wrapper ruleguard rule.
- path: _test\.go$
linters: [gocritic]
Loading