refactor(sidecarapi): extract the sidecar wire contract into its own module - #503
Conversation
PR SummaryMedium Risk Overview Build & CI now treat nested modules explicitly: Guardrails: Reviewed by Cursor Bugbot for commit a9d89c5. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7514425. Configure here.
…module The controller talked to the sidecar through github.com/sei-protocol/seictl, so it depended on the seictl repo for a contract it co-owns. That dependency also made `go mod tidy` fail outright: seictl requires gogo/protobuf v1.3.3, a version only the regen-network fork has, and the replace that redirects it is seictl's, not ours — Go ignores replace directives in dependency modules. sidecarapi is that contract as its own module: the OpenAPI spec, the client generated from it, and the dependency-free wire types. Eight dependencies, no sei-chain, no k8s, and no replace directives of its own (a dependency module's replaces are ignored, so carrying any would be a silent no-op). The root module resolves it through a filesystem `replace`, deliberately, not a tag. A tagged require would need the commit to exist before the commit that adds it, forcing this into two merges; a `go.work` is worse — it promotes a used module's replaces to main-module status, so a local `go build ./...` would succeed where GOWORK=off (Docker, release CI, external consumers) fails. Also here, because each one is load-bearing rather than incidental: - The k8s v0.35.0 replace block is gone, not inherited. Its comment blamed seictl's transitive constraints; that turned out to be true, so with seictl dropped MVS settles on v0.35.1 unaided and controller-runtime v0.23.1 still builds. Re-derived rather than carried forward. - A depguard rule, `contract-stays-light`, denies the chain graph to anything under sidecarapi/ — _test.go files included, which is where this broke before (seictl#238 added such an import to a test and every consumer's `go mod tidy` stopped working). depguard is the conventional mechanism for an import restriction and it already ships in the linter this repo runs; Kubernetes solves the same problem with import-boss. Note the trailing slash on the sidecar/ deny entry: `pkg` is a prefix match, so without it the rule also denies sidecarapi itself. - CI now fans out per module. Go package patterns stop at a nested module boundary, so `go list ./...` in the root never sees sidecarapi/ — a root-only lint and test would have gone green over uncompiled code. Adds a lint matrix, a hygiene job running `go mod tidy -diff` per module, and MODULES-driven make targets. Between them, depguard catches the offending import and tidy-check catches the unresolvable graph it produces. - Both Dockerfiles copy sidecarapi/go.mod before `go mod download`, and .dockerignore re-includes nested module files. `!go.mod` matched the root only, so the replace target was absent from the build context. - openapi.yaml declared no security on /v0/status while the server requires X-Remote-User there. Harmless while nothing read the spec; not harmless once this module is the contract of record, because reconciling the server "down" to it would expose the status snapshot. /v0/healthz gets an explicit empty security block so public-by-design and block-forgotten stay distinguishable. The 27 import sites move group, not just prefix — the path is under goimports' local-prefixes now, so a bare sed would have left a lint-failing tree. Verified: gofmt, goimports, go build, go vet, tidy-check and verify-generated all clean; 14 root and 3 sidecarapi test packages pass; the controller's build closure holds zero sei-chain/cosmos/cometbft/gogo packages. The depguard rule was exercised against a reintroduction of the seictl#238 import and reports it at the file with its reason. Not verified locally: the Docker builds — no daemon available here, so the .dockerignore fix was checked by simulating pattern resolution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7514425 to
d2da03a
Compare
Adding sidecarapi to the lint matrix surfaced 51 findings in code that had
never been linted — seictl carries no golangci-lint config at all. The root
module's `lint (.)` leg hides its own backlog behind only-new-issues, but
every file here is new to this branch, so nothing was filtered.
34 modernize hits were interface{} where any will do. Replaced in the three
hand-written files. sidecar.gen.go keeps its two: it is generated and marked
DO NOT EDIT, and the generated-exclusion already keeps it out of the report.
goconst is excluded for sidecarapi/client instead. What it flags there are the
JSON keys inside the request-params maps — chainId, keyName, fees, gas, title,
initialDeposit, address. Those literals are the wire contract: being able to
read a params map and see the JSON it emits is what makes this client
reviewable against the server's struct tags, and it is why the client/server
key mismatch this module was extracted over was findable at all. Hoisting them
behind constants trades that legibility for a DRY score. The rest of the hits
are test fixtures (1usei, config.toml, an RPC URL).
Verified with golangci-lint against the module: 0 issues. Build, tests,
tidy-check and verify-generated unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Phase 2 of moving the sidecar out of
seictl. This lands the contract module and repoints the controller; the implementation move is Phase 3 (#504).Why
The controller reached the sidecar through
github.com/sei-protocol/seictl, so it depended on the seictl repo for a contract it co-owns. That dependency also madego mod tidyfail outright:gogo/protobuf v1.3.3exists only in theregen-networkfork, and the replace that redirects it is seictl's — Go ignores replace directives in dependency modules.What
sidecarapi/is that contract as its own module: the OpenAPI spec, the client generated from it, and the dependency-free wire types. 8 dependencies, no sei-chain, no k8s, no replace directives (a dependency module's replaces are ignored, so carrying any would be a silent no-op).The root module resolves it through a filesystem
replace, deliberately:requirewould need the commit to exist before the commit that adds it, forcing two merges;go.workis worse — it promotes a used module's replaces to main-module status, sogo build ./...locally would succeed whereGOWORK=off(Docker, release CI, external consumers) fails. Measured.Things worth a second look
The k8s replace block is deleted, not inherited. Its comment blamed seictl's transitive constraints. That was true — with seictl dropped, MVS settles k8s at v0.35.1 unaided and controller-runtime v0.23.1 still builds. Re-derived rather than carried forward.
A
depguardrule replaces what I first wrote as a bespoke test.contract-stays-lightdenies the chain graph to anything undersidecarapi/,_test.gofiles included — which is where this broke before (seictl#238 added such an import to a test). depguard is the conventional mechanism for an import restriction, already ships in the linter this repo runs, and Kubernetes solves the same problem withimport-boss.An earlier revision of this PR had a test that shelled out to
go listinstead. It was worse in three measurable ways: it only saw the host platform whilego mod tidyresolves all of them,go testcaching keyed on the wrong inputs so it returned a cached pass with a real leak present, and its deny-list of heavy names missedgo-ethereumentirely. Gone.One detail for reviewers: note the trailing slash on the
sidecar/deny entry.pkgis a prefix match, so without it the rule also deniessidecarapiitself — I hit that while testing.CI now fans out per module. Go package patterns stop at a nested module boundary, so
go list ./...in the root never seessidecarapi/. A root-only lint and test would have gone green over uncompiled code. Adds a lint matrix, ahygienejob runninggo mod tidy -diffper module, andMODULES-driven make targets. Between them: depguard catches the offending import,tidy-checkcatches the unresolvable graph it produces.openapi.yamldeclared no security on/v0/statuswhile the server requiresX-Remote-Userthere. Harmless while nothing reads the spec; not harmless once this module is the contract of record, since reconciling the server down to the spec would expose the status snapshot./v0/healthzgets an explicit emptysecurity: []so public-by-design and block-forgotten stay distinguishable.The 27 import sites change group, not just prefix. The path is under goimports'
local-prefixesnow, so a baresedwould have left a lint-failing tree.Test plan
gofmt -s,goimports -local,go build ./...,go vet ./...cleanmake tidy-check— both modules tidymake verify-generated— no generated driftgo mod tidysucceeds with no replace for gogoNot verified locally — please confirm in CI: the Docker builds. No daemon was available, so the
.dockerignorefix (!go.modmatched the root only, leaving the replace target out of the build context) was checked by simulating pattern resolution rather than by building. Thedepguardrule was validated with golangci-lint against a copy of the module with itsgodirective lowered, because the pinned linter cannot load ago 1.26.0module in this environment.Follow-ups, not in this PR
zz_generated.deepcopy.gois flagged bygoimportsonmainas well (controller-gen doesn't emit the alias goimports wants). Left alone.🤖 Generated with Claude Code