Skip to content

fix(build): pin sebuf plugin via PATH in make generate#3371

Merged
koala73 merged 8 commits into
mainfrom
fix/makefile-generate-path-prefix
Apr 24, 2026
Merged

fix(build): pin sebuf plugin via PATH in make generate#3371
koala73 merged 8 commits into
mainfrom
fix/makefile-generate-path-prefix

Conversation

@koala73

@koala73 koala73 commented Apr 24, 2026

Copy link
Copy Markdown
Owner

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:

  1. default → per-service `.yaml`
  2. `format=json` → per-service `.json`
  3. `bundle=true, bundle_only=true, strategy: all` → single `worldmonitor.openapi.yaml`

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`:

  • `/opt/homebrew/bin/protoc-gen-openapiv3` → `mod sebuf v0.7.0`
  • `$HOME/go/bin/protoc-gen-openapiv3` → `mod sebuf v0.11.1`

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

  • `make generate` — clean run, no collisions
  • No changes to generated output vs main (comparing this branch vs origin/main `src/generated/` + `docs/api/`)
  • Reviewer sanity check: on a fresh clone with Homebrew sebuf pre-installed, `make generate` should succeed instead of failing

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).

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.
@vercel

vercel Bot commented Apr 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview Apr 24, 2026 2:58pm

Request Review

@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Prepends $HOME/go/bin to PATH in the generate Make recipe so the Makefile-pinned sebuf v0.11.1 (installed by make install-plugins) always wins over any older binary on the system PATH (e.g. a Homebrew-managed v0.7.x). The fix mirrors the identical guard already present in .husky/pre-push and is a no-op on CI where actions/setup-go already places ~/go/bin on PATH.

Confidence Score: 5/5

Safe 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

Filename Overview
Makefile Prepends $HOME/go/bin to PATH in the generate recipe so the pinned sebuf v0.11.1 wins over any stale Homebrew binary; consistent with .husky/pre-push and benign on CI where setup-go already adds GOPATH/bin.

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"]
Loading

Reviews (1): Last reviewed commit: "fix(build): pin sebuf plugin via PATH in..." | Re-trigger Greptile

Comment thread Makefile Outdated
Comment on lines +57 to +63
# 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
# 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.
koala73 added 5 commits April 24, 2026 17:15
…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.
@koala73
koala73 merged commit a04c53f into main Apr 24, 2026
11 checks passed
@koala73
koala73 deleted the fix/makefile-generate-path-prefix branch April 24, 2026 15:01
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.

1 participant