fix(build): pin sebuf plugin via PATH in make generate#3371
Conversation
Without this, developers who have an older sebuf protoc-gen-openapiv3
binary installed via a package manager (Homebrew ships v0.7.0 at
/opt/homebrew/bin) hit this failure on a fresh `make generate`:
Failure: file ".../AviationService.openapi.yaml" was generated
multiple times: once by plugin "protoc-gen-openapiv3" and again
by plugin "protoc-gen-openapiv3"
Root cause: `buf.gen.yaml` declares three `protoc-gen-openapiv3`
invocations — the default yaml, a `format=json` variant, and a
`bundle_only=true, strategy: all` unified bundle. Sebuf v0.11.x
honors both `format=json` (emits .json extension) and `bundle_only=true`
(skips per-service emission), so the three invocations write distinct
files. Sebuf v0.7.x does NOT honor either option — it silently emits
the same per-service .yaml filenames from all three plugins and buf
rejects the collision.
`Makefile: install-plugins` installs v0.11.1 (SEBUF_VERSION) to
$HOME/go/bin. But the `generate` target doesn't prepend that to PATH,
so `which protoc-gen-openapiv3` resolves to the stale Homebrew binary
for anyone with both installed.
Verified by `go version -m`:
/opt/homebrew/bin/protoc-gen-openapiv3 — mod sebuf v0.7.0
/Users/eliehabib/go/bin/protoc-gen-openapiv3 — mod sebuf v0.11.1
Fix: prepend $$HOME/go/bin to PATH in the `generate` recipe. Matches
what .husky/pre-push:151-153 already does before invoking this target,
so CI and local behavior converge. No sebuf upstream bug.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Greptile SummaryPrepends Confidence Score: 5/5Safe to merge — one-line targeted fix with no logic changes and no risk to generated output or CI. The only finding is a P2 style note about Make echoing unsilenced comment lines; the core PATH fix is correct, consistent with the pre-push hook, and harmless on CI. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["make generate"] --> B["PATH=\$HOME/go/bin:\$PATH buf generate"]
B --> C{Which protoc-gen-openapiv3?}
C -->|"$HOME/go/bin first"| D["sebuf v0.11.1\n(install-plugins)"]
C -->|"rest of PATH"| E["sebuf v0.7.x\n(Homebrew / stale)"]
D --> F["Honors format=json\n+ bundle_only=true"]
E --> G["Ignores format=json\n+ bundle_only=true\n→ duplicate output error"]
F --> H["✅ 3 distinct output files\nNo collision"]
G --> I["❌ buf rejects duplicate\nfile paths"]
Reviews (1): Last reviewed commit: "fix(build): pin sebuf plugin via PATH in..." | Re-trigger Greptile |
| # Prepend $$HOME/go/bin so the Makefile-declared sebuf version | ||
| # ($(SEBUF_VERSION)) installed by `install-plugins` wins over any | ||
| # stale sebuf binary that a package manager (Homebrew, etc.) may | ||
| # have placed earlier on PATH. Without this, `buf generate` can | ||
| # pick up an older sebuf v0.7.x build that ignores `bundle_only=true` | ||
| # / `format=json` and produces duplicate-output errors. This matches | ||
| # what .husky/pre-push already does before invoking this target. |
There was a problem hiding this comment.
Recipe comment lines will be echoed by Make
Lines starting with # inside a Make recipe are passed to the shell (which ignores them) but Make still echoes them before execution. Running make generate will print all five comment lines verbatim to stdout. Prefix with @ to silence them, or move the comment above the recipe block so Make treats it as a Makefile comment instead.
| # Prepend $$HOME/go/bin so the Makefile-declared sebuf version | |
| # ($(SEBUF_VERSION)) installed by `install-plugins` wins over any | |
| # stale sebuf binary that a package manager (Homebrew, etc.) may | |
| # have placed earlier on PATH. Without this, `buf generate` can | |
| # pick up an older sebuf v0.7.x build that ignores `bundle_only=true` | |
| # / `format=json` and produces duplicate-output errors. This matches | |
| # what .husky/pre-push already does before invoking this target. | |
| @# Prepend $$HOME/go/bin so the Makefile-declared sebuf version | |
| @# ($(SEBUF_VERSION)) installed by `install-plugins` wins over any | |
| @# stale sebuf binary that a package manager (Homebrew, etc.) may | |
| @# have placed earlier on PATH. Without this, `buf generate` can | |
| @# pick up an older sebuf v0.7.x build that ignores `bundle_only=true` | |
| @# / `format=json` and produces duplicate-output errors. This matches | |
| @# what .husky/pre-push already does before invoking this target. |
Reviewer (Codex) on PR #3371: the previous patch hardcoded \$HOME/go/bin, which is only the default fallback when GOBIN is unset AND GOPATH defaults to \$HOME/go. On machines with a custom GOBIN or a non-default GOPATH, `go install` targets a different directory — so hardcoding \$HOME/go/bin can force a stale binary from there to win over the freshly-installed SEBUF_VERSION sitting at the actual install location. Fix: resolve the install dir the same way `go install` does: GOBIN first, then GOPATH/bin. Shell expression: `go env GOBIN` returns an empty string (exit 0) when unset, so `||` alone doesn't cascade. Using explicit `[ -n "$gobin" ]` instead. Also dropped the misleading comment that claimed the pre-push hook used the same rule — it still hardcodes \$HOME/go/bin. Called that out in a note, but left the hook alone because its PATH prepend is belt-and-suspenders (only matters for locating `buf` itself; the Makefile's own recipe-level prepend decides plugin resolution). Verified on a machine with empty GOBIN: resolved → /Users/eliehabib/go/bin And \`make generate\` succeeds without manual PATH overrides.
Reviewer (Codex) on commit 6db0b53: the GOBIN-empty fallback used `$(go env GOPATH)/bin`, which silently breaks on setups where GOPATH is a colon-separated list. Example: GOPATH=/p1:/p2 previous code → "/p1:/p2/bin" ^ two PATH entries; neither is the actual install target /p1/bin `go install` writes binaries only into the first GOPATH entry's bin, so the stale-plugin case this PR is trying to fix can still bite. Fix: extract the first entry via `cut -d: -f1`. Matches Go's own behavior in cmd/go/internal/modload/init.go:gobin(), which uses filepath.SplitList + [0]. Verified: - Single-entry GOPATH (this machine) → /Users/eliehabib/go/bin ✓ - Simulated GOPATH=/fake/path1:/fake/path2 → /fake/path1/bin ✓ - make generate succeeds in both cases.
…iew P1/P3)
Codex P1: the previous recipe prepended the Go install dir to PATH
before invoking `buf generate`, which also changed which `buf` binary
ran. On a machine with a stale buf in GOBIN/$HOME/go/bin, the recipe
would silently downgrade buf itself and reintroduce version-skew
failures — the exact class of bug this PR was trying to fix.
Fix: two-stage resolution.
1. `BUF_BIN=$(command -v buf)` resolves buf using the CALLER's PATH
(Homebrew, Go install, distro package — whichever the developer
actually runs day-to-day).
2. Invoke the resolved buf via absolute path ("$BUF_BIN"), with a
PATH whose first entry is the Go install dir. That affects ONLY
plugin lookup inside `buf generate` (protoc-gen-ts-*,
protoc-gen-openapiv3) — not buf itself, which was already resolved.
Adds a loud failure when `buf` is not on PATH:
buf not found on PATH — run: make install-buf
Previously a missing buf would cascade into a confusing error deeper
in the pipeline.
Codex P3: added tests/makefile-generate-plugin-path.test.mjs — scrapes
the generate recipe text and asserts:
- `command -v buf` captures buf before the PATH override
- Missing-buf case fails loudly
- buf is invoked via "$BUF_BIN" (absolute path)
- GOBIN + GOPATH/bin resolution is present
- Install-dir prepend precedes $$PATH (order matters)
- The subshell expression resolves on the current machine
Codex P2 (Windows GOPATH semicolon delimiter) is acknowledged but
not fixed here — this repo does not support Windows dev per CLAUDE.md,
the pre-push hook and CI are Unix-only, and a cross-platform
implementation would require a separate Make detection or a
platform-selected helper script. Documented inline as a known
Unix assumption.
Verified:
- `make generate` clean
- `command -v buf` → /opt/homebrew/bin/buf
- protoc-gen-openapiv3 via plugin-PATH → ~/go/bin/protoc-gen-openapiv3
- New test suite 6/6 pass
- npm run typecheck clean
Codex P2: recipe lines starting with `#` are passed to the shell (which ignores them) but Make still echoes them to stdout before execution. Running `make generate` printed all 34 comment lines verbatim. Noise for developers, and dilutes the signal when the actual error output matters. Fix: prefix every in-recipe `#` comment with `@` so Make suppresses the echo. No semantic change — all comments still read identically in the source. Verified: `make generate` now prints only "Clean complete!", the buf invocation line (silenced with @... would hide the invocation which helps debugging, so leaving that audible), and "Code generation complete!".
…view High)
Codex High: previous recipe computed PLUGIN_DIR from `go env GOBIN` /
`go env GOPATH` without checking that `go` itself was on PATH. When
go is missing:
- `go env GOBIN` fails silently, gobin=""
- `go env GOPATH` fails silently, cut returns ""
- printf '%s/bin' "" yields "/bin"
- PATH becomes "/bin:$PATH" — doesn't override anything
- `buf generate` falls back to whatever stale sebuf plugin is on
PATH, reintroducing the exact duplicate-output failure this PR
was supposed to fix.
Fix (chained in a single shell line so any guard failure aborts):
1. `command -v go` — fail with clear "install Go" message.
2. `command -v buf` — fail with clear "run: make install-buf".
3. Resolve PLUGIN_DIR via GOBIN / GOPATH[0]/bin.
4. `[ -n "$$PLUGIN_DIR" ]` — fail if resolution returned empty
(shouldn't happen after the go-guard, but belt-and-suspenders
against future shell weirdness).
5. `[ -x "$$PLUGIN_DIR/protoc-gen-ts-client" ]` — fail if the
plugin isn't installed, telling the user to run
`make install-plugins`. Catches the case where `go` exists but
the user has never installed sebuf locally.
6. `PATH="$$PLUGIN_DIR:$$PATH" "$$BUF_BIN" generate`.
Verified failure modes:
- go missing → "go not found on PATH — run: ... install-plugins"
- buf missing → "buf not found on PATH — run: make install-buf"
- happy path → clean `Code generation complete!`
Extended tests/makefile-generate-plugin-path.test.mjs with:
- `fails loudly when go is not on PATH`
- `verifies the sebuf plugin binary is actually present before invoking buf`
- Rewrote `PATH override order` to target the new PLUGIN_DIR form.
All 8 tests pass. Typecheck clean.
proto/buf.gen.yaml invokes THREE sebuf binaries: - protoc-gen-ts-client - protoc-gen-ts-server - protoc-gen-openapiv3 (× 3 plugin entries) The previous guard only verified protoc-gen-ts-client was present in the pinned Go install dir. If the other two were missing from that dir (or only partially installed by a prior failed `make install-plugins`), `PATH="$PLUGIN_DIR:$PATH" buf generate` would fall through to whatever stale copy happened to be earlier on the caller's normal PATH — exactly the mixed-sebuf-version failure this PR is meant to eliminate. Fix: iterate every plugin name in a shell `for` loop. Any missing binary aborts with the same `Run: make install-plugins` remediation the previous guard showed. Tests: - Update the existing plugin-presence assertion to require all three binaries by name AND the `[ -x "$PLUGIN_DIR/$p" ]` loop pattern. - Add a cross-reference test that parses proto/buf.gen.yaml, extracts every `local:` plugin, and fails if any declared plugin is missing from the Makefile guard list. This catches future drift without relying on a human to remember the dependency. Closes the PR #3371 High finding that a `ts-server` or `openapiv3` missing from $PLUGIN_DIR would silently re-enable the stale-plugin bug.
The proto-freshness hook's unconditional `export PATH="$HOME/go/bin:$PATH"` defeated the Makefile-side caller-PATH-first invariant: on machines with both a preferred buf (Homebrew, /usr/local, etc.) AND an older `go install buf@<old>` leftover at `~/go/bin/buf`, the prepend placed the stale copy first. `make generate` then resolved buf via `command -v buf` and picked up the shadowed stale binary — recreating the mixed-version failure this PR is meant to eliminate. Fix: 1. Only prepend `$HOME/go/bin` when buf is NOT already on the caller's PATH. Now buf's Homebrew/system copy always wins; `~/go/bin/buf` is a pure fallback. 2. Widen the plugin-presence check to accept either a PATH-resolvable `protoc-gen-ts-client` OR the default go-install location `$HOME/go/bin/protoc-gen-ts-client`. `make generate` now resolves plugins via its own PLUGIN_DIR (GOBIN, then first-entry GOPATH/bin), so requiring them on PATH is too strict. 3. Drop the redundant plugin-only PATH prepend — the Makefile's own plugin-path resolution handles it authoritatively. Tests: add a regression guard that reads the hook, verifies the prepend is gated on `! command -v buf`, and explicitly asserts the OLD buggy pattern is not present. Closes the PR #3371 High finding about the hook's unconditional prepend defeating the Makefile-side caller-PATH-first invariant.
Summary
Developers with an older sebuf `protoc-gen-openapiv3` binary installed via Homebrew or similar hit this on a fresh `make generate`:
```
Failure: file ".../AviationService.openapi.yaml" was generated multiple times:
once by plugin "protoc-gen-openapiv3" and again by plugin "protoc-gen-openapiv3"
```
Not a sebuf bug — a PATH/versioning issue inside this project.
Root cause
`proto/buf.gen.yaml` has 3 `protoc-gen-openapiv3` invocations:
Sebuf v0.11.x correctly honors `format=json` and `bundle_only=true`, so the three invocations write distinct files. Sebuf v0.7.x silently ignores both — all three emit the same per-service `.yaml` filenames, which buf rejects as collisions.
Our Makefile's `install-plugins` installs v0.11.1 to `$HOME/go/bin` (`SEBUF_VERSION := v0.11.1` at `Makefile:14`). But the `generate` target doesn't prepend that to PATH, so anyone with both binaries installed picks up the stale Homebrew one.
Verified via `go version -m`:
Fix
Prepend `$HOME/go/bin` to PATH in the `generate` recipe. Matches `.husky/pre-push:151-153` which already does the same before invoking this target — so CI and local behavior now converge.
Test plan
Follow-up
Unblocks all proto-touching PRs that hit the pre-push proto freshness check on a contributor's dev machine (e.g. the paused PR B for energy disruption `countries[]` denorm).