Skip to content

feature: velox server #248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 17, 2025
Merged

feature: velox server #248

merged 15 commits into from
Aug 17, 2025

Conversation

rustatian
Copy link
Member

@rustatian rustatian commented Aug 13, 2025

Reason for This PR

  • Velox server with HTTP/gRPC api used to build RR binaries remotely.

Description of Changes

  1. HTTP/gRPC (h2c) support.
  2. Caching of already built binaries (30min LRU cache).
  3. Multithreading. Removed syscalls to chdir (since process-wide).
  4. buf + connectrpc for protobuf.
  5. Build on any platform for any platform (Go cross compilation).
  6. Automatic input validation based on the protolint-rules.
  7. All (GitHub, GitLab, custom urls, etc) platforms are supported. GITHUB_TOKEN might be set, but is not required to download an RR base template from GitHub.
  8. Command to run: vx server -a 127.0.0.1:9000.
  9. TBD: Docs: update how to develop, for the development, close RR, use replace in the go.mod. velox v2 (not a release branch) will support only production building, since dev builds can be done manually with ease (explain how).

Request example:

{
  "request_id": "b66d5617-64dd-419b-a68b-b002938320ab",
  "force_rebuild": true,
  "target_platform": {
    "os": "windows",
    "arch": "amd64"
  },
  "rr_version": "v2025.1.2",
  "plugins": [
    {
      "module_name": "github.com/roadrunner-server/http/v5",
      "tag": "v5.2.7"
    }
  ]
}

cURL:

curl --request POST \
  --url http://127.0.0.1:9000/api.service.v1.BuildService/Build \
  --header 'content-type: application/json' \
  --data '{
  "request_id": "b66d5617-64dd-419b-a68b-b002938320ab",
  "force_rebuild": true,
  "target_platform": {
    "os": "windows",
    "arch": "amd64"
  },
  "rr_version": "v2025.1.2",
  "plugins": [
    {
      "module_name": "github.com/roadrunner-server/http/v5",
      "tag": "v5.2.7"
    }
  ]
}'

100 concurrent users are bombing with requests to build RR with all available plugins for the different platforms and archs:
Top memory usage - 42mb.
image

k6 results:
image

License Acceptance

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

PR Checklist

[Author TODO: Meet these criteria.]
[Reviewer TODO: Verify that these criteria are met. Request changes if not]

  • All commits in this PR are signed (git commit -s).
  • The reason for this PR is clearly provided (issue no. or explanation).
  • The description of changes is clear and encompassing.
  • Any required documentation changes (code and docs) are included in this PR.
  • Any user-facing changes are mentioned in CHANGELOG.md.
  • All added/changed functionality is tested.

Summary by CodeRabbit

  • New Features
    • HTTP/2 "server" subcommand exposing a Build API with validation, caching, and concurrency guards.
    • Protobuf BuildRequest/BuildResponse and BuildService endpoint; generated Go code config via Buf.
    • New v2 builder, plugin model, GitHub template downloader, in-memory RR cache, and functional-options Builder API.
  • Documentation
    • Comprehensive developer guide for the build system, workflows, and setup.
  • Chores
    • Makefile "regenerate" target; added k6 load test; .gitignore ignores .DS_Store.
  • Refactor
    • Modernized path handling and several dependency upgrades.
  • Tests
    • Added template and GitHub client tests.

Signed-off-by: Valery Piashchynski <[email protected]>
@rustatian rustatian requested a review from Copilot August 13, 2025 19:49
@rustatian rustatian self-assigned this Aug 13, 2025
@rustatian rustatian added the A-other Area: other label Aug 13, 2025
Copy link

coderabbitai bot commented Aug 13, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds protobuf APIs and Buf configs, upgrades dependencies, refactors Builder to an options-based API, introduces a v2 templated builder and GitHub template downloader, implements an h2c BuildService server with validation, caching and concurrency guards, wires a CLI server command, and adds docs, tests, a k6 benchmark, and tooling targets.

Changes

Cohort / File(s) Summary
Documentation
.claude/CLAUDE.md
New comprehensive project documentation describing architecture, tools, commands, repo layout, development patterns, and setup.
Ignore / Make
.gitignore, Makefile
Added .DS_Store to .gitignore; added regenerate Make target to remove ./gen, run buf generate and buf format -w.
Protobuf & Buf config
api/request/v1/request.proto, api/response/v1/response.proto, api/service/v1/service.proto, buf.yaml, buf.gen.yaml
Added BuildRequest, BuildResponse, Platform, Plugin messages, BuildService RPC, field validation annotations, and Buf generation/lint/breaking configs for Go, gRPC, and Connect.
Go module / deps
go.mod
Added/updated dependencies (buf protovalidate, connect-go, google/go-github v74, grpc/protobuf upgrades, lru, and various indirects) and replace .../gen => ./gen.
Legacy Builder refactor
builder/*
builder/builder.go, builder/options.go, builder/builder_test.go, builder/templates/templateV2025.go
Migrated Builder to functional-options API, added exec centralization, output/log capture, GOOS/GOARCH options, tests updated, switched to filepath, removed hard-coded replace in template.
Config defaults
config.go
Added exported DefaultRRRef and DefaultConfig global defaults.
Provider clients & path fixes
github/pool.go, github/repo.go, gitlab/repo.go
Replaced path with filepath for OS-aware joins and bumped google/go-github client to v74.
CLI wiring & server flag
internal/cli/root.go, internal/cli/build/build.go
Added persistent --address flag, integrated server subcommand via server.BindCommand, skip config load for server, and switched build invocation to options-based Builder.
BuildService server
internal/cli/server/*
internal/cli/server/command.go, internal/cli/server/server.go
New server command and BuildServer with protovalidate validation, LRU result cache, concurrency guard, RR template download, builder orchestration, and error-code handling.
v2 builder, templating & tests
v2/builder/*, v2/builder/templates/*, v2/builder/templates/template_test.go
New v2 Builder with options, template compilation utilities, versioned go.mod/plugins templates (2024/2025), build/write/exec flow, cross-compilation flags and a template unit test.
v2 cache & config
v2/cache/cache.go, v2/config/builder.go
Added RRCache (concurrency-safe bytes.Buffer cache) and BuilderConfig struct.
v2 GitHub downloader & test
v2/github/github.go, v2/github/github_test.go
New GitHubClient to download/extract RR templates (tags/branches/commits), in-memory caching, safe extraction, and a smoke test.
v2 plugin helpers
v2/plugin/plugin.go
Added Plugin type producing import lines, require entries, and code snippets (with random prefixes).
k6 load test
k6-benchmark.js
Added k6 script to exercise BuildService with platform matrix, metrics, thresholds, and payload generation.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ServerCLI as CLI (h2c)
  participant S as BuildServer
  participant V as Validator
  participant C as LRU Cache
  participant GH as GitHubClient
  participant B as Builder

  Client->>ServerCLI: POST BuildService/Build
  ServerCLI->>S: Build(request)
  S->>V: Validate(request)
  alt invalid
    S-->>ServerCLI: CodeInvalidArgument
    ServerCLI-->>Client: Error
  else valid
    S->>C: Check in-progress/cache
    alt in-progress
      S-->>ServerCLI: CodeAlreadyExists
      ServerCLI-->>Client: Error
    else cached and not force
      C-->>S: return path
      S-->>ServerCLI: BuildResponse(path, logs note)
      ServerCLI-->>Client: 200
    else build needed
      S->>GH: DownloadTemplate(rrVersion)
      GH-->>S: template path
      S->>B: NewBuilder(...options)
      S->>B: Build(rrVersion)
      B-->>S: binary path + logs
      S->>C: Store(path)
      S-->>ServerCLI: BuildResponse(path, logs)
      ServerCLI-->>Client: 200
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

go, dependencies

Poem

I’m a rabbit with a builder’s hat,
I stitched some protos, cached a stat.
Templates hopped from v24 to v25,
Servers hummed and builds came alive.
Carrot-binary ready — hop, ship, thrive! 🥕🛠️

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/server

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copilot

This comment was marked as outdated.

@rustatian rustatian moved this to 🏗 In progress in Jira 😄 Aug 13, 2025
@rustatian rustatian marked this pull request as ready for review August 17, 2025 09:33
@rustatian rustatian requested a review from Copilot August 17, 2025 09:33
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces a new Velox server feature that provides HTTP/gRPC build-as-a-service functionality for RoadRunner binaries. The server supports cross-platform builds with caching, request validation, and concurrent request handling.

Key changes include:

  • New HTTP/gRPC server with build service API using buf/connectrpc
  • Refactored v2 architecture with improved builder, caching, and GitHub integration
  • Template system supporting multiple RoadRunner versions (2024/2025) with cross-compilation support

Reviewed Changes

Copilot reviewed 37 out of 40 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
internal/cli/server/ New server command and build service implementation
v2/ Refactored v2 components (builder, cache, GitHub client, plugin management)
api/ Protocol buffer service definitions for build requests/responses
gen/ Generated protobuf code for gRPC/Connect services
go.mod Added dependencies for buf, connect-go, protovalidate, and LRU cache

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Signed-off-by: Valery Piashchynski <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 21

🔭 Outside diff range comments (5)
github/pool.go (2)

59-60: Compile-time bug: invalid range over int

for range p.maxWorkers does not compile in Go (cannot range over an int). Use a classic for-loop to spawn workers.

 func (p *processor) run() {
-	for range p.maxWorkers {
+	for i := 0; i < p.maxWorkers; i++ {
 		go func() {
 			for v := range p.queueCh {

116-119: Resource leak: close the content reader (rc), not resp.Body

DownloadContents returns an io.ReadCloser (rc) that must be closed. Closing resp.Body here is incorrect and can lead to leaks or double-closes. Close rc after scanning.

-				err = resp.Body.Close()
-				if err != nil {
-					p.log.Warn("failed to close response body, continuing", zap.Any("error", err))
-				}
+				if err := rc.Close(); err != nil {
+					p.log.Warn("failed to close content reader, continuing", zap.Any("error", err))
+				}
v2/cache/cache.go (1)

1-57: Add eviction/TTL to RRCache to prevent unbounded memory growth

After inspecting the codebase, I confirmed that v2/cache/cache.go’s RRCache
• Holds all entries in a plain map with no size or time‐based eviction
• Is used to cache potentially large GitHub release blobs (zipped binaries)

Even though we already use an expirable LRU in internal/cli/server, RRCache remains unbounded—contradicting the PR’s 30-minute LRU promise. Without eviction, a long-running process can exhaust memory.

Recommended refactor (mandatory):

  • In v2/cache/cache.go, replace the custom map with an expirable LRU (from github.com/hashicorp/golang-lru/v2/expirable) configured with TTL=30 minutes (and optionally a max entry count)
  • Or extend RRCache to wrap each buffer with timestamps and run a background sweeper that deletes entries past their expireAt or when total size/count exceeds a cap

Example using hashicorp’s expirable LRU:

-import (
-   "bytes"; "io"; "sync"
-)
+import (
+   "bytes"; "io"
+   "github.com/hashicorp/golang-lru/v2/expirable"
+)
 
-type RRCache struct {
-   mu   *sync.RWMutex
-   data map[string]*bytes.Buffer
-}
+type RRCache struct {
+   lru *expirable.LRU[string, *bytes.Buffer]
+}
 
 func NewRRCache() *RRCache {
-   return &RRCache{
-      mu:   &sync.RWMutex{},
-      data: make(map[string]*bytes.Buffer),
-   }
+   l, _ := expirable.New[string, *bytes.Buffer](128, 30*time.Minute, nil)
+   return &RRCache{lru: l}
 }
 
 func (c *RRCache) Get(key string) *bytes.Buffer {
-   c.mu.RLock()
-   defer c.mu.RUnlock()
-   if cache, ok := c.data[key]; ok {
+   if cache, ok := c.lru.Get(key); ok {
       buf := new(bytes.Buffer)
       buf.Grow(cache.Len())
       _, err := io.Copy(buf, bytes.NewReader(cache.Bytes()))
       if err != nil {
         panic(err)
       }
       return buf
     }
 
     return nil
 }
 
 func (c *RRCache) Set(key string, buf *bytes.Buffer) {
-   c.mu.Lock()
-   defer c.mu.Unlock()
-
-   if buf == nil {
-     panic("cannot set nil value in cache")
-   }
-   if _, ok := c.data[key]; ok {
-     return
-   }
-   cache := new(bytes.Buffer)
-   cache.Grow(buf.Len())
-   _, err := io.Copy(cache, bytes.NewReader(buf.Bytes()))
-   if err != nil {
-     panic(err)
-   }
-   c.data[key] = cache
+   if buf == nil {
+     panic("cannot set nil value in cache")
+   }
+   // Copy buffer to avoid mutation
+   cache := new(bytes.Buffer)
+   cache.Grow(buf.Len())
+   if _, err := io.Copy(cache, bytes.NewReader(buf.Bytes())); err != nil {
+     panic(err)
+   }
+   c.lru.Add(key, cache)
 }
builder/builder.go (1)

288-296: Duplicate -ldflags usage likely overrides flags; combine into a single value

You append -ldflags twice: once for “-s” and again for build metadata. The last one wins and drops “-s”. Merge them.

Apply this diff:

 case false:
-  buildCmdArgs = append(buildCmdArgs, "-ldflags", "-s")
+  // will be merged with metadata below
+  buildCmdArgs = append(buildCmdArgs, "-ldflags", "-s")
 }
 
 // LDFLAGS for version and build time, always appended
-buildCmdArgs = append(buildCmdArgs, "-ldflags")
-buildCmdArgs = append(buildCmdArgs, fmt.Sprintf(ldflags, b.rrVersion, time.Now().UTC().Format(time.RFC3339)))
+// Merge previous -ldflags (if any) with metadata flags.
+// Collect last -ldflags index if present.
+ld := fmt.Sprintf(ldflags, b.rrVersion, time.Now().UTC().Format(time.RFC3339))
+if n := len(buildCmdArgs); n >= 2 && buildCmdArgs[n-2] == "-ldflags" {
+  buildCmdArgs[n-1] = buildCmdArgs[n-1] + " " + ld
+} else {
+  buildCmdArgs = append(buildCmdArgs, "-ldflags", ld)
+}

Optional cleanup: also consolidate gcflags like: "-gcflags", "all=-N -l".

internal/cli/root.go (1)

62-63: Potential nil pointer dereference when config is nil

The config pointer is only initialized after the command validation passes. If the PersistentPreRunE hook is bypassed or if there's an error, config could remain nil when accessed by the build command.

Initialize the config pointer to prevent nil dereferences:

 	var (
 		pathToConfig string // path to the velox configuration
 		outputFile   string // output file (optionally with directory)
 		address      string
-		config       *velox.Config // velox configuration
+		config       = &velox.Config{} // velox configuration
 	)
🧹 Nitpick comments (50)
v2/config/builder.go (1)

3-6: Add struct tags (json/yaml/toml) for config unmarshalling

If this struct is loaded from config, add tags to ensure consistent key mapping across formats. Also consider a short doc comment to satisfy linters.

Apply tags:

 type BuilderConfig struct {
-	ModuleName string
-	Tag        string
+	ModuleName string `json:"module_name" yaml:"module_name" toml:"module_name"`
+	Tag        string `json:"tag" yaml:"tag" toml:"tag"`
 }

Optional doc comment (outside the selected lines):

// BuilderConfig carries module configuration for the v2 builder.
api/response/v1/response.proto (1)

7-10: Document fields; consider future-proofing logs delivery

Add concise comments for lint/readability. Also consider that logs can be large; if you anticipate big payloads, think about streaming logs or returning a URL to logs instead of embedding full text in the response.

-message BuildResponse {
-  string path = 1;
-  string logs = 2;
-}
+// BuildResponse is returned by BuildService.Build.
+message BuildResponse {
+  // Absolute (or container-local) path to the built RR binary.
+  string path = 1;
+  // Build output logs. Consider streaming or capping length if logs can be large.
+  string logs = 2;
+}

Optional: If caching status is useful to clients, add a boolean to indicate cache hits in a backward-compatible way:

bool cache_hit = 3;
github/repo.go (4)

65-66: Fix log typo: “seding” → “sending”.

Apply:

-	r.log.Info("seding download request", zap.String("url", url.String()))
+	r.log.Info("sending download request", zap.String("url", url.String()))

73-76: Guard against non-200 download responses.

After Do, validate the status code to fail fast on errors (rate limits, 404, etc.).

Apply:

 	do, err := r.client.Do(context.Background(), request, buf)
 	if err != nil {
 		return "", err
 	}
+	if do.Response == nil || do.Response.StatusCode != http.StatusOK {
+		if do != nil && do.Response != nil {
+			return "", fmt.Errorf("unexpected status code from archive download: %d", do.Response.StatusCode)
+		}
+		return "", fmt.Errorf("nil response from archive download")
+	}
 
 	_, _ = io.Copy(io.Discard, do.Body)
 	_ = do.Body.Close()

87-96: Cleanup temp archive after extraction to avoid disk bloat.

The .zip file under tmp is not removed. Remove it after use.

Apply:

 	f, err := os.Create(name + zipExt)
 	if err != nil {
 		return "", err
 	}
 
 	defer func() {
 		_ = f.Close()
 	}()
+	// ensure temporary archive is removed after extraction succeeds/fails
+	defer func() {
+		_ = os.Remove(name + zipExt)
+	}()

133-145: Redundant “..” checks on zip entry names.

The checks against .. on the absolute/relative zip entry names are redundant given the stronger prefix check in extract(). Consider removing to reduce noise.

Makefile (1)

4-7: Add PHONY targets and consider buf lint/breaking checks.

Marking targets as .PHONY avoids accidental conflicts with files named test or regenerate. Optionally, add buf lint (and buf breaking in CI) to catch proto issues earlier.

Apply:

+ .PHONY: test regenerate
 test:
 	go test -v -race ./...
 
 regenerate:
 	rm -rf ./gen
 	buf generate
 	buf format -w

Optionally extend:

proto-check:
	buf lint
# In CI, consider: buf breaking --against '.git#branch=main'
config.go (1)

20-32: Avoid exporting a mutable global Config; provide a constructor to return a fresh copy.

An exported global pointer can be mutated from multiple places, leading to hidden coupling and data races in long-running server processes. Prefer a constructor returning a new instance.

Keep DefaultConfig for backward compatibility, but add:

// NewDefaultConfig returns a fresh default configuration instance.
func NewDefaultConfig() *Config {
	return &Config{
		Roadrunner: map[string]string{ref: defaultBranch},
		Debug:      &Debug{Enabled: false},
		Log:        map[string]string{"level": "debug", "mode": "development"},
	}
}

Callers should use velox.NewDefaultConfig() rather than mutating velox.DefaultConfig.

v2/cache/cache.go (1)

9-12: RWMutex should not be a pointer.

RWMutex is safe to embed by value; storing it as a pointer is unnecessary and invites accidental copying.

Apply:

 type RRCache struct {
-	mu   *sync.RWMutex
+	mu   sync.RWMutex
 	data map[string]*bytes.Buffer
 }
 
 func NewRRCache() *RRCache {
 	return &RRCache{
-		mu:   &sync.RWMutex{},
+		mu:   sync.RWMutex{},
 		data: make(map[string]*bytes.Buffer),
 	}
 }
.claude/CLAUDE.md (2)

17-27: Add a language to this fenced block (MD040).

markdownlint flags this block; add a language hint for better rendering and lint compliance.

Apply this diff:

-```
+```text

46-49: Align server run example with the PR’s server port (9000).

PR text and server command use 9000; docs show 8080. Let’s avoid drift.

Apply this diff:

-./vx -c velox.toml server -a 127.0.0.1:8080
+./vx -c velox.toml server -a 127.0.0.1:9000
v2/plugin/plugin.go (3)

3-6: Avoid weak/global RNG; use a deterministic alias for reproducible builds.

math/rand with the default global source is predictable and varies per run if seeded. A deterministic alias derived from (moduleName, tag) improves reproducibility and cache hit rates while still minimizing collision risk.

Apply this diff to replace math/rand with a deterministic hash-based prefix:

 import (
 	"fmt"
-	"math/rand"
+	"hash/fnv"
 )

23-29: Derive the prefix deterministically from moduleName and tag.

This ensures stable imports and generated code across identical requests.

Apply this diff:

 func NewPlugin(moduleName, tag string) *Plugin {
 	return &Plugin{
-		prefix:     randStringBytes(5),
+		prefix:     deterministicPrefix(moduleName, tag),
 		moduleName: moduleName,
 		tag:        tag,
 	}
 }

50-56: Replace rand-based generator with a small deterministic hash encoder.

Keeps the 5-char alias while eliminating RNG. Collision probability remains very low for typical plugin sets.

Apply this diff:

-func randStringBytes(n int) string {
-	b := make([]byte, n)
-	for i := range b {
-		b[i] = letterBytes[rand.Intn(len(letterBytes))] //nolint:gosec
-	}
-	return string(b)
-}
+func deterministicPrefix(moduleName, tag string) string {
+	const n = 5
+	h := fnv.New32a()
+	// Write input deterministically; no errors for these writes.
+	_, _ = h.Write([]byte(moduleName))
+	_, _ = h.Write([]byte("@"))
+	_, _ = h.Write([]byte(tag))
+	v := h.Sum32()
+
+	alphabet := []byte(letterBytes)
+	base := uint32(len(alphabet))
+	out := make([]byte, n)
+	for i := 0; i < n; i++ {
+		out[i] = alphabet[v%base]
+		v /= base
+	}
+	return string(out)
+}
v2/builder/templates/templateV2025.go (1)

6-8: Confirm Go 1.25 toolchain availability for consumers.

The template pins go 1.25/toolchain 1.25.0. If your build fleet or users are on Go 1.24, template builds will fail. Consider downgrading to 1.24 until 1.25 is broadly available, or make this configurable.

Would you like me to make the Go version/toolchain templated off a CLI flag or RRModuleVersion?

v2/builder/options.go (2)

20-24: Defensive copy for plugins slice

Assigning the incoming slice directly ties Builder state to the caller’s backing array. A later append in caller scope can mutate Builder’s plugins unexpectedly. Make a shallow copy to decouple.

Apply this diff:

 func WithPlugins(plugins ...*plugin.Plugin) Option {
   return func(b *Builder) {
-    b.plugins = plugins
+    if len(plugins) == 0 {
+      b.plugins = nil
+      return
+    }
+    cp := make([]*plugin.Plugin, len(plugins))
+    copy(cp, plugins)
+    b.plugins = cp
   }
 }

47-52: Guard against nil logger usage downstream

Many call sites (e.g., Builder.Build, goBuildCmd) use b.log without nil checks. If WithLogger wasn’t provided, this will panic. Either default to zap.NewNop() in NewBuilder or add a setter ensuring a non-nil logger.

Would you like me to patch v2/builder/builder.go to default the logger to zap.NewNop() when not provided?

v2/builder/templates/template_test.go (1)

12-48: Add go.mod rendering tests to catch template/data mismatches

Given recent shape changes, add tests for:

  • CompileGoModTemplate2025: validate module line and require entries.
  • (If supported) CompileGoModTemplate2024.

This would have caught the mismatch between Template and templateV2025/V2024.

Want me to add those tests with realistic plugin require lines?

builder/builder.go (2)

279-284: Consolidate gcflags into a single argument

Passing -gcflags twice is unusual; use “all=-N -l” in one argument.

Apply this diff:

-// turn off optimizations
-buildCmdArgs = append(buildCmdArgs, "-gcflags", "-N")
-// turn off inlining
-buildCmdArgs = append(buildCmdArgs, "-gcflags", "-l")
+// turn off optimizations and inlining
+buildCmdArgs = append(buildCmdArgs, "-gcflags", "all=-N -l")

303-316: Guard GOPATH/GOCACHE composition when GOOS/GOARCH are empty

When not set, GOPATH/GOCACHE become “…/go//…”. Either default to runtime.GOOS/GOARCH or avoid embedding empty path components.

I can patch this to default GOOS/GOARCH from runtime if empty and ensure directories exist before build.

v2/github/github_test.go (2)

19-21: Use t.TempDir() for isolation and automatic cleanup

Using os.TempDir() can cause cross-test interference and leave artifacts behind. Prefer t.TempDir().

Apply this diff:

-	tmpDir := os.TempDir()
+	tmpDir := t.TempDir()

11-34: Gate network calls and add a minimal assertion

This test performs a live network call and always passes (no assertions). Gate it behind an env var or testing.Short() and assert at least the error type or result invariants to make it meaningful.

Example minimal changes (safe for CI):

 func TestGitHubClient_DownloadTemplate(t *testing.T) {
+	t.Parallel()
+	if os.Getenv("INTEGRATION_GITHUB") != "1" {
+		t.Skip("skipping integration test; set INTEGRATION_GITHUB=1 to run")
+	}
@@
-	// Basic assertions - just check if method executed without panicking
-	if err != nil {
-		t.Logf("DownloadTemplate returned error (expected for test without real token): %v", err)
-	}
-
-	if resultPath != "" {
-		t.Logf("DownloadTemplate returned path: %s", resultPath)
-	}
+	// Basic assertion
+	if err == nil && resultPath == "" {
+		t.Fatalf("expected non-empty resultPath when err is nil")
+	}
buf.gen.yaml (1)

6-10: Consider dropping grpc-go generation if you’re only serving Connect

If the codebase exclusively uses Connect handlers/clients, the grpc/go stubs may be unnecessary. Removing them reduces gen surface and dependency footprint. If you plan to support plain gRPC too, keep as is.

internal/cli/server/command.go (4)

24-26: Nit: Rename variable to reflect role (server/handler, not client)

This is a service implementation, not a client. A name like svc or buildSrv improves clarity.

-			client := NewBuildServer(zlog)
-			path, handler := servicev1.NewBuildServiceHandler(client)
+			svc := NewBuildServer(zlog)
+			path, handler := servicev1.NewBuildServiceHandler(svc)

27-33: Add server timeouts to mitigate slowloris and stuck connections

Only ReadHeaderTimeout is set. Add ReadTimeout, WriteTimeout, and IdleTimeout for better resilience.

 			server := &http.Server{
 				Addr: *address,
 				Handler: h2c.NewHandler(mux, &http2.Server{
 					MaxConcurrentStreams: 256,
 				}),
-				ReadHeaderTimeout: time.Minute,
+				// Tighten header timeout and add full connection management timeouts
+				ReadHeaderTimeout: 10 * time.Second,
+				ReadTimeout:       30 * time.Second,
+				WriteTimeout:      2 * time.Minute,
+				IdleTimeout:       2 * time.Minute,
 			}

20-44: Optional: Graceful shutdown via command context

Leverage cmd.Context() to shut down cleanly on cancellation (e.g., signals). This avoids relying solely on http.ErrServerClosed.

Example refactor (adds context import):

-		RunE: func(_ *cobra.Command, _ []string) error {
+		RunE: func(cmd *cobra.Command, _ []string) error {
+			ctx := cmd.Context()
@@
-			err := server.ListenAndServe()
-			if err != nil {
-				if errors.Is(err, http.ErrServerClosed) {
-					return nil
-				}
-
-				return err
-			}
-			return nil
+			errCh := make(chan error, 1)
+			go func() {
+				errCh <- server.ListenAndServe()
+			}()
+			select {
+			case <-ctx.Done():
+				shCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+				defer cancel()
+				_ = server.Shutdown(shCtx)
+				return nil
+			case err := <-errCh:
+				if errors.Is(err, http.ErrServerClosed) {
+					return nil
+				}
+				return err
+			}

Remember to add:

  • import "context"

29-31: Security note: h2c should be behind a trusted boundary

Serving HTTP/2 cleartext (h2c) is fine for local/dev. For production, terminate TLS at a proxy or use TLS directly to avoid downgrades and on-path interference.

builder/builder_test.go (4)

122-122: Use file-appropriate permissions for go.mod

0700 makes go.mod executable and unreadable to other users. Prefer 0644 for files.

-		_ = os.WriteFile(filepath.Join(v.Replace, goModStr), associated[v.ModuleName], rights)
+		_ = os.WriteFile(filepath.Join(v.Replace, goModStr), associated[v.ModuleName], 0o644)

85-91: Avoid mutating Builder internals; pass modules via constructor

You instantiate with an empty modules slice and then assign b.modules. Prefer passing modules to NewBuilder to mirror real usage and reduce test brittleness.

Sketch:

mods := []*velox.ModulesInfo{
  {Version: "master", ModuleName: "dummy_one_relative", Replace: "/tmp/dummy_one_relative"},
  // ...
}
b := NewBuilder("/tmp", mods, WithRRVersion(version), WithDebug(false), WithLogger(l))

76-91: Make test sandboxed: use t.TempDir and pass t to setup

Writing to hardcoded /tmp paths can collide across runs. Accept *testing.T in setup, and base on t.TempDir() for isolation and automatic cleanup.

Example:

func setup(t *testing.T, version string) *Builder {
  t.Helper()
  base := t.TempDir()
  // use filepath.Join(base, "dummy_one_relative") for Replace dirs
}

Then update callers to setup(t, "...") and drop clean().

Also applies to: 120-126


148-235: Parallelize independent tests for speed

These tests don’t share state when using t.TempDir; mark them parallel to reduce wall time.

 func Test_Builder_getDepsReplace_multipleAbsolute_V2024(t *testing.T) {
+	t.Parallel()
@@
 func Test_Builder_getDepsReplace_multipleRelative_V2024(t *testing.T) {
+	t.Parallel()
@@
 // ... apply similarly to the remaining test functions
internal/cli/build/build.go (4)

25-30: Use os.Getwd instead of syscall.Getwd

syscall.Getwd is low-level and unnecessary here.

-				wd, err := syscall.Getwd()
+				wd, err := os.Getwd()

47-51: Prefer returning errors from RunE instead of os.Exit for testability

Let Cobra handle exit codes. Returning errors improves composability and unit testing.

-			path, err := rp.DownloadTemplate(os.TempDir(), cfg.Roadrunner[ref])
-			if err != nil {
-				zlog.Error("downloading template", zap.Error(err))
-				os.Exit(1)
-			}
+			path, err := rp.DownloadTemplate(os.TempDir(), cfg.Roadrunner[ref])
+			if err != nil {
+				return fmt.Errorf("downloading template: %w", err)
+			}

Remember to:

  • add import "fmt"

53-57: Return error instead of exiting on GetPluginsModData failure

Same rationale as above.

-			pMod, err := rp.GetPluginsModData()
-			if err != nil {
-				zlog.Error("get plugins mod data", zap.Error(err))
-				os.Exit(1)
-			}
+			pMod, err := rp.GetPluginsModData()
+			if err != nil {
+				return fmt.Errorf("get plugins mod data: %w", err)
+			}

64-70: Return error instead of exiting on build failure

Same rationale as above.

-			err = builder.NewBuilder(path, pMod,
+			err = builder.NewBuilder(path, pMod,
 				builder.WithOutputDir(*out),
 				builder.WithRRVersion(cfg.Roadrunner[ref]),
 				builder.WithDebug(cfg.Debug.Enabled),
 				builder.WithLogger(zlog.Named("Builder")),
 			).Build(cfg.Roadrunner[ref])
 			if err != nil {
-				zlog.Error("fatal", zap.Error(err))
-				os.Exit(1)
+				return fmt.Errorf("build failed: %w", err)
 			}

(Remember import "fmt")

internal/cli/root.go (1)

35-38: Consider using a more specific check for the server command

Instead of checking if cmd.Use == "server", consider using cmd.Name() which is more idiomatic and reliable for command identification.

-		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
-			if cmd.Use == "server" {
-				return nil
-			}
+		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
+			if cmd.Name() == "server" {
+				return nil
+			}
v2/github/github.go (4)

102-144: Consider adding retry logic for network operations

The download operation could benefit from retry logic to handle transient network failures, especially for production use.

Would you like me to provide an implementation of retry logic with exponential backoff for the download operations?


137-141: Improve error handling for HTTP response body

The deferred close of the response body should check for errors.

-	_, err = io.Copy(buf, resp.Body)
-	_ = resp.Body.Close()
-	if err != nil {
-		return fmt.Errorf("failed to copy response body: %w", err)
-	}
+	_, err = io.Copy(buf, resp.Body)
+	closeErr := resp.Body.Close()
+	if err != nil {
+		return fmt.Errorf("failed to copy response body: %w", err)
+	}
+	if closeErr != nil {
+		return fmt.Errorf("failed to close response body: %w", closeErr)
+	}

248-250: Consider more robust commit SHA detection

The current SHA detection only checks for length 40, but partial SHAs (7-40 characters) are also valid. Consider using a hex pattern check.

-	// assume that we have a sha here
-	if len(rrVersion) == 40 {
-		return url.Parse(fmt.Sprintf("https://github.com/roadrunner-server/roadrunner/archive/%s.zip", rrVersion))
-	}
+	// assume that we have a sha here (7-40 hex characters)
+	if len(rrVersion) >= 7 && len(rrVersion) <= 40 && isHexString(rrVersion) {
+		return url.Parse(fmt.Sprintf("https://github.com/roadrunner-server/roadrunner/archive/%s.zip", rrVersion))
+	}

And add this helper function:

func isHexString(s string) bool {
	for _, c := range s {
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
			return false
		}
	}
	return true
}

282-283: Good security awareness with decompression bomb comment

The G110 comment correctly identifies the potential DoS risk. Consider implementing size limits or using a limited reader for additional protection.

Consider adding size validation:

const maxZipSize = 100 * 1024 * 1024 // 100MB limit

// Before extraction, check the uncompressed size
var totalSize int64
for _, f := range rc.File {
    totalSize += int64(f.UncompressedSize64)
}
if totalSize > maxZipSize {
    return fmt.Errorf("archive too large: %d bytes exceeds maximum of %d", totalSize, maxZipSize)
}
api/request/v1/request.proto (2)

29-29: Consider simplifying the regex pattern

The regex pattern has redundant escaping. In proto CEL expressions, you can use single backslashes.

-      expression: "this.matches('^(v\\\\d+\\\\.\\\\d+\\\\.\\\\d+.*|master|[a-f0-9]{7,40})$')"
+      expression: "this.matches('^(v\\d+\\.\\d+\\.\\d+.*|master|[a-f0-9]{7,40})$')"

32-35: Consider making target_platform required if it affects build output

If the target platform significantly affects the build output, consider making it required to avoid ambiguity about which platform was targeted.

-  Platform target_platform = 4 [(buf.validate.field).required = false];
+  Platform target_platform = 4 [(buf.validate.field).required = true];
v2/builder/builder.go (2)

235-246: Debug build flags could be consolidated

The debug gcflags are being set twice with different values, which may cause the second to override the first.

 	case true:
 		// debug flags
 		// turn off optimizations
-		buildCmdArgs = append(buildCmdArgs, "-gcflags", "-N")
-		// turn off inlining
-		buildCmdArgs = append(buildCmdArgs, "-gcflags", "-l")
+		// turn off optimizations and inlining
+		buildCmdArgs = append(buildCmdArgs, "-gcflags", "-N -l")
 		// build with debug tags
 		buildCmdArgs = append(buildCmdArgs, "-tags", "debug")

309-336: Consider using os.Rename for better performance

The current implementation reads the entire file into memory and writes it back. For large binaries, this could be inefficient.

 func moveFile(from, to string) error {
+	// Try rename first (atomic and efficient if on same filesystem)
+	err := os.Rename(from, to)
+	if err == nil {
+		return nil
+	}
+	
+	// Fall back to copy if rename fails (e.g., cross-device)
 	ffInfo, err := os.Stat(from)
 	if err != nil {
 		return err
 	}
k6-benchmark.js (5)

51-51: Exercise caching by default (force_rebuild=false).

With force_rebuild: true every request bypasses the 30-minute LRU cache, defeating one of the key PR goals. Recommend defaulting to false and toggling via an env var or scenario if you want to stress rebuilds.

-  force_rebuild: true,
+  force_rebuild: false,

221-229: Add a correctness check on the response payload (path present).

Validate that the service returns a JSON body with a non-empty "path". This guards against false positives where the server returns 200 with an error payload.

-  const isSuccess = check(response, {
+  // Parse JSON body (best-effort)
+  let respJson = null;
+  try {
+    respJson = response.json();
+  } catch (e) {
+    // ignore parse errors
+  }
+
+  const isSuccess = check(response, {
     "status is 200": (r) => r.status === 200,
     "response time < 30000ms": (r) => r.timings.duration < 30000,
     "response has body": (r) => r.body && r.body.length > 0,
     "content-type is application/json": (r) =>
       r.headers["Content-Type"] &&
       r.headers["Content-Type"].includes("application/json"),
+    "response has non-empty path": () =>
+      respJson && typeof respJson.path === "string" && respJson.path.length > 0,
   });

26-26: Fix misleading comment (duration is 60s, not 30s).

-      duration: "60s", // for 30 seconds
+      duration: "60s", // for 60 seconds

42-42: Fix misleading comment (threshold is 20s, not 5s).

-    http_req_duration: ["p(95)<20000"], // 95% of requests must complete below 5s
+    http_req_duration: ["p(95)<20000"], // 95% of requests must complete below 20s

217-217: Fix misleading comment (timeout is 120s).

-      timeout: "120s", // Allow up to 60 seconds for build operations
+      timeout: "120s", // Allow up to 120 seconds for build operations
internal/cli/server/server.go (1)

44-57: Optional: remove redundant file deletion on cache eviction.

You already remove the build directory (os.TempDir()/hash). Deleting the binary path first is redundant since it resides within that directory.

-			err := os.RemoveAll(rrBinPath)
-			if err != nil {
-				log.Error("removing cached file", zap.String("path", rrBinPath), zap.Error(err))
-			}
-			// remove path
-			log.Info("removing cached directory", zap.String("path", filepath.Join(os.TempDir(), hash)))
-			err = os.RemoveAll(filepath.Join(os.TempDir(), hash))
+			log.Info("removing cached directory", zap.String("path", filepath.Join(os.TempDir(), hash)))
+			err := os.RemoveAll(filepath.Join(os.TempDir(), hash))
 			if err != nil {
 				log.Error("failed to remove directory", zap.String("path", filepath.Join(os.TempDir(), hash)), zap.Error(err))
 			}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ce4ac5d and 5c5ea99.

⛔ Files ignored due to path filters (7)
  • buf.lock is excluded by !**/*.lock
  • gen/go/api/request/v1/request.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • gen/go/api/response/v1/response.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • gen/go/api/service/v1/service.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • gen/go/api/service/v1/serviceV1connect/service.connect.go is excluded by !**/gen/**
  • gen/go/api/service/v1/service_grpc.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (33)
  • .claude/CLAUDE.md (1 hunks)
  • .gitignore (1 hunks)
  • Makefile (1 hunks)
  • api/request/v1/request.proto (1 hunks)
  • api/response/v1/response.proto (1 hunks)
  • api/service/v1/service.proto (1 hunks)
  • buf.gen.yaml (1 hunks)
  • buf.yaml (1 hunks)
  • builder/builder.go (9 hunks)
  • builder/builder_test.go (3 hunks)
  • builder/options.go (1 hunks)
  • builder/templates/templateV2025.go (0 hunks)
  • config.go (1 hunks)
  • github/pool.go (2 hunks)
  • github/repo.go (2 hunks)
  • gitlab/repo.go (2 hunks)
  • go.mod (2 hunks)
  • internal/cli/build/build.go (1 hunks)
  • internal/cli/root.go (4 hunks)
  • internal/cli/server/command.go (1 hunks)
  • internal/cli/server/server.go (1 hunks)
  • k6-benchmark.js (1 hunks)
  • v2/builder/builder.go (1 hunks)
  • v2/builder/options.go (1 hunks)
  • v2/builder/templates/compile.go (1 hunks)
  • v2/builder/templates/templateV2024.go (1 hunks)
  • v2/builder/templates/templateV2025.go (1 hunks)
  • v2/builder/templates/template_test.go (1 hunks)
  • v2/cache/cache.go (1 hunks)
  • v2/config/builder.go (1 hunks)
  • v2/github/github.go (1 hunks)
  • v2/github/github_test.go (1 hunks)
  • v2/plugin/plugin.go (1 hunks)
💤 Files with no reviewable changes (1)
  • builder/templates/templateV2025.go
🧰 Additional context used
🧬 Code Graph Analysis (14)
builder/options.go (1)
builder/builder.go (1)
  • Builder (36-48)
v2/github/github_test.go (2)
v2/cache/cache.go (1)
  • NewRRCache (14-19)
v2/github/github.go (1)
  • NewHTTPClient (42-67)
v2/builder/options.go (4)
v2/builder/builder.go (1)
  • Builder (32-44)
builder/builder.go (1)
  • Builder (36-48)
gen/go/api/request/v1/request.pb.go (3)
  • Plugin (160-168)
  • Plugin (181-181)
  • Plugin (196-198)
v2/plugin/plugin.go (1)
  • Plugin (13-17)
v2/builder/templates/template_test.go (2)
v2/plugin/plugin.go (1)
  • NewPlugin (23-29)
v2/builder/templates/compile.go (1)
  • NewTemplate (22-37)
internal/cli/server/command.go (2)
internal/cli/server/server.go (1)
  • NewBuildServer (41-64)
gen/go/api/service/v1/serviceV1connect/service.connect.go (1)
  • NewBuildServiceHandler (85-99)
v2/builder/builder.go (5)
builder/builder.go (1)
  • Builder (36-48)
v2/plugin/plugin.go (1)
  • Plugin (13-17)
v2/builder/options.go (1)
  • Option (11-11)
config.go (2)
  • V2025 (13-13)
  • V2024 (14-14)
builder/templates/templateV2025.go (1)
  • CompileGoModTemplate2025 (71-78)
v2/builder/templates/compile.go (5)
gen/go/api/request/v1/request.pb.go (3)
  • Plugin (160-168)
  • Plugin (181-181)
  • Plugin (196-198)
v2/plugin/plugin.go (1)
  • Plugin (13-17)
builder/templates/templateV2025.go (2)
  • GoModTemplateV2025 (8-43)
  • PluginsTemplateV2025 (45-68)
v2/builder/templates/templateV2025.go (2)
  • GoModTemplateV2025 (3-33)
  • PluginsTemplateV2025 (35-59)
v2/builder/templates/templateV2024.go (2)
  • GoModTemplateV2024 (3-34)
  • PluginsTemplateV2024 (36-58)
internal/cli/server/server.go (9)
v2/cache/cache.go (1)
  • NewRRCache (14-19)
gen/go/api/request/v1/request.pb.go (6)
  • BuildRequest (79-93)
  • BuildRequest (106-106)
  • BuildRequest (121-123)
  • Plugin (160-168)
  • Plugin (181-181)
  • Plugin (196-198)
gen/go/api/response/v1/response.pb.go (3)
  • BuildResponse (24-30)
  • BuildResponse (43-43)
  • BuildResponse (58-60)
v2/builder/builder.go (2)
  • Builder (32-44)
  • NewBuilder (47-58)
builder/builder.go (2)
  • Builder (36-48)
  • NewBuilder (51-63)
v2/plugin/plugin.go (2)
  • Plugin (13-17)
  • NewPlugin (23-29)
v2/github/github.go (1)
  • NewHTTPClient (42-67)
v2/builder/options.go (8)
  • Option (11-11)
  • WithPlugins (20-24)
  • WithOutputDir (55-59)
  • WithRRVersion (62-66)
  • WithLogs (41-45)
  • WithLogger (48-52)
  • WithGOOS (27-31)
  • WithGOARCH (34-38)
builder/options.go (7)
  • Option (10-10)
  • WithOutputDir (48-52)
  • WithRRVersion (55-59)
  • WithLogs (34-38)
  • WithLogger (41-45)
  • WithGOOS (20-24)
  • WithGOARCH (27-31)
internal/cli/root.go (3)
config.go (1)
  • Config (33-44)
internal/cli/build/build.go (1)
  • BindCommand (19-79)
internal/cli/server/command.go (1)
  • BindCommand (16-45)
internal/cli/build/build.go (3)
builder/builder.go (1)
  • NewBuilder (51-63)
builder/options.go (4)
  • WithOutputDir (48-52)
  • WithRRVersion (55-59)
  • WithDebug (13-17)
  • WithLogger (41-45)
config.go (1)
  • Debug (46-48)
v2/github/github.go (2)
config.go (2)
  • Token (50-52)
  • Debug (46-48)
logger/logger.go (1)
  • Mode (13-13)
builder/builder_test.go (3)
builder/builder.go (1)
  • NewBuilder (51-63)
modulesInfo.go (1)
  • ModulesInfo (13-22)
builder/options.go (3)
  • WithRRVersion (55-59)
  • WithDebug (13-17)
  • WithLogger (41-45)
v2/plugin/plugin.go (1)
gen/go/api/request/v1/request.pb.go (3)
  • Plugin (160-168)
  • Plugin (181-181)
  • Plugin (196-198)
builder/builder.go (4)
modulesInfo.go (1)
  • ModulesInfo (13-22)
v2/builder/builder.go (2)
  • Builder (32-44)
  • NewBuilder (47-58)
v2/builder/options.go (1)
  • Option (11-11)
builder/options.go (1)
  • Option (10-10)
🪛 LanguageTool
.claude/CLAUDE.md

[grammar] ~5-~5: There might be a mistake here.
Context: ...iew Velox is an automated build system for RoadRunner server and its plugins. It's...

(QB_NEW_EN)


[grammar] ~9-~9: There might be a mistake here.
Context: ...ilds. Key Technologies: - Go 1.24+ - Protocol Buffers (protobuf) with buf - G...

(QB_NEW_EN)


[grammar] ~10-~10: There might be a mistake here.
Context: ...+ - Protocol Buffers (protobuf) with buf - GitHub/GitLab API integration - gRPC and...

(QB_NEW_EN)


[grammar] ~11-~11: There might be a mistake here.
Context: ...with buf - GitHub/GitLab API integration - gRPC and Connect - Cobra CLI framework ...

(QB_NEW_EN)


[grammar] ~12-~12: There might be a mistake here.
Context: ...itLab API integration - gRPC and Connect - Cobra CLI framework ## Repository Struc...

(QB_NEW_EN)


[grammar] ~131-~131: There might be a mistake here.
Context: ...tiquette ### Branching - Main branch: master - Feature branches: feature/description ...

(QB_NEW_EN)


[grammar] ~132-~132: There might be a mistake here.
Context: ...in branch: master - Feature branches: feature/description - Use conventional commit messages ### Pl...

(QB_NEW_EN)


[grammar] ~139-~139: There might be a mistake here.
Context: ...mportant Plugin Guidelines:** - Do not use plugin's master branch - Use tags wit...

(QB_NEW_EN)


[grammar] ~154-~154: There might be a mistake here.
Context: ...isites - Go 1.24+ (toolchain: go1.24.0) - buf CLI for protocol buffer generation -...

(QB_NEW_EN)


[grammar] ~155-~155: There might be a mistake here.
Context: ...- buf CLI for protocol buffer generation - Git for version control ### Setup Steps...

(QB_NEW_EN)


[grammar] ~199-~199: There might be a mistake here.
Context: ...ld Templates 1. Create new template in builder/templates/ 2. Update builder.go to reference new tem...

(QB_NEW_EN)


[grammar] ~200-~200: There might be a mistake here.
Context: ...r/templates/2. Updatebuilder.go` to reference new template 3. Add corresponding tests...

(QB_NEW_EN)


[grammar] ~211-~211: There might be a mistake here.
Context: ...LI Commands - Follow cobra patterns in internal/cli/ - Use persistent flags for common options ...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
.claude/CLAUDE.md

17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (19)
.gitignore (1)

8-8: LGTM: Ignore .DS_Store

Good addition; this will keep macOS metadata out of the repo.

github/repo.go (2)

84-85: OS-aware path join: good change.

Switching to filepath.Join is correct for cross-platform path handling.


15-19: Verified GetArchiveLink Signature

The Repositories.GetArchiveLink method in go-github/v74 indeed has the signature:

GetArchiveLink(ctx context.Context, owner, repo string, archiveformat ArchiveFormat, opts *RepositoryContentGetOptions, maxRedirects int) (*url.URL, *Response, error)

The last parameter is maxRedirects int, so passing 10 is correct. No changes needed.

config.go (1)

20-32: Double-check token optionality vs. Validate() usage in server paths.

PR objectives state GitHub token is optional for downloading RR base templates, but Validate() (invoked by some flows) hard-requires github.token. Ensure server paths that only need base templates don’t fail validation or consider relaxing checks when no plugins are specified.

Would you like a targeted patch that:

  • Requires token only if GitHub/GitLab plugins are configured, and
  • Allows base template downloads without a token?

I can prepare it once you confirm where Validate() is used in the server setup.

go.mod (1)

28-29: Replace mapping is correct; no changes needed

The replace github.com/roadrunner-server/velox/v2025/gen => ./gen directive applies to all subpackages under gen (including gen/go/...), so imports like

"github.com/roadrunner-server/velox/v2025/gen/go/api/request/v1"

will be resolved to ./gen/go/api/request/v1 on disk. There is intentionally no gen/go/go.mod, since these packages live under the root module once replaced.

No adjustments to the replace directive or additional go.mod files are required.

v2/builder/options.go (1)

1-67: Solid functional-options setup

The options API is cohesive and straightforward. Constructors are small, side-effect free, and map cleanly to Builder fields.

v2/builder/templates/compile.go (1)

39-46: Adapt 2025 go.mod execution to expected data shape

Bridge from Template.Requires (strings like "module version") to the GoModTemplateV2025’s expected .Entries at execution time.

[ suggest_essential_refactor ]
Apply this diff:

 func CompileGoModTemplate2025(wr io.Writer, t *Template) error {
-  tmpl, err := template.New("go.mod").Parse(GoModTemplateV2025)
+  tmpl, err := template.New("go.mod").Parse(GoModTemplateV2025)
   if err != nil {
     return err
   }
 
-  return tmpl.Execute(wr, t)
+  // Build view model expected by the template:
+  //   { RRModuleVersion string; Entries []struct{ Module, PseudoVersion, Replace string } }
+  type entry struct {
+    Module        string
+    PseudoVersion string
+    Replace       string
+  }
+  vm := struct {
+    RRModuleVersion string
+    Entries         []entry
+  }{
+    RRModuleVersion: t.RRModuleVersion,
+    Entries:         make([]entry, 0, len(t.Requires)),
+  }
+  for _, req := range t.Requires {
+    // Expect format: "<module> <version>"
+    parts := strings.Fields(req)
+    if len(parts) >= 2 {
+      vm.Entries = append(vm.Entries, entry{Module: parts[0], PseudoVersion: parts[1]})
+    }
+  }
+  return tmpl.Execute(wr, vm)
 }
v2/builder/templates/template_test.go (1)

12-48: Good coverage of v2025 plugins template generation

The test covers core expectations (package, bundled imports, bundled plugin inits, and dynamic plugin rendering).

builder/builder.go (1)

235-241: Potential nil deref on logger in Write and elsewhere

Write (and other methods) call b.log.* unconditionally. If WithLogger isn’t supplied, this will panic.

Do all builder paths guarantee a non-nil logger? If not, set a default (zap.NewNop()) in NewBuilder.

api/service/v1/service.proto (1)

10-12: Service definition looks good

Simple, clear RPC with correct package wiring and go_package option.

buf.gen.yaml (1)

1-15: LGTM: Buf v2 plugins and options look consistent

Pinned plugin versions, consistent source_relative opts, and a single gen/go out dir are all good.

internal/cli/root.go (1)

85-86: Pass config safely to build command

Since build.BindCommand receives a pointer to a pointer (*velox.Config), ensure proper initialization as mentioned above to prevent issues.

v2/github/github.go (1)

71-100: LGTM! Well-structured template download logic with proper caching

The caching implementation is well done - using the version as the key and properly managing the buffer lifecycle with defer rrbuf.Reset().

api/request/v1/request.proto (1)

1-8: LGTM! Proper proto3 syntax and package structure

The proto file follows best practices with proper syntax declaration, package naming, and Go package path configuration.

builder/options.go (1)

1-60: LGTM! Clean implementation of the functional options pattern

The options are well-documented and follow Go best practices for the functional options pattern. This provides a flexible and extensible API for configuring the Builder.

v2/builder/builder.go (1)

62-64: LGTM! Good validation for required plugins

The early validation ensures that the builder has plugins before proceeding with the build.

internal/cli/server/server.go (3)

88-94: LGTM: cache hit path and logs behavior.

Returning the cached path and a short logs notice when ForceRebuild is false is appropriate and keeps responses lightweight.


66-72: LGTM: request validation via protovalidate.

Good use of protovalidate to fail fast with CodeInvalidArgument on malformed inputs.


108-114: LGTM: GitHub client and template retrieval.

Token-optional HTTP client with an RR in-memory cache is a solid approach to reduce rate-limit issues.

Signed-off-by: Valery Piashchynski <[email protected]>
Signed-off-by: Valery Piashchynski <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
internal/cli/server/server.go (2)

152-173: Make cache key stable: exclude request_id/force_rebuild, canonicalize plugins, default target platform; never return empty key

Including RequestId breaks caching and the in-progress guard; plugin order variance and a nil target_platform also produce spurious misses. Returning "" on marshal failure collapses different requests into the same bucket.

Refactor to build a canonical request copy, sort plugins, default platform to host if nil, and fall back to a string concat if marshaling fails.

Apply this diff:

-func (b *BuildServer) generateCacheHash(req *connect.Request[requestV1.BuildRequest]) string {
-	cacheReq := &requestV1.BuildRequest{
-		RequestId:      req.Msg.GetRequestId(),
-		RrVersion:      req.Msg.GetRrVersion(),
-		TargetPlatform: req.Msg.GetTargetPlatform(),
-		Plugins:        req.Msg.GetPlugins(),
-	}
-
-	data, err := proto.MarshalOptions{
-		Deterministic: true,
-		AllowPartial:  false,
-	}.Marshal(cacheReq)
-	if err != nil {
-		// TODO: might be just fail processing?
-		b.log.Error("marshaling cache key error, cache creation would be skipped", zap.Error(err))
-		return ""
-	}
-
-	h := fnv.New64a()
-	h.Write(data)
-	return strconv.FormatUint(h.Sum64(), 16)
-}
+func (b *BuildServer) generateCacheHash(req *connect.Request[requestV1.BuildRequest]) string {
+	// Build a deterministic, canonical copy of the request.
+	// - Excludes RequestId and ForceRebuild as they should not affect the artifact.
+	// - Defaults target platform to host if unspecified.
+	// - Canonicalizes plugin ordering to avoid order-dependent keys.
+	tp := req.Msg.GetTargetPlatform()
+	if tp == nil {
+		tp = &requestV1.Platform{
+			Os:   runtime.GOOS,
+			Arch: runtime.GOARCH,
+		}
+	}
+
+	cacheReq := &requestV1.BuildRequest{
+		RrVersion:      req.Msg.GetRrVersion(),
+		TargetPlatform: tp,
+		Plugins:        nil,
+	}
+	for _, p := range req.Msg.GetPlugins() {
+		if p == nil {
+			continue
+		}
+		cacheReq.Plugins = append(cacheReq.Plugins, &requestV1.Plugin{
+			ModuleName: p.GetModuleName(),
+			Tag:        p.GetTag(),
+		})
+	}
+	sort.Slice(cacheReq.Plugins, func(i, j int) bool {
+		if cacheReq.Plugins[i].GetModuleName() == cacheReq.Plugins[j].GetModuleName() {
+			return cacheReq.Plugins[i].GetTag() < cacheReq.Plugins[j].GetTag()
+		}
+		return cacheReq.Plugins[i].GetModuleName() < cacheReq.Plugins[j].GetModuleName()
+	})
+
+	data, err := proto.MarshalOptions{Deterministic: true, AllowPartial: false}.Marshal(cacheReq)
+	if err != nil {
+		// Fallback to a plain string concat to avoid disabling caching entirely.
+		b.log.Error("marshaling cache key failed; falling back to string concat", zap.Error(err))
+		var sb strings.Builder
+		sb.WriteString(cacheReq.GetRrVersion())
+		if tp := cacheReq.GetTargetPlatform(); tp != nil {
+			sb.WriteString(tp.GetOs())
+			sb.WriteString(tp.GetArch())
+		}
+		for _, p := range cacheReq.Plugins {
+			sb.WriteString(p.GetModuleName())
+			sb.WriteString(p.GetTag())
+		}
+		h := fnv.New64a()
+		h.Write([]byte(sb.String()))
+		return strconv.FormatUint(h.Sum64(), 16)
+	}
+
+	h := fnv.New64a()
+	h.Write(data)
+	return strconv.FormatUint(h.Sum64(), 16)
+}

3-27: Add missing import for sorting plugins in cache key

Required for the canonicalization in generateCacheHash.

-	"runtime"
+	"runtime"
+	"sort"
v2/builder/builder.go (2)

172-181: Fix incorrect os.IsExist usage after os.Stat.

The error check pattern is incorrect. After os.Stat(), you should check err == nil to determine if the directory exists, not os.IsExist(err).

-	if os.IsExist(err) {
-		b.log.Info("output path already exists, cleaning up", zap.String("dir", b.outputDir))
-		_ = os.RemoveAll(b.outputDir)
-	}
+	if err == nil {
+		// Directory exists, clean it up
+		b.log.Info("output path already exists, cleaning up", zap.String("dir", b.outputDir))
+		_ = os.RemoveAll(b.outputDir)
+	}

237-252: Fix duplicate -ldflags argument.

You're appending "-ldflags" twice which could cause command parsing issues. The ldflags should be combined into a single argument.

	case false:
-		buildCmdArgs = append(buildCmdArgs, "-ldflags", "-s")
+		// Strip symbols for release builds, combine with version flags
+		ldFlags := fmt.Sprintf("-s %s", fmt.Sprintf(ldflags, b.rrVersion, time.Now().UTC().Format(time.RFC3339)))
+		buildCmdArgs = append(buildCmdArgs, "-ldflags", ldFlags)
	}

-	// LDFLAGS for version and build time, always appended
-	buildCmdArgs = append(buildCmdArgs, "-ldflags")
-	buildCmdArgs = append(buildCmdArgs, fmt.Sprintf(ldflags, b.rrVersion, time.Now().UTC().Format(time.RFC3339)))
+	// For debug builds, add version flags separately
+	if b.debug {
+		buildCmdArgs = append(buildCmdArgs, "-ldflags")
+		buildCmdArgs = append(buildCmdArgs, fmt.Sprintf(ldflags, b.rrVersion, time.Now().UTC().Format(time.RFC3339)))
+	}
🧹 Nitpick comments (8)
internal/cli/server/server.go (6)

77-88: Non-atomic Contains/Add on currentlyProcessing; window for duplicate builds

Contains followed by Add is a TOCTOU pattern. Two concurrent requests for the same key can pass Contains before either Add executes and both proceed to build.

Consider one of:

  • Use a singleflight.Group to collapse concurrent builds of the same key and return the first result to followers.
  • Or switch to an atomic ContainsOrAdd (if available in the chosen LRU), or replace with a sync.Map and LoadOrStore to ensure atomicity.
  • As a minimal mitigation, guard with a separate mutex around the check-add pair.

Do you want a targeted patch using singleflight?


59-63: Guard TTL may expire mid-build; prefer manual removal or longer TTL

currentlyProcessing uses a 5m TTL but you also remove the key with defer. If a build exceeds 5m, the guard can expire and allow a duplicate to start.

  • Remove TTL (if supported) and rely on explicit Remove in defer.
  • Or increase TTL beyond the maximum expected build duration.

67-73: Clarify validation vs. defaulting semantics for target_platform

You default target_platform later, but validate before defaulting. If protovalidate requires target_platform, requests relying on the “host default” will be rejected.

Decide one of:

  • If host default is intended: default target_platform before validation, or relax the validator to make it optional.
  • If it’s required: remove the defaulting block and update docs accordingly.

Want me to patch the defaulting to occur before Validate?


137-141: Clean up partial output on build failure

On error, the output directory may contain partial artifacts and wastes disk.

 	binaryPath, err := builder.NewBuilder(path, opts...).Build(req.Msg.GetRrVersion())
 	if err != nil {
-		b.log.Error("fatal", zap.Error(err))
+		b.log.Error("build failed", zap.Error(err))
+		// cleanup partial output on failure
+		if rmErr := os.RemoveAll(outputPath); rmErr != nil {
+			b.log.Warn("failed to cleanup output directory", zap.String("path", outputPath), zap.Error(rmErr))
+		}
 		return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("building plugins: %w", err))
 	}

45-58: Eviction cleanup: remove the directory once; drop redundant file removal

Removing rrBinPath and then the directory results in two RemoveAll calls. Removing the directory alone is sufficient and reduces syscalls/log noise.

-			err := os.RemoveAll(rrBinPath)
-			if err != nil {
-				log.Error("removing cached file", zap.String("path", rrBinPath), zap.Error(err))
-			}
-			// remove path
-			log.Info("removing cached directory", zap.String("path", filepath.Join(os.TempDir(), hash)))
-			err = os.RemoveAll(filepath.Join(os.TempDir(), hash))
+			// Remove the whole temp directory for this build (it contains the binary).
+			dir := filepath.Join(os.TempDir(), hash)
+			log.Info("removing cached directory", zap.String("path", dir))
+			err := os.RemoveAll(dir)
 			if err != nil {
-				log.Error("failed to remove directory", zap.String("path", filepath.Join(os.TempDir(), hash)), zap.Error(err))
+				log.Error("failed to remove directory", zap.String("path", dir), zap.Error(err))
 			}

143-149: Returning a server-local filesystem path is not useful for remote clients

Remote callers can’t read a local path on the server. Consider:

  • Streaming the artifact bytes in a separate Download RPC or via HTTP GET with auth.
  • Returning a pre-signed URL or content-addressed link to an object store.
  • Or embedding the artifact in the Build response as bytes (with size limits), if acceptable.
v2/builder/builder.go (2)

276-277: Consider making GOPATH and GOCACHE configurable.

The hardcoded paths might not work well in all environments. Consider making these configurable through builder options.

+	// Use configurable paths or fallback to defaults
+	gopath := filepath.Join(hd, "go", b.goos, b.goarch)
+	gocache := filepath.Join(hd, "go", b.goos, b.goarch, "go-build")
+	
+	// These could be made configurable via options in the future
	cmd.Env = append(cmd.Env, fmt.Sprintf("GOPATH=%s", filepath.Join(hd, "go", b.goos, b.goarch)))
	cmd.Env = append(cmd.Env, fmt.Sprintf("GOCACHE=%s", filepath.Join(hd, "go", b.goos, b.goarch, "go-build")))

311-338: Consider using os.Rename for better performance.

The moveFile function reads the entire file into memory and recreates it. For better performance, consider using os.Rename first, falling back to copy-and-delete only when moving across filesystems.

func moveFile(from, to string) error {
+	// Try rename first (fast path for same filesystem)
+	if err := os.Rename(from, to); err == nil {
+		return nil
+	}
+	
+	// Fallback to copy-and-delete for cross-filesystem moves
	ffInfo, err := os.Stat(from)
	if err != nil {
		return err
	}

	fFile, err := os.ReadFile(from)
	if err != nil {
		return err
	}

	toFile, err := os.Create(to)
	if err != nil {
		return err
	}

	err = toFile.Chmod(ffInfo.Mode())
	if err != nil {
		return err
	}

	_, err = toFile.Write(fFile)
	if err != nil {
		return err
	}

-	return toFile.Close()
+	err = toFile.Close()
+	if err != nil {
+		return err
+	}
+	
+	// Remove original file after successful copy
+	return os.Remove(from)
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5c5ea99 and 6f483a5.

📒 Files selected for processing (2)
  • internal/cli/server/server.go (1 hunks)
  • v2/builder/builder.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
internal/cli/server/server.go (8)
v2/cache/cache.go (1)
  • NewRRCache (14-19)
gen/go/api/request/v1/request.pb.go (9)
  • BuildRequest (79-93)
  • BuildRequest (106-106)
  • BuildRequest (121-123)
  • Plugin (160-168)
  • Plugin (181-181)
  • Plugin (196-198)
  • Platform (25-33)
  • Platform (46-46)
  • Platform (61-63)
gen/go/api/response/v1/response.pb.go (3)
  • BuildResponse (24-30)
  • BuildResponse (43-43)
  • BuildResponse (58-60)
v2/builder/builder.go (2)
  • Builder (32-44)
  • NewBuilder (47-58)
builder/builder.go (2)
  • Builder (36-48)
  • NewBuilder (51-63)
v2/plugin/plugin.go (2)
  • Plugin (13-17)
  • NewPlugin (23-29)
v2/github/github.go (1)
  • NewHTTPClient (42-67)
builder/options.go (5)
  • Option (10-10)
  • WithOutputDir (48-52)
  • WithRRVersion (55-59)
  • WithGOOS (20-24)
  • WithGOARCH (27-31)
v2/builder/builder.go (6)
builder/builder.go (1)
  • Builder (36-48)
v2/plugin/plugin.go (1)
  • Plugin (13-17)
v2/builder/options.go (1)
  • Option (11-11)
v2/builder/templates/compile.go (5)
  • NewTemplate (22-37)
  • CompileTemplateV2025 (48-60)
  • CompileTemplateV2024 (76-88)
  • CompileGoModTemplate2025 (39-46)
  • CompileGoModTemplate2024 (62-74)
config.go (3)
  • V2025 (13-13)
  • V2024 (14-14)
  • Debug (46-48)
builder/templates/templateV2025.go (1)
  • CompileGoModTemplate2025 (71-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build (Go stable, OS ubuntu-latest)
🔇 Additional comments (6)
v2/builder/builder.go (6)

32-44: Consider removing modules field from struct.

The struct contains a modules field that's referenced in the AI summary but doesn't appear in the actual struct definition, suggesting this field may have been refactored out. The struct is well-designed with clear separation of concerns.


46-58: Constructor follows Go best practices.

The constructor implementation is clean and follows Go conventions with proper options pattern application.


61-64: Good validation of required plugins.

Early validation ensures plugins are provided before attempting to build, preventing wasted work.


98-110: Cleanup logic is well-structured.

The defer cleanup with proper error handling and logging provides good housekeeping.


214-227: Robust version validation logic.

The version validation handles both "master" branch and semantic versioning correctly, providing good backwards compatibility.


205-212: Well-designed Write method for output capturing.

The Write method properly implements io.Writer interface and handles both logging and optional string builder capture.

Signed-off-by: Valery Piashchynski <[email protected]>
Signed-off-by: Valery Piashchynski <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (4)
k6-benchmark.js (2)

52-55: LGTM: correct field name target_platform (matches proto).

This fixes the earlier build_platform mismatch and should pass proto validation.

Also applies to: 206-210


235-243: Prevent crash when logging failed responses with empty/non-string bodies.

response.body.substring(...) will throw if body is empty or not a string, skewing results.

Apply:

-  if (!isSuccess) {
-    console.log(`Request failed:
-      Status: ${response.status}
-      Duration: ${response.timings.duration}ms
-      Body: ${response.body.substring(0, 200)}...
-      Request ID: ${uniquePayload.request_id}
-      Platform: ${randomPlatform.os} + ${randomPlatform.arch}
-    `);
-  }
+  if (!isSuccess) {
+    const bodyPreview =
+      typeof response.body === "string" ? response.body.substring(0, 200) : "<empty>";
+    console.log(`Request failed:
+      Status: ${response.status}
+      Duration: ${response.timings.duration}ms
+      Body: ${bodyPreview}...
+      Request ID: ${uniquePayload.request_id}
+      Platform: ${randomPlatform.os} + ${randomPlatform.arch}
+    `);
+  }
v2/github/github.go (2)

21-25: Introduce a sane per-file extraction limit to mitigate decompression bombs

Add a max per-file extraction size constant. This will be used in extract() to guard against zip bombs.

 const (
   rrOwner string = "roadrunner-server"
   rrRepo  string = "roadrunner"
   zipExt  string = ".zip"
+  // Guard against decompression bombs; tune as appropriate for your use case.
+  maxFileExtractSize int64 = 100 << 20 // 100 MiB
 )

251-288: Harden extraction: ensure parent dirs, forbid symlinks, limit decompressed size

Current extraction can fail if directory entries are missing, is vulnerable to zip bombs, and allows symlinks. Harden it:

  • Ensure parent directories exist for file entries.
  • Reject symlink entries.
  • Enforce a per-file decompressed size limit using both header size and an io.LimitedReader.
 func extract(dest string, zf *zip.File) error {
   pt := filepath.Join(dest, zf.Name) //nolint:gosec
 
   if !strings.HasPrefix(pt, filepath.Clean(dest)+string(os.PathSeparator)) {
     return fmt.Errorf("invalid file path: %s", pt)
   }
 
+  // Disallow symlinks
+  if zf.FileInfo().Mode()&os.ModeSymlink != 0 {
+    return fmt.Errorf("symlink entries are not allowed: %s", zf.Name)
+  }
+
   if zf.FileInfo().IsDir() {
     err := os.MkdirAll(pt, os.ModePerm)
     if err != nil {
       return err
     }
     return nil
   }
 
-  destFile, err := os.OpenFile(pt, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zf.Mode())
+  // Ensure parent directory exists (some ZIPs may omit explicit dir entries)
+  if err := os.MkdirAll(filepath.Dir(pt), os.ModePerm); err != nil {
+    return err
+  }
+
+  destFile, err := os.OpenFile(pt, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zf.Mode())
   if err != nil {
     return err
   }
 
   zippedFile, err := zf.Open()
   if err != nil {
     _ = destFile.Close()
     return err
   }
 
-  // G110: Potential DoS vulnerability via decompression bomb
-  _, err = io.Copy(destFile, zippedFile) //nolint:gosec
+  // Guard against decompression bombs
+  if zf.UncompressedSize64 > uint64(maxFileExtractSize) {
+    _ = destFile.Close()
+    _ = zippedFile.Close()
+    return fmt.Errorf("file too large after decompression: %s (%d bytes)", zf.Name, zf.UncompressedSize64)
+  }
+  lr := &io.LimitedReader{R: zippedFile, N: maxFileExtractSize}
+  _, err = io.Copy(destFile, lr)
   if err != nil {
     _ = destFile.Close()
     _ = zippedFile.Close()
     return err
   }
+  if lr.N == 0 {
+    _ = destFile.Close()
+    _ = zippedFile.Close()
+    return fmt.Errorf("file exceeds max allowed extracted size: %s", zf.Name)
+  }
 
   _ = destFile.Close()
   _ = zippedFile.Close()
   return nil
 }
🧹 Nitpick comments (10)
k6-benchmark.js (6)

26-26: Fix misleading comments (60s vs 30s / 120s).

Comments contradict the actual values; align them to avoid confusion.

-      duration: "60s", // for 30 seconds
+      duration: "60s", // for 60 seconds
-      timeout: "120s", // Allow up to 60 seconds for build operations
+      timeout: "120s", // Allow up to 120 seconds for build operations

Also applies to: 217-217


1-5: Parameterize target URL/timeout via env and tag requests by platform.

Improves portability (e.g., remote targets, CI) and observability (per-OS/arch stats).

Add base configuration:

 import { uuidv4 } from "https://jslib.k6.io/k6-utils/1.4.0/index.js";
 
+// Base configuration (overridable via environment)
+const BASE_URL = __ENV.BASE_URL || "http://127.0.0.1:9000";
+const BUILD_PATH = "/api.service.v1.BuildService/Build";
+const HTTP_TIMEOUT = __ENV.HTTP_TIMEOUT || "120s";

Use it and add tags:

-  const response = http.post(
-    "http://127.0.0.1:9000/api.service.v1.BuildService/Build",
+  const response = http.post(
+    `${BASE_URL}${BUILD_PATH}`,
     JSON.stringify(uniquePayload),
     {
       headers: headers,
-      timeout: "120s", // Allow up to 60 seconds for build operations
+      timeout: HTTP_TIMEOUT, // Configurable via HTTP_TIMEOUT env var
+      tags: {
+        os: randomPlatform.os,
+        arch: randomPlatform.arch,
+      },
     },
   );

Update setup log:

-  console.log(
-    "Target: http://127.0.0.1:9000/api.service.v1.BuildService/Build",
-  );
+  console.log(`Target: ${BASE_URL}${BUILD_PATH}`);

Also applies to: 212-219, 249-251


226-229: Make Content-Type check robust (case-insensitive, support Connect JSON).

Connect endpoints may respond with application/connect+json; also header casing can vary.

-    "content-type is application/json": (r) =>
-      r.headers["Content-Type"] &&
-      r.headers["Content-Type"].includes("application/json"),
+    "content-type is JSON (Connect or plain)": (r) => {
+      const ct = String(
+        r.headers["Content-Type"] || r.headers["content-type"] || "",
+      ).toLowerCase();
+      return (
+        ct.includes("application/json") ||
+        ct.includes("application/connect+json")
+      );
+    },

194-197: Broaden Accept header to include Connect JSON.

Avoids 406/negotiation issues when server replies with Connect JSON.

 const headers = {
   "Content-Type": "application/json",
-  Accept: "application/json",
+  Accept: "application/json, application/connect+json",
 };

232-232: Use explicit 0/1 for Rate metric to avoid implicit coercion.

Minor clarity/readability improvement.

-errorRate.add(!isSuccess);
+errorRate.add(isSuccess ? 0 : 1);

48-55: Avoid redundant default target_platform in base payload.

You always overwrite it per-request; remove from the base payload to reduce confusion.

 const payload = {
   request_id: "",
   force_rebuild: true,
-  target_platform: {
-    os: "linux",
-    arch: "amd64",
-  },
   rr_version: "v2025.1.2",
   plugins: [
v2/github/github.go (4)

96-100: Store a clone in cache to avoid aliasing with future readers

Storing the same buffer pointer in cache risks accidental mutation/aliasing. Prefer storing an independent copy.

-	// save zipped rr buffer
-	r.cache.Set(rrVersion, buf)
+	// save zipped rr buffer (store an independent copy)
+	r.cache.Set(rrVersion, bytes.NewBuffer(bytes.Clone(buf.Bytes())))

169-175: Use io.Copy to avoid short writes and reduce memory overhead

Writing the full buffer with f.Write risks short writes and makes the log count misleading. io.Copy handles partial writes and is idiomatic here.

-  n, err := f.Write(buf.Bytes())
-  if err != nil {
-    return "", err
-  }
-
-  r.log.Debug("repository saved", zap.Int("bytes written", n))
+  if _, err := io.Copy(f, bytes.NewReader(buf.Bytes())); err != nil {
+    return "", err
+  }
+  r.log.Debug("repository saved", zap.Int("bytes written", buf.Len()))

205-213: Redundant/ineffective path check (Abs removes “..”)

Computing Abs(rc.File[0].Name) cleans the path so the subsequent strings.Contains(abs, "..") will never trigger. The following outDir check already covers “..”. Remove the dead code.

-  abs, err := filepath.Abs(rc.File[0].Name)
-  if err != nil {
-    return "", err
-  }
-  // for this repository (roadrunner-server/roadrunner), 0-th element is a directory with content
-  if strings.Contains(abs, "..") {
-    return "", errors.New("path should not contain the '..' symbols")
-  }
-
   outDir := rc.File[0].Name
   if strings.Contains(outDir, "..") {
     return "", errors.New("CWE-22, output dir from a zip file can't contain a '..' filesystem operation, more info: https://cwe.mitre.org/data/definitions/22.html")
   }

237-249: Heuristic for tag vs branch can misclassify branch names starting with 'v'

If a branch is named e.g. "v-next", it will be treated as a tag. Consider making the tag detection stricter (e.g., v prefix), or allow caller to indicate type.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c9c5027 and b2a466d.

📒 Files selected for processing (2)
  • k6-benchmark.js (1 hunks)
  • v2/github/github.go (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build (Go stable, OS ubuntu-latest)

@rustatian rustatian merged commit bdd1522 into master Aug 17, 2025
7 checks passed
@github-project-automation github-project-automation bot moved this from 🏗 In progress to ✅ Done in Jira 😄 Aug 17, 2025
@rustatian rustatian deleted the feature/server branch August 17, 2025 11:29
@coderabbitai coderabbitai bot mentioned this pull request Aug 17, 2025
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-other Area: other
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

1 participant