Skip to content

feat: add xcaddy-style custom binary builder#141

Open
gaarutyunov wants to merge 4 commits into
mainfrom
claude/issue-126-20260417-2050
Open

feat: add xcaddy-style custom binary builder#141
gaarutyunov wants to merge 4 commits into
mainfrom
claude/issue-126-20260417-2050

Conversation

@gaarutyunov

@gaarutyunov gaarutyunov commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Adds pkg/builder and cmd/builder — a tool that generates and compiles custom mcp-anything binaries containing only the registry-implementing packages the user selects (tree-shaking).

Mirrors xcaddy's approach: creates a temp Go module with a replace directive pointing to the local module, copies go.sum for offline transitive-dep verification, renders a target-specific main.go from a template, then runs go build.

Supports targets: proxy, operator, caddy, kong

Closes #126

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a CLI builder to produce standalone Proxy, Operator, Caddy, and Kong binaries with selectable packages, custom outputs, and embedded build metadata.
    • New Docker-based build flow to produce custom Caddy and Kong images using the builder.
  • Tests

    • New end-to-end suites validating the Caddy builder and Kong/JS runtime scenarios.
  • Chores

    • Build automation and CI updated to build and publish the new builder and include it in checks.

Adds pkg/builder and cmd/builder — a tool that generates and compiles
custom mcp-anything binaries containing only the registry-implementing
packages the user selects (tree-shaking).

Mirrors xcaddy's approach: creates a temp Go module with a replace
directive pointing to the local module, copies go.sum for offline
transitive-dep verification, renders a target-specific main.go from
a template, then runs go build.

Targets:
- proxy    — standalone mcp-anything proxy with custom component set
- operator — Kubernetes operator (component list reserved for future
             selective CRD generation)
- caddy    — Caddy binary with mcp-anything module + extra backends
- kong     — Kong Go plugin wrapping mcp-anything (downloads go-pdk)

Usage:
  mcp-anything-builder \
    --target=proxy \
    --package=github.com/gaarutyunov/mcp-anything/pkg/cache/redis \
    --package=github.com/gaarutyunov/mcp-anything/pkg/auth/inbound/jwt \
    --output=bin/my-proxy

Closes #126

Co-authored-by: German Arutyunov <gaarutyunov@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds an xcaddy-style builder CLI and library that generate and compile custom binaries (proxy, operator, Caddy plugin, Kong plugin). Adds Makefile target and Dockerfiles to build via the builder, unit tests for the builder package, e2e tests exercising built binaries in Kubernetes, and CI steps to build/publish the Caddy builder image.

Changes

Cohort / File(s) Summary
Build orchestration
Makefile, Dockerfile.caddy-builder, Dockerfile.kong-builder
Adds BUILDER_BINARY := bin/builder and a build-builder phony target; new multi-stage Dockerfiles that compile the builder CLI and use it to produce Caddy/Kong artifacts.
CLI entrypoint
cmd/builder/main.go
New mcp-anything-builder CLI: flags (--target, repeatable --package, --output, --module-dir, --ldflags), build metadata vars, signal handling, constructs builder.Config and invokes the builder.
Builder implementation
pkg/builder/builder.go, pkg/builder/targets.go
New builder package with exported Config, New, Builder.Build; creates temp module dirs, writes go.mod and per-target main.go templates, optionally runs go mod download, and runs go build with controlled env. Registers four targets (proxy, operator, caddy, kong) and target-specific templates/requirements.
Unit tests
pkg/builder/builder_test.go
Adds tests for New validation/defaults, go.mod and main.go generation, go.sum copying, and registered targets completeness.
E2E tests
tests/e2e/caddy_builder_e2e_test.go, tests/e2e/kong_js_tools_e2e_test.go
Adds two e2e-tagged test suites that deploy WireMock and Caddy/Kong workloads in Kubernetes, create ConfigMaps, port-forward, exercise MCP endpoints, and validate tool discovery and execution.
CI
.github/workflows/ci.yml
Adds CADDY_BUILDER_IMAGE env and build-caddy-builder-image job; updates e2e-tests job to depend on it and includes new e2e matrix entries.

Sequence Diagram

sequenceDiagram
    participant User as CLI User
    participant CLI as cmd/builder
    participant Builder as pkg/builder.Builder
    participant TempDir as Temp Dir
    participant GoTool as Go tool (mod/download & build)

    User->>CLI: run mcp-anything-builder with flags
    CLI->>Builder: New(Config) / Build(ctx)
    Builder->>TempDir: create temp dir, write go.mod and main.go
    Builder->>TempDir: copy go.sum (if present)
    Builder->>GoTool: run `go mod download` (if needed)
    Builder->>GoTool: run `go build -o <output>` (with LDFlags, env overrides)
    GoTool-->>Builder: binary produced at output path
    Builder->>TempDir: cleanup temp dir
    Builder-->>CLI: return success / error
    CLI-->>User: exit (0 or 1)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop through modules, templates in paw,

temp dirs hum softly, go.mod sings the law.
LDFlags glint, imports gather like spring,
builder crafts binaries and makes my heart sing.
Cheers — a new binary, springtime hopping!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add xcaddy-style custom binary builder' clearly and concisely describes the main change: introducing a builder tool that creates custom binaries in an xcaddy-like pattern.
Linked Issues check ✅ Passed The PR successfully implements all core requirements from issue #126: an xcaddy-like builder supporting proxy, operator, caddy, and kong targets; custom binary generation with selected packages; and comprehensive E2E tests for Caddy and Kong use cases.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the builder implementation objectives: new builder package, CLI entry point, Dockerfiles for building artifacts, E2E tests validating practical scenarios, and CI configuration to support the new workflow.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-126-20260417-2050

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Makefile (1)

57-57: ⚠️ Potential issue | 🟡 Minor

check target doesn't include build-builder.

The AI summary states that check's dependency chain was updated to include build-builder, but line 57 still reads check: lint vet test build build-operator treeshake. The new builder binary is therefore not validated by make check, and build-builder is declared in .PHONY but never exercised by the default verification flow.

Proposed fix
-check: lint vet test build build-operator treeshake
+check: lint vet test build build-operator build-builder treeshake
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` at line 57, The Makefile's check target is missing the new
build-builder prerequisite so the builder binary isn't validated by make check;
update the check target definition (the line starting with "check:") to include
build-builder among its prerequisites (e.g., add build-builder alongside lint
vet test build build-operator treeshake) so the new builder is exercised by the
default verification flow; verify .PHONY already lists build-builder and adjust
if necessary.
🧹 Nitpick comments (4)
pkg/builder/builder.go (2)

213-223: Prefer GOSUMDB=off over GONOSUMDB=*, and use strings.HasPrefix.

GONOSUMDB takes a comma-separated glob list of module-path prefixes (inherited from / analogous to GOPRIVATE); passing * is undocumented and relies on glob semantics that may or may not match every module path depending on the Go toolchain version. The canonical way to fully disable checksum-database lookups is GOSUMDB=off. Since you also copy go.sum into the temp module for offline verification, this is the intended behavior.

Separately, the manual length-8 slice compare is brittle; use strings.HasPrefix(e, "GOFLAGS=").

Proposed fix
-	env := make([]string, 0, len(os.Environ())+2)
-	for _, e := range os.Environ() {
-		if len(e) >= 8 && e[:8] == "GOFLAGS=" {
-			continue
-		}
-		env = append(env, e)
-	}
-	env = append(env, "GONOSUMDB=*", "GOFLAGS=-mod=mod")
+	env := make([]string, 0, len(os.Environ())+2)
+	for _, e := range os.Environ() {
+		if strings.HasPrefix(e, "GOFLAGS=") {
+			continue
+		}
+		env = append(env, e)
+	}
+	env = append(env, "GOSUMDB=off", "GOFLAGS=-mod=mod")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/builder.go` around lines 213 - 223, Replace the brittle manual
prefix check and unset of GOFLAGS with strings.HasPrefix when filtering
os.Environ() and change the sum DB disable from the undocumented "GONOSUMDB=*"
to the canonical "GOSUMDB=off"; specifically update the loop that builds env
(used to set cmd.Env) to use strings.HasPrefix(e, "GOFLAGS=") and append
"GOSUMDB=off" and "GOFLAGS=-mod=mod" instead of "GONOSUMDB=*" so checksum
lookups are reliably disabled.

148-148: Hardcoded go 1.24 in the generated go.mod may drift from the host module.

If the parent repo's go.mod ever declares a newer directive (e.g. go 1.25), the temp module will require go 1.24 while transitively pulling in packages that may use features/toolchain directives beyond that, leading to confusing build failures. Consider reading the go directive from <ModuleDir>/go.mod (via golang.org/x/mod/modfile or a small regex) and reusing it, or at minimum centralize this constant next to pseudoVer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/builder.go` at line 148, The generated go.mod in builder.go
hardcodes "go 1.24"; update the code that writes the header (the fmt.Fprintf
call producing "module mcp-anything-custom\n\ngo 1.24\n\nrequire (\n") to read
the host module's "go" directive and reuse it (use
golang.org/x/mod/modfile.Parse or a small regex to read the go directive from
the parent module's go.mod), falling back to a single centralized constant (near
pseudoVer) if parsing fails; ensure you reference this computed goVersion
variable when formatting the go.mod instead of the literal "1.24".
pkg/builder/targets.go (1)

334-334: Use errors.Is(err, context.Canceled) instead of direct comparison.

startErr != context.Canceled misses wrapped cancellation errors (e.g. fmt.Errorf("serving: %w", context.Canceled)), which is how the rest of the codebase surfaces errors per the repo's guideline to wrap with %w. Matches the proxy template at line 51 which already uses errors.Is.

Proposed fix
-		if startErr := proxy.Start(ctx); startErr != nil && startErr != context.Canceled {
+		if startErr := proxy.Start(ctx); startErr != nil && !errors.Is(startErr, context.Canceled) {

(also add "errors" to the template's import block)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/targets.go` at line 334, Replace the direct comparison against
context.Canceled in the proxy start error check with errors.Is to correctly
handle wrapped cancellation errors: change the condition that checks startErr
from "startErr != context.Canceled" to use errors.Is(startErr, context.Canceled)
(i.e., treat the error as ignorable when errors.Is(startErr, context.Canceled)
is true) and ensure the "errors" package is imported; this change applies to the
proxy.Start(ctx) call and its startErr handling in targets.go.
pkg/builder/builder_test.go (1)

69-116: Consider using t.Helper() + errcheck-safe New in helpers.

Minor: b, _ := New(...) in TestWriteGoMod_*, TestWriteMain_*, and TestCopyGoSum_* silently ignores errors; if New later starts rejecting something (e.g. ModuleDir must exist), these tests would segfault on b.writeGoMod rather than fail cleanly. Using if err != nil { t.Fatalf(...) } everywhere makes failures diagnosable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/builder_test.go` around lines 69 - 116, Replace silent New calls
in TestWriteGoMod_noExtraRequire and TestWriteGoMod_extraRequire with an
err-checked helper: create a small helper (e.g. func mustNewBuilder(t
*testing.T, cfg Config) *Builder) that calls t.Helper(), invokes New(cfg), fails
the test with t.Fatalf if err != nil, and returns the builder; then use
mustNewBuilder(t, Config{...}) instead of b, _ := New(...). Also update other
tests noted (TestWriteMain_*, TestCopyGoSum_*) to use the same mustNewBuilder
helper so New errors are surfaced instead of causing panics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/builder/targets.go`:
- Around line 299-339: startProxy has a TOCTOU on port reservation and
double-starts the server; fix by changing it to hand an open net.Listener into
the proxy (avoid reserve/close using net.Listen and pass that listener to
mcpanything.New or a new NewWithListener API) instead of setting cfg.Server.Addr
from ln.Addr(), remove the duplicate startup call so only one of
proxy.StartBackground(ctx) or proxy.Start(ctx) is invoked (pick the background
helper and delete the extra goroutine), and block until the server is actually
ready before returning (implement a short readiness poll using net.DialTimeout
to the listener address or use a StartBackground-ready signal if available) so
startProxy reliably returns a bound, accepting address.
- Around line 90-96: The init() currently uses utilruntime.Must which panics on
failure; instead implement an initScheme(s *runtime.Scheme) error that performs
the calls to clientgoscheme.AddToScheme(scheme), corev1.AddToScheme,
appsv1.AddToScheme, v1alpha1.AddToScheme and gatewayv1.Install and returns any
error encountered (do not call utilruntime.Must), then remove the panic-raising
init() body and call initScheme(scheme) from cmd/operator/main.go (or wherever
process startup occurs), logging the error and exiting with non-zero status on
failure; reference the symbols utilruntime.Must, init(),
initScheme(*runtime.Scheme), scheme, and main when making the change.
- Around line 277-295: In Access (func (c *Config) Access) replace the current
approach of setting "X-Forwarded-Host" on kong.ServiceRequest with a proper
upstream rewrite: parse proxyAddr using net.SplitHostPort to get host and port,
then call kong.Service.SetTarget(host, port) to direct Kong to the embedded
proxy (handle SplitHostPort and conversion errors and call
kong.Response.SetStatus on failure similar to the existing error handling); keep
existing proxy startup logic (startProxy, startOnce, proxyErr) and log errors
via slog as done now.

---

Outside diff comments:
In `@Makefile`:
- Line 57: The Makefile's check target is missing the new build-builder
prerequisite so the builder binary isn't validated by make check; update the
check target definition (the line starting with "check:") to include
build-builder among its prerequisites (e.g., add build-builder alongside lint
vet test build build-operator treeshake) so the new builder is exercised by the
default verification flow; verify .PHONY already lists build-builder and adjust
if necessary.

---

Nitpick comments:
In `@pkg/builder/builder_test.go`:
- Around line 69-116: Replace silent New calls in TestWriteGoMod_noExtraRequire
and TestWriteGoMod_extraRequire with an err-checked helper: create a small
helper (e.g. func mustNewBuilder(t *testing.T, cfg Config) *Builder) that calls
t.Helper(), invokes New(cfg), fails the test with t.Fatalf if err != nil, and
returns the builder; then use mustNewBuilder(t, Config{...}) instead of b, _ :=
New(...). Also update other tests noted (TestWriteMain_*, TestCopyGoSum_*) to
use the same mustNewBuilder helper so New errors are surfaced instead of causing
panics.

In `@pkg/builder/builder.go`:
- Around line 213-223: Replace the brittle manual prefix check and unset of
GOFLAGS with strings.HasPrefix when filtering os.Environ() and change the sum DB
disable from the undocumented "GONOSUMDB=*" to the canonical "GOSUMDB=off";
specifically update the loop that builds env (used to set cmd.Env) to use
strings.HasPrefix(e, "GOFLAGS=") and append "GOSUMDB=off" and "GOFLAGS=-mod=mod"
instead of "GONOSUMDB=*" so checksum lookups are reliably disabled.
- Line 148: The generated go.mod in builder.go hardcodes "go 1.24"; update the
code that writes the header (the fmt.Fprintf call producing "module
mcp-anything-custom\n\ngo 1.24\n\nrequire (\n") to read the host module's "go"
directive and reuse it (use golang.org/x/mod/modfile.Parse or a small regex to
read the go directive from the parent module's go.mod), falling back to a single
centralized constant (near pseudoVer) if parsing fails; ensure you reference
this computed goVersion variable when formatting the go.mod instead of the
literal "1.24".

In `@pkg/builder/targets.go`:
- Line 334: Replace the direct comparison against context.Canceled in the proxy
start error check with errors.Is to correctly handle wrapped cancellation
errors: change the condition that checks startErr from "startErr !=
context.Canceled" to use errors.Is(startErr, context.Canceled) (i.e., treat the
error as ignorable when errors.Is(startErr, context.Canceled) is true) and
ensure the "errors" package is imported; this change applies to the
proxy.Start(ctx) call and its startErr handling in targets.go.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30b5c0fb-2d7a-4a61-9744-57b57cafe633

📥 Commits

Reviewing files that changed from the base of the PR and between d9ed47b and 5c98af2.

📒 Files selected for processing (5)
  • Makefile
  • cmd/builder/main.go
  • pkg/builder/builder.go
  • pkg/builder/builder_test.go
  • pkg/builder/targets.go

Comment thread pkg/builder/targets.go
Comment on lines +90 to +96
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(appsv1.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))
utilruntime.Must(gatewayv1.Install(scheme))
}

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.

⚠️ Potential issue | 🟡 Minor

Generated operator uses utilruntime.Must, which panics on scheme-registration failure.

The repo guideline says: "Never use panic() anywhere in the codebase — not in library code, not in init(), not in factories, not in tests." utilruntime.Must internally panics via its handler chain, which violates this rule in generated code too. The existing cmd/operator/main.go presumably does the same, so this may be a pre-existing tradeoff for controller-runtime idioms — flagging for your awareness rather than as a blocker for this PR. If you prefer strict compliance, return an error from an initScheme(*runtime.Scheme) error helper and log-and-exit from main.

As per coding guidelines: "Never use panic() anywhere in the codebase — not in library code, not in init(), not in factories, not in tests. All error conditions must be surfaced by returning an error value."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/targets.go` around lines 90 - 96, The init() currently uses
utilruntime.Must which panics on failure; instead implement an initScheme(s
*runtime.Scheme) error that performs the calls to
clientgoscheme.AddToScheme(scheme), corev1.AddToScheme, appsv1.AddToScheme,
v1alpha1.AddToScheme and gatewayv1.Install and returns any error encountered (do
not call utilruntime.Must), then remove the panic-raising init() body and call
initScheme(scheme) from cmd/operator/main.go (or wherever process startup
occurs), logging the error and exiting with non-zero status on failure;
reference the symbols utilruntime.Must, init(), initScheme(*runtime.Scheme),
scheme, and main when making the change.

Comment thread pkg/builder/targets.go
Comment thread pkg/builder/targets.go
Comment on lines +299 to +339
func startProxy(cfgPath string) (string, error) {
// Reserve a local port before starting the proxy so we know the address.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", fmt.Errorf("reserving local port: %w", err)
}
addr := ln.Addr().String()
if err := ln.Close(); err != nil {
return "", fmt.Errorf("releasing port: %w", err)
}

var (
path string
cfg *pkgconfig.ProxyConfig
)
if cfgPath != "" {
path = cfgPath
cfg, err = pkgconfig.Load(path)
} else {
path, cfg, err = mcpanything.LoadConfig()
}
if err != nil {
return "", fmt.Errorf("loading config: %w", err)
}

// Override the listen address with the reserved port.
cfg.Server.Addr = addr

ctx := context.Background()
proxy, err := mcpanything.New(ctx, cfg, mcpanything.WithConfigPath(path))
if err != nil {
return "", fmt.Errorf("creating proxy: %w", err)
}
proxy.StartBackground(ctx)
go func() {
if startErr := proxy.Start(ctx); startErr != nil && startErr != context.Canceled {
slog.Error("mcp-anything kong plugin: proxy error", "error", startErr)
}
}()
return addr, nil
}

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.

⚠️ Potential issue | 🔴 Critical

Kong plugin: port TOCTOU and double server start.

Two issues in startProxy:

  1. Port reserve/close race. Lines 301-308 open a listener on 127.0.0.1:0, read the address, and close it before handing the port to mcpanything.New. Another process (or the Go runtime's own ephemeral allocator) may grab that port in the window before proxy.Start rebinds, producing a hard-to-debug startup failure on loaded machines. Consider passing the open net.Listener directly into the proxy instead of round-tripping through an address string, or use addr := "127.0.0.1:0" and query the actual bound port from the proxy after startup.

  2. Double-start. Lines 332-337 call proxy.StartBackground(ctx) and then immediately spawn a goroutine that also calls proxy.Start(ctx). Based on the naming, one of these already runs the server in the background; calling both will either panic on "address already in use" (the second bind) or double-register handlers. Pick exactly one.

  3. No readiness wait. startProxy returns as soon as Start is invoked, so the very first Kong request after startOnce.Do may race the listener and get 502. A short net.DialTimeout poll (or using StartBackground's ready signal if it exposes one) would make this deterministic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/targets.go` around lines 299 - 339, startProxy has a TOCTOU on
port reservation and double-starts the server; fix by changing it to hand an
open net.Listener into the proxy (avoid reserve/close using net.Listen and pass
that listener to mcpanything.New or a new NewWithListener API) instead of
setting cfg.Server.Addr from ln.Addr(), remove the duplicate startup call so
only one of proxy.StartBackground(ctx) or proxy.Start(ctx) is invoked (pick the
background helper and delete the extra goroutine), and block until the server is
actually ready before returning (implement a short readiness poll using
net.DialTimeout to the listener address or use a StartBackground-ready signal if
available) so startProxy reliably returns a bound, accepting address.

@gaarutyunov

Copy link
Copy Markdown
Owner Author

@claude you must write e2e tests for this with examples similar to remote dev example and kraken. It’s important to use some real use case scenarios for users. For example for caddy and Kong users would have other normal APIs on the proxies. Also Kong has MCPs by itself, so we need to show our proxies capabilities that do not exist in Kong: JavaScript tools. Will need to setup CI too to build all that, because we don’t build during tests to avoid timeouts.

@claude

claude Bot commented Apr 18, 2026

Copy link
Copy Markdown

E2E Tests for Builder

  • Gather context: read existing E2E tests, builder code, CI workflow
  • Implement TestCaddyBuilderE2E — product catalog served by Caddy with normal HTTP + MCP
  • Implement TestKongJSToolsE2E — currency/crypto tools showcasing JavaScript capability Kong lacks
  • Create Dockerfile.caddy-builder for CI pre-building
  • Verified compilation with go build -tags e2e
  • make check passes
  • Committed and pushed

What was added

tests/e2e/caddy_builder_e2e_test.goTestCaddyBuilderE2E

Deploys CADDY_BUILDER_IMAGE (a Caddy binary built by mcp-anything-builder --target caddy) in k3s alongside WireMock as a product catalog backend. The Caddy pod routes:

  • GET /products/* → WireMock reverse proxy (normal HTTP traffic)
  • GET /mcp*mcpanything Caddy middleware (MCP tools)

Test verifies: direct HTTP calls through Caddy's reverse proxy work, and the MCP client can call products__list_products / products__get_product tools — both through the same custom-built binary.

tests/e2e/kong_js_tools_e2e_test.goTestKongJSToolsE2E

Deploys the standard PROXY_IMAGE with a type: script upstream. WireMock simulates a crypto/FX rates API. Two JavaScript tools are mounted from a ConfigMap:

  1. portfolio__calculate_value — calls ctx.fetch("http://wiremock:8080/rates/BTC"), multiplies quantity × price in JS, returns formatted result
  2. portfolio__compare_pairs — iterates over pairs, calls ctx.fetch for each, builds a comparison table

This demonstrates ctx.fetch + JavaScript computation that Kong's native MCP plugins cannot do.

Dockerfile.caddy-builder — multi-stage build:

  1. Compiles cmd/buildermcp-builder binary
  2. Runs mcp-builder --target caddy --module-dir /src to produce a custom Caddy binary
  3. Packages it in alpine:3.21 (the image to set as CADDY_BUILDER_IMAGE)

CI workflow changes required

Since I cannot modify .github/workflows/ci.yml directly, please add the following to .github/workflows/ci.yml:

1. Add CADDY_BUILDER_IMAGE to the top-level env block:

CADDY_BUILDER_IMAGE: ghcr.io/${{ github.repository }}-caddy-builder:sha-${{ github.sha }}

2. Add a new build job (runs in parallel with other image builds):

  build-caddy-builder-image:
    name: Build & Push Caddy Builder Image
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          file: Dockerfile.caddy-builder
          push: true
          tags: ${{ env.CADDY_BUILDER_IMAGE }}

3. Add build-caddy-builder-image to the e2e-tests job's needs:

  e2e-tests:
    needs: [build-image, build-operator-image, build-workspace-image, build-caddy-builder-image]

4. Add the new tests to the e2e-tests matrix:

        include:
          # ... existing entries ...
          - name: Caddy Builder
            test: TestCaddyBuilderE2E
          - name: Kong JS Tools
            test: TestKongJSToolsE2E

5. Pass CADDY_BUILDER_IMAGE to the e2e test step:

        env:
          PROXY_IMAGE: ${{ env.PROXY_IMAGE }}
          OPERATOR_IMAGE: ${{ env.OPERATOR_IMAGE }}
          CADDY_BUILDER_IMAGE: ${{ env.CADDY_BUILDER_IMAGE }}
          E2E_TEST: ${{ matrix.test }}
          WORKSPACE_IMAGE: ${{ env.WORKSPACE_IMAGE }}

Note: TestKongJSToolsE2E uses PROXY_IMAGE which is already available — no new image is needed for it.

View job run | Branch

TestCaddyBuilderE2E: deploys a custom Caddy binary built by
mcp-anything-builder --target caddy in k3s. WireMock backs a product
catalog REST API. Caddy routes /products/* as a normal reverse proxy
and /mcp* via the mcpanything handler, verifying that the builder
produces a binary that handles both use cases simultaneously.

TestKongJSToolsE2E: deploys the standard proxy image configured with
a script upstream. Two JavaScript tools (portfolio__calculate_value,
portfolio__compare_pairs) use ctx.fetch + JavaScript math to compute
crypto portfolio values and compare FX pairs — demonstrating JavaScript
tool capabilities that Kong's native MCP plugins lack.

Also adds Dockerfile.caddy-builder: a multi-stage Dockerfile that
builds the mcp-anything-builder binary, runs it to compile a custom
Caddy binary (--target caddy), and packages the result for CI use as
CADDY_BUILDER_IMAGE.

Co-authored-by: German Arutyunov <gaarutyunov@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (5)
tests/e2e/kong_js_tools_e2e_test.go (2)

86-100: Dead args.amount fallback in calculate_value.js.

The input schema declares only coin and quantity (both required, lines 53–62), so args.amount will always be undefined and the || fallback never fires. Either drop it for clarity, or add amount as an accepted alias in the schema if that was the intent.

♻️ Proposed fix
-    var coin = args.coin;
-    var quantity = args.amount || args.quantity;
+    var coin = args.coin;
+    var quantity = args.quantity;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/kong_js_tools_e2e_test.go` around lines 86 - 100, The script
exported as kongJSCalculateValueScript (module.exports = function(args, ctx) {
... }) uses args.amount as a fallback for quantity, but the request schema only
defines coin and quantity so args.amount is always undefined; fix this by
removing the dead fallback and consistently using args.quantity for quantity,
price calculation, and the summary, or alternatively add "amount" as an accepted
alias in the input schema so args.amount can be populated—update the function
references (quantity assignment, summary construction, and any tests)
accordingly to keep naming consistent.

394-445: Consider using consistent constants for WireMock port references.

The container, readiness probe, and service port all reference wiremockPort (lines 397, 402, 422), but the port-forward on line 442 uses wiremockAdminPort. Both constants are currently equal (8080), so there is no functional issue. However, using two different constant names interchangeably is a maintainability footgun—if one changes, the inconsistency becomes a silent bug. Either use the same constant throughout, or rename them to clarify the semantic distinction (e.g., wiremockDefaultPort and wiremockAdminPort if they should ever differ).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/kong_js_tools_e2e_test.go` around lines 394 - 445, The code uses
two interchangeable constants wiremockPort and wiremockAdminPort (used in the
container, readiness probe, Service, and the portForwardToPod call), which is
brittle; unify them by using a single constant for the WireMock port across all
uses (e.g., replace wiremockAdminPort with wiremockPort in the
portForwardToPod(...) call) or explicitly define clearly distinct constants
(e.g., wiremockDefaultPort vs wiremockAdminPort) and update all references
(Container spec, readiness probe, Service spec, and the portForwardToPod
invocation) so the same semantic port symbol is used everywhere to avoid silent
divergence.
Dockerfile.caddy-builder (2)

28-32: Optional: drop privileges in the runtime stage.

Trivy flags this image for running as root (DS-0002). For a CI-only artifact used by TestCaddyBuilderE2E this is low risk, but since Caddy listens on :8080 (not a privileged port), adding a non-root USER is trivial and keeps scanners quiet when this image is later reused outside CI.

🛡️ Proposed diff
 FROM alpine:3.21
 RUN apk add --no-cache ca-certificates
 COPY --from=build /usr/local/bin/caddy /usr/local/bin/caddy
+RUN addgroup -S caddy && adduser -S -G caddy caddy
+USER caddy
 EXPOSE 8080
 ENTRYPOINT ["caddy", "run", "--config", "/etc/caddy/Caddyfile"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile.caddy-builder` around lines 28 - 32, The runtime stage runs Caddy
as root which triggers container scanners; create a non-root user and switch to
it before the ENTRYPOINT so Caddy still binds to :8080 but runs unprivileged. In
the Dockerfile.caddy-builder runtime stage add a minimal user/group (e.g.,
caddy) and set ownership of any required paths (such as /etc/caddy and
/usr/local/bin/caddy) to that user, then add USER caddy (or UID/GID) before
ENTRYPOINT so the caddy binary and config are accessible and the process runs
non-root.

22-25: Consider pinning CGO_ENABLED=0 for the second-stage build.

Stage 1 builds mcp-builder with CGO_ENABLED=0 (line 9), but stage 2 invokes mcp-builder, which internally shells out to go build (see pkg/builder/builder.go compile) and inherits the container environment. In golang:1.25-alpine there is no C toolchain, so CGO is effectively disabled today, but this is implicit. If a future transitive dependency pulls in a CGO component (or someone adds gcc to the image), the resulting caddy binary could dynamically link against musl and break the alpine:3.21 runtime copy-in. Pinning the env explicitly makes the final binary reproducibly static and matches stage 1.

♻️ Proposed diff
-RUN /bin/mcp-builder \
+ENV CGO_ENABLED=0
+RUN /bin/mcp-builder \
     --target caddy \
     --output /usr/local/bin/caddy \
     --module-dir /src
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile.caddy-builder` around lines 22 - 25, The second-stage RUN invoking
/bin/mcp-builder should explicitly set CGO_ENABLED=0 to ensure the final caddy
binary is built statically like stage 1; modify the Dockerfile.caddy-builder RUN
that calls /bin/mcp-builder (the caddy target/output/module-dir invocation) to
export or prefix CGO_ENABLED=0 so the builder's internal go build inherits that
environment and prevents accidental dynamic linking.
tests/e2e/caddy_builder_e2e_test.go (1)

307-311: Minor: surface session.Close() errors on test teardown.

defer session.Close() silently drops the error. On MCP transport teardown this is usually benign, but logging it via t.Logf (or using t.Cleanup with an error check) makes it easier to diagnose cases where the server returned an unexpected state during shutdown, which is the kind of thing this e2e is specifically meant to catch. Same low-priority observation applies to the two io.ReadAll(resp.Body) calls at Lines 280 and 292 whose errors are discarded with _.

♻️ Proposed diff
-	session, err := mcpClient.Connect(callCtx, mcpTransport, nil)
+	session, err := mcpClient.Connect(callCtx, mcpTransport, nil)
 	if err != nil {
 		t.Fatalf("connecting MCP client: %v", err)
 	}
-	defer session.Close()
+	defer func() {
+		if err := session.Close(); err != nil {
+			t.Logf("closing MCP session: %v", err)
+		}
+	}()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/caddy_builder_e2e_test.go` around lines 307 - 311, The defer
session.Close() drops errors—wrap the close in a check and log via the test
(e.g., replace defer session.Close() with defer func() { if err :=
session.Close(); err != nil { t.Logf("session.Close error: %v", err) } }() or
use t.Cleanup to call session.Close and log any returned error) and likewise
handle the errors returned by io.ReadAll(resp.Body) (check the error and call
t.Fatalf/t.Errorf/t.Logf as appropriate after each io.ReadAll call) so teardown
and response-read failures are surfaced; look for the session variable returned
by mcpClient.Connect and the resp.Body/io.ReadAll calls near the test to apply
these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Dockerfile.caddy-builder`:
- Around line 28-32: The runtime stage runs Caddy as root which triggers
container scanners; create a non-root user and switch to it before the
ENTRYPOINT so Caddy still binds to :8080 but runs unprivileged. In the
Dockerfile.caddy-builder runtime stage add a minimal user/group (e.g., caddy)
and set ownership of any required paths (such as /etc/caddy and
/usr/local/bin/caddy) to that user, then add USER caddy (or UID/GID) before
ENTRYPOINT so the caddy binary and config are accessible and the process runs
non-root.
- Around line 22-25: The second-stage RUN invoking /bin/mcp-builder should
explicitly set CGO_ENABLED=0 to ensure the final caddy binary is built
statically like stage 1; modify the Dockerfile.caddy-builder RUN that calls
/bin/mcp-builder (the caddy target/output/module-dir invocation) to export or
prefix CGO_ENABLED=0 so the builder's internal go build inherits that
environment and prevents accidental dynamic linking.

In `@tests/e2e/caddy_builder_e2e_test.go`:
- Around line 307-311: The defer session.Close() drops errors—wrap the close in
a check and log via the test (e.g., replace defer session.Close() with defer
func() { if err := session.Close(); err != nil { t.Logf("session.Close error:
%v", err) } }() or use t.Cleanup to call session.Close and log any returned
error) and likewise handle the errors returned by io.ReadAll(resp.Body) (check
the error and call t.Fatalf/t.Errorf/t.Logf as appropriate after each io.ReadAll
call) so teardown and response-read failures are surfaced; look for the session
variable returned by mcpClient.Connect and the resp.Body/io.ReadAll calls near
the test to apply these changes.

In `@tests/e2e/kong_js_tools_e2e_test.go`:
- Around line 86-100: The script exported as kongJSCalculateValueScript
(module.exports = function(args, ctx) { ... }) uses args.amount as a fallback
for quantity, but the request schema only defines coin and quantity so
args.amount is always undefined; fix this by removing the dead fallback and
consistently using args.quantity for quantity, price calculation, and the
summary, or alternatively add "amount" as an accepted alias in the input schema
so args.amount can be populated—update the function references (quantity
assignment, summary construction, and any tests) accordingly to keep naming
consistent.
- Around line 394-445: The code uses two interchangeable constants wiremockPort
and wiremockAdminPort (used in the container, readiness probe, Service, and the
portForwardToPod call), which is brittle; unify them by using a single constant
for the WireMock port across all uses (e.g., replace wiremockAdminPort with
wiremockPort in the portForwardToPod(...) call) or explicitly define clearly
distinct constants (e.g., wiremockDefaultPort vs wiremockAdminPort) and update
all references (Container spec, readiness probe, Service spec, and the
portForwardToPod invocation) so the same semantic port symbol is used everywhere
to avoid silent divergence.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0cca380-5dd7-49ee-ab6d-fbfe5b3d7c3c

📥 Commits

Reviewing files that changed from the base of the PR and between 5c98af2 and dd8e89a.

📒 Files selected for processing (3)
  • Dockerfile.caddy-builder
  • tests/e2e/caddy_builder_e2e_test.go
  • tests/e2e/kong_js_tools_e2e_test.go

@gaarutyunov

Copy link
Copy Markdown
Owner Author

@claude You must use the Kong plugin builder and build it with king for the JavaScript e2e test, not raw proxy.

@claude

claude Bot commented Apr 18, 2026

Copy link
Copy Markdown

Claude finished @gaarutyunov's task in 15m 21s —— View job


Refactoring Kong JS E2E Test to Use Builder

  • Read current Kong JS test and relevant builder code
  • Fix pkg/builder/targets.go: use kong.Service.SetTarget, remove double-start, add 30s readiness poll
  • Fix pkg/builder/builder.go: GOSUMDB=off, strings.HasPrefix
  • Create Dockerfile.kong-builder
  • Rewrite TestKongJSToolsE2E to deploy Kong Gateway with plugin (not raw proxy)
  • Verify compilation with go build -tags e2e
  • Run make check — passes
  • Committed and pushed

What changed

pkg/builder/targets.go — Fixed the Kong plugin template:

  • Access now calls kong.Service.SetTarget(host, port) (was incorrectly setting X-Forwarded-Host which doesn't route traffic)
  • startProxy no longer double-starts the server (removed the duplicate goroutine that called proxy.Start)
  • Added a readiness poll (up to 30s) so Access only proceeds once the embedded proxy accepts connections

pkg/builder/builder.go — Fixed env setup:

  • GOSUMDB=off instead of undocumented GONOSUMDB=*
  • strings.HasPrefix(e, "GOFLAGS=") instead of manual byte-slice check

Dockerfile.kong-builder — New multi-stage Dockerfile:

  1. Build mcp-builder CLI
  2. Use mcp-builder --target kong to compile the Kong Go plugin binary
  3. Package in kong:3.9 with the binary at /usr/local/share/lua/5.1/go-plugins/mcp-anything

tests/e2e/kong_js_tools_e2e_test.go — Completely rewritten:

  • Uses KONG_BUILDER_IMAGE instead of PROXY_IMAGE
  • Deploys Kong Gateway (db-less mode) with two routes: /rates → WireMock (normal HTTP), /mcp → mcp-anything plugin
  • Verifies normal HTTP routing still works alongside the MCP plugin
  • Warms up the plugin before connecting the MCP client (avoids MCP handshake timeout during cold start)
  • Tests portfolio__calculate_value and portfolio__compare_pairs via Kong

Makefile — Added build-builder to the check target.


CI workflow changes required

Since I cannot modify .github/workflows/ci.yml directly, please add:

1. Add KONG_BUILDER_IMAGE to the top-level env block:

KONG_BUILDER_IMAGE: ghcr.io/${{ github.repository }}-kong-builder:sha-${{ github.sha }}

2. Add a new build job:

  build-kong-builder-image:
    name: Build & Push Kong Builder Image
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          file: Dockerfile.kong-builder
          push: true
          tags: ${{ env.KONG_BUILDER_IMAGE }}

3. Add build-kong-builder-image to the e2e-tests job's needs:

  e2e-tests:
    needs: [build-image, build-operator-image, build-workspace-image, build-caddy-builder-image, build-kong-builder-image]

4. Update the e2e-tests matrix (replace the existing Kong JS Tools entry):

        include:
          # ... existing entries ...
          - name: Kong JS Tools
            test: TestKongJSToolsE2E

5. Pass KONG_BUILDER_IMAGE to the e2e test step:

        env:
          PROXY_IMAGE: ${{ env.PROXY_IMAGE }}
          OPERATOR_IMAGE: ${{ env.OPERATOR_IMAGE }}
          CADDY_BUILDER_IMAGE: ${{ env.CADDY_BUILDER_IMAGE }}
          KONG_BUILDER_IMAGE: ${{ env.KONG_BUILDER_IMAGE }}
          E2E_TEST: ${{ matrix.test }}
          WORKSPACE_IMAGE: ${{ env.WORKSPACE_IMAGE }}

| Branch

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Line 229: The CI workflow's timeout-minutes is too low (10) causing
TestCaddyBuilderE2E (and related Caddy Builder / Kong JS Tools matrix cases) to
be killed before their internal 15-minute context; update the workflow to raise
the per-job and per-attempt timeouts to exceed 15 minutes (recommend 20–30
minutes) and, if desired, apply the larger timeout only to the matrix entries
for "Caddy Builder" and "Kong JS Tools" so those jobs get a higher timeout while
others remain unchanged; ensure you update every timeout-minutes key affecting
the job and matrix (including the entries referenced in the comment) so the
action-level and job-level timeouts both allow the test's 15-minute deadline and
leave headroom for retries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d34b0b0c-4e13-4a0d-96e8-68a9d790e933

📥 Commits

Reviewing files that changed from the base of the PR and between dd8e89a and 9c3a70f.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml

Comment thread .github/workflows/ci.yml
runs-on: ubuntu-latest
needs: [build-image, build-operator-image, build-workspace-image]
needs: [build-image, build-operator-image, build-caddy-builder-image, build-workspace-image]
timeout-minutes: 10

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.

⚠️ Potential issue | 🟠 Major

Raise the e2e timeout for the new builder-backed tests.

TestCaddyBuilderE2E allows up to 15 minutes internally, but the workflow kills each attempt after 10 minutes and the whole job after 10 minutes. The new builder/Kong cases can be terminated before their own deadline, making CI flaky or consistently failing on slower runners.

Suggested timeout adjustment
-    timeout-minutes: 10
+    timeout-minutes: 65
@@
-          timeout_minutes: 10
+          timeout_minutes: 20

If runtime is a concern, make this matrix-specific for only Caddy Builder and Kong JS Tools; the key is that the per-attempt timeout must exceed the test’s 15-minute context, and the job timeout must leave room for retries.

Also applies to: 242-245, 257-257

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 229, The CI workflow's timeout-minutes is
too low (10) causing TestCaddyBuilderE2E (and related Caddy Builder / Kong JS
Tools matrix cases) to be killed before their internal 15-minute context; update
the workflow to raise the per-job and per-attempt timeouts to exceed 15 minutes
(recommend 20–30 minutes) and, if desired, apply the larger timeout only to the
matrix entries for "Caddy Builder" and "Kong JS Tools" so those jobs get a
higher timeout while others remain unchanged; ensure you update every
timeout-minutes key affecting the job and matrix (including the entries
referenced in the comment) so the action-level and job-level timeouts both allow
the test's 15-minute deadline and leave headroom for retries.

Replace raw proxy deployment with a proper Kong Gateway setup:
- TestKongJSToolsE2E now requires KONG_BUILDER_IMAGE (Kong image with
  the mcp-anything Go plugin binary built by `builder --target kong`)
- Add Dockerfile.kong-builder: builds the plugin, packages it in kong:3.9
- Fix kongTarget template: use kong.Service.SetTarget instead of
  X-Forwarded-Host, remove double-start in startProxy, add readiness poll
- Fix builder.go: use strings.HasPrefix and GOSUMDB=off (was GONOSUMDB=*)
- Add build-builder to Makefile check target

The test scenario: Kong handles normal HTTP (GET /rates/* → WireMock) and
the mcp-anything plugin adds JavaScript MCP tools (portfolio__calculate_value,
portfolio__compare_pairs) that Kong's native MCP support cannot execute.

Co-authored-by: German Arutyunov <gaarutyunov@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (6)
tests/e2e/kong_js_tools_e2e_test.go (2)

452-469: Minor: waitForHTTPNon5xx swallows transport errors, making CI timeouts hard to debug.

When c.Get returns a non-nil err (DNS failure, connection refused during Kong startup, etc.) the error is ignored and lastStatus stays at its previous value. On context expiration the returned message reports last status: 0 with no clue about the underlying network error, which is exactly the scenario where this warm-up helper is most likely to time out in CI. Consider tracking and surfacing the most recent error too:

♻️ Proposed refactor
 func waitForHTTPNon5xx(ctx context.Context, c *http.Client, targetURL string) error {
-	var lastStatus int
+	var (
+		lastStatus int
+		lastErr    error
+	)
 	for {
 		resp, err := c.Get(targetURL) //nolint:noctx
 		if err == nil {
 			lastStatus = resp.StatusCode
 			_ = resp.Body.Close()
 			if resp.StatusCode < 500 {
 				return nil
 			}
+		} else {
+			lastErr = err
 		}
 		select {
 		case <-ctx.Done():
-			return fmt.Errorf("context expired waiting for non-5xx from %s (last status: %d): %w", targetURL, lastStatus, ctx.Err())
+			return fmt.Errorf("context expired waiting for non-5xx from %s (last status: %d, last err: %v): %w", targetURL, lastStatus, lastErr, ctx.Err())
 		case <-time.After(3 * time.Second):
 		}
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/kong_js_tools_e2e_test.go` around lines 452 - 469, The helper
waitForHTTPNon5xx currently ignores transport errors from c.Get, so on timeout
you lose the underlying network error; modify waitForHTTPNon5xx to record the
most recent non-nil error (e.g., lastErr) each loop iteration when c.Get returns
err, and include that error in the returned fmt.Errorf alongside lastStatus when
ctx expires; ensure you still close resp.Body only when resp != nil and preserve
existing 3s retry behavior.

551-646: deployKong returns before the pod is actually up — verify callers always gate on readiness.

deployKong creates the Deployment and returns; it does not block on pod readiness. The caller at Line 280 does waitForDeploymentPod, so the current flow is correct, but a future caller might reasonably expect deployKong to return a ready deployment (mirroring deployKongJSWiremock, which does wait internally). Consider either making the helper symmetric by waiting internally, or renaming to createKongDeployment to signal that the caller owns readiness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/kong_js_tools_e2e_test.go` around lines 551 - 646, deployKong
currently creates the Kong Deployment but returns before pods are ready (unlike
deployKongJSWiremock which waits); either make it symmetric by blocking until a
pod is ready or rename it to indicate it only creates the resource. Fix by
updating deployKong: after c.Create(...) succeed, call the existing
waitForDeploymentPod (or the same readiness logic used by deployKongJSWiremock)
to wait for the pod/readiness probe to pass before returning; alternatively,
rename the function to createKongDeployment and update all callers (e.g. the
caller that currently calls waitForDeploymentPod at line ~280) so the
responsibility for readiness is clear.
Dockerfile.kong-builder (1)

29-30: Consider pinning the Kong base image by digest and/or asserting a non-root user.

kong:3.9 is a floating minor tag; a future rebuild can silently pull a different image. For reproducible e2e runs (especially in CI where KONG_BUILDER_IMAGE is shared across tests) consider pinning to a specific patch tag (kong:3.9.x) or digest (kong@sha256:...). The kong:3.9 image runs as the kong user by default, so the Trivy DS-0002 warning is effectively a false positive — but an explicit USER kong at the bottom makes that intent explicit and guards against base-image regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile.kong-builder` around lines 29 - 30, The Dockerfile currently uses
the floating tag "kong:3.9" and doesn't assert the runtime user; change the FROM
line to pin to a specific patch tag or digest (e.g., replace "kong:3.9" with
"kong:3.9.x" or "kong@sha256:...") to ensure reproducible builds, and add an
explicit runtime user declaration by appending "USER kong" (or the correct
non-root username) at the end of the Dockerfile to make the non-root intent
explicit and silence DS-0002.
pkg/builder/builder.go (3)

207-230: Asymmetric stream config: Stderr is user-configurable but Stdout is hardcoded.

cfg.Stderr is overridable; cmd.Stdout = os.Stdout is not. This makes it awkward for callers (CLI wrappers, tests, image build pipelines) to fully capture the go build output — e.g. a library user wanting to tee both streams into a logger has to re-route globals. Consider adding a Stdout io.Writer field to Config that defaults to os.Stdout, mirroring Stderr.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/builder.go` around lines 207 - 230, The goCmd function currently
writes stdout directly to os.Stdout while stderr honors the configurable
b.cfg.Stderr, so add a Stdout io.Writer field to the Config struct (defaulting
to os.Stdout) and use it in goCmd (replace cmd.Stdout = os.Stdout with
cmd.Stdout = b.cfg.Stdout) to make both streams configurable; ensure Config
initialization sets Stdout to os.Stdout when nil and update any
constructors/tests to propagate the new field.

159-171: Minor: prefer errors.Is(err, fs.ErrNotExist) over os.IsNotExist.

Also, os.IsNotExist is called before the generic error check, but os.ReadFile can return wrapped errors (*PathError). os.IsNotExist handles that case, so behavior is correct today — but errors.Is(err, fs.ErrNotExist) is the idiomatic replacement and composes better with wrapped errors.

♻️ Proposed refactor
-	src := filepath.Join(b.cfg.ModuleDir, "go.sum")
-	data, err := os.ReadFile(src)
-	if os.IsNotExist(err) {
-		return nil // module has no go.sum yet
-	}
-	if err != nil {
-		return fmt.Errorf("reading go.sum: %w", err)
-	}
+	src := filepath.Join(b.cfg.ModuleDir, "go.sum")
+	data, err := os.ReadFile(src)
+	if errors.Is(err, fs.ErrNotExist) {
+		return nil // module has no go.sum yet
+	}
+	if err != nil {
+		return fmt.Errorf("reading go.sum: %w", err)
+	}

(requires adding "errors" and "io/fs" to imports)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/builder.go` around lines 159 - 171, In copyGoSum, replace the
os.IsNotExist(err) check with the idiomatic errors.Is(err, fs.ErrNotExist) to
better handle wrapped errors; keep that existence check before the generic if
err != nil branch, and add the required imports ("errors" and "io/fs") to the
file so the function (copyGoSum) compiles and preserves the current behavior of
returning nil when the module has no go.sum.

147-157: Hardcoded go 1.24 does not match the project's actual Go directive.

The root go.mod specifies go 1.25.0, but the generated go.mod is pinned to go 1.24. This mismatch can cause go build to fail or resolve modules differently than the main project if the parent module relies on language/toolchain features from the newer directive. Use golang.org/x/mod/modfile to parse the root go.mod and emit the matching version in the generated module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/builder.go` around lines 147 - 157, The writeGoMod function
currently hardcodes "go 1.24"; instead, read and parse the root go.mod to emit
the matching "go" directive: use golang.org/x/mod/modfile to load and parse the
root module's go.mod (e.g., from b.cfg.ModuleDir), extract the Go version from
the parsed modfile.Go setting, and write that value into the generated buffer
rather than the literal "1.24"; keep the existing use of localModule, pseudoVer,
and t.extraRequire when building the file and return the same os.WriteFile
result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/builder/builder.go`:
- Around line 213-224: The code currently unconditionally sets "GOSUMDB=off" in
the env slice (env, cmd.Env) which weakens checksum verification for targets
that fetch external deps; change the env construction so "GOSUMDB=off" is only
appended when the build target does not need downloads (i.e., when
!target.needsDownload or when the local replace is the only concern). Locate the
env/shell block that builds env and sets cmd.Env and conditionally append
"GOSUMDB=off" only when target.needsDownload is false (leave "GOFLAGS=-mod=mod"
handling the same), so external module downloads still consult the sum DB.

---

Nitpick comments:
In `@Dockerfile.kong-builder`:
- Around line 29-30: The Dockerfile currently uses the floating tag "kong:3.9"
and doesn't assert the runtime user; change the FROM line to pin to a specific
patch tag or digest (e.g., replace "kong:3.9" with "kong:3.9.x" or
"kong@sha256:...") to ensure reproducible builds, and add an explicit runtime
user declaration by appending "USER kong" (or the correct non-root username) at
the end of the Dockerfile to make the non-root intent explicit and silence
DS-0002.

In `@pkg/builder/builder.go`:
- Around line 207-230: The goCmd function currently writes stdout directly to
os.Stdout while stderr honors the configurable b.cfg.Stderr, so add a Stdout
io.Writer field to the Config struct (defaulting to os.Stdout) and use it in
goCmd (replace cmd.Stdout = os.Stdout with cmd.Stdout = b.cfg.Stdout) to make
both streams configurable; ensure Config initialization sets Stdout to os.Stdout
when nil and update any constructors/tests to propagate the new field.
- Around line 159-171: In copyGoSum, replace the os.IsNotExist(err) check with
the idiomatic errors.Is(err, fs.ErrNotExist) to better handle wrapped errors;
keep that existence check before the generic if err != nil branch, and add the
required imports ("errors" and "io/fs") to the file so the function (copyGoSum)
compiles and preserves the current behavior of returning nil when the module has
no go.sum.
- Around line 147-157: The writeGoMod function currently hardcodes "go 1.24";
instead, read and parse the root go.mod to emit the matching "go" directive: use
golang.org/x/mod/modfile to load and parse the root module's go.mod (e.g., from
b.cfg.ModuleDir), extract the Go version from the parsed modfile.Go setting, and
write that value into the generated buffer rather than the literal "1.24"; keep
the existing use of localModule, pseudoVer, and t.extraRequire when building the
file and return the same os.WriteFile result.

In `@tests/e2e/kong_js_tools_e2e_test.go`:
- Around line 452-469: The helper waitForHTTPNon5xx currently ignores transport
errors from c.Get, so on timeout you lose the underlying network error; modify
waitForHTTPNon5xx to record the most recent non-nil error (e.g., lastErr) each
loop iteration when c.Get returns err, and include that error in the returned
fmt.Errorf alongside lastStatus when ctx expires; ensure you still close
resp.Body only when resp != nil and preserve existing 3s retry behavior.
- Around line 551-646: deployKong currently creates the Kong Deployment but
returns before pods are ready (unlike deployKongJSWiremock which waits); either
make it symmetric by blocking until a pod is ready or rename it to indicate it
only creates the resource. Fix by updating deployKong: after c.Create(...)
succeed, call the existing waitForDeploymentPod (or the same readiness logic
used by deployKongJSWiremock) to wait for the pod/readiness probe to pass before
returning; alternatively, rename the function to createKongDeployment and update
all callers (e.g. the caller that currently calls waitForDeploymentPod at line
~280) so the responsibility for readiness is clear.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff71b507-ac9e-47ca-a683-eef49835a785

📥 Commits

Reviewing files that changed from the base of the PR and between 9c3a70f and 929886a.

📒 Files selected for processing (5)
  • Dockerfile.kong-builder
  • Makefile
  • pkg/builder/builder.go
  • pkg/builder/targets.go
  • tests/e2e/kong_js_tools_e2e_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • Makefile
  • pkg/builder/targets.go

Comment thread pkg/builder/builder.go
Comment on lines +213 to +224

// Inherit env but clear GOFLAGS to avoid interference; disable sum DB so
// the local replacement module does not require a go.sum entry.
env := make([]string, 0, len(os.Environ())+2)
for _, e := range os.Environ() {
if strings.HasPrefix(e, "GOFLAGS=") {
continue
}
env = append(env, e)
}
env = append(env, "GOSUMDB=off", "GOFLAGS=-mod=mod")
cmd.Env = env

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.

⚠️ Potential issue | 🟡 Minor

GOSUMDB=off silently weakens checksum verification for targets that fetch extra deps.

For kong (needsDownload: true with github.com/Kong/go-pdk), go mod download populates go.sum from downloaded content without consulting a sum DB. If the project's own go.sum doesn't already pin go-pdk (which is not a main-project dep), there is no external verification of the fetched module. The in-code comment justifies the setting only for the local replace directive — worth narrowing the rationale or keeping sum-DB verification on for external deps (e.g. only set GOSUMDB=off when !target.needsDownload, or add GOFLAGS=-insecure=false and rely on GOPROXY/sumdb for third-party modules).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/builder/builder.go` around lines 213 - 224, The code currently
unconditionally sets "GOSUMDB=off" in the env slice (env, cmd.Env) which weakens
checksum verification for targets that fetch external deps; change the env
construction so "GOSUMDB=off" is only appended when the build target does not
need downloads (i.e., when !target.needsDownload or when the local replace is
the only concern). Locate the env/shell block that builds env and sets cmd.Env
and conditionally append "GOSUMDB=off" only when target.needsDownload is false
(leave "GOFLAGS=-mod=mod" handling the same), so external module downloads still
consult the sum DB.

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.

Add xcaddy like builder

1 participant