Skip to content

feat: add gRPC-protoc framework support#14

Merged
spencercjh merged 22 commits intomainfrom
feat/grpc-support
Mar 8, 2026
Merged

feat: add gRPC-protoc framework support#14
spencercjh merged 22 commits intomainfrom
feat/grpc-support

Conversation

@spencercjh
Copy link
Copy Markdown
Owner

Summary

Add gRPC/protobuf framework support for native protoc projects (not buf-managed) to generate OpenAPI specs using protoc-gen-connect-openapi.

Key Features

  • Project Detection: Automatically detects gRPC-protoc projects with .proto files
  • Buf Rejection: Clear error message when buf.yaml is detected
  • HTTP Annotations: Automatic detection and support for google.api.http annotations (gRPC-Gateway style REST mappings)
  • Service Proto Files: Only processes proto files with service definitions to avoid duplicate errors

Architecture

Implements the standard extractor pattern: Detector → Patcher → Generator

internal/extractor/grpcprotoc/
├── grpcprotoc.go       # Package types, Info struct
├── extractor.go        # Extractor interface implementation
├── detector.go         # Project detection, service file discovery
├── patcher.go          # protoc + plugin installation check
└── generator.go        # protoc command execution

Usage

# Basic usage
spec-forge generate ./my-grpc-project

# With additional import paths
spec-forge generate ./my-grpc-project --proto-import-path ./third_party

Requirements

  • protoc installed
  • protoc-gen-connect-openapi installed (go install github.com/sudorandom/protoc-gen-connect-openapi@latest)

Test Plan

  • Unit tests for Detector, Patcher, Generator
  • E2E test with demo project (integration-tests/grpc_protoc_test.go)
  • Demo project with HTTP annotations (integration-tests/grpc-protoc-demo/)
  • All tests passing: make verify
  • Linter clean: make lint

Changes

Component Changes
internal/extractor/grpcprotoc/ New package with full implementation
internal/extractor/builtin/register.go Register grpcprotoc extractor
cmd/generate.go Add --proto-import-path flag
integration-tests/grpc-protoc-demo/ Demo project with HTTP annotations
integration-tests/grpc_protoc_test.go E2E test
docs/plans/2026-03-08-grpc-protoc-*.md Design and implementation docs
README.md, CLAUDE.md Documentation updates

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 8, 2026 00:58
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add gRPC-protoc framework support with OpenAPI spec generation

✨ Enhancement 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• **gRPC-protoc Framework Support**: Complete implementation of gRPC/protobuf framework support for
  native protoc projects (non-buf-managed) to generate OpenAPI specs using
  protoc-gen-connect-openapi
• **Core Components**: Implements standard extractor pattern with Detector (project identification
  and service file discovery), Patcher (tool verification), and Generator (protoc command execution)
• **HTTP Annotations Support**: Automatic detection and support for google.api.http annotations
  enabling gRPC-Gateway style REST mappings
• **CLI Enhancement**: Added --proto-import-path flag for specifying additional protoc import
  paths
• **Comprehensive Testing**: 1,600+ lines of unit tests covering Detector, Patcher, Generator
  components plus end-to-end integration test with demo project
• **Demo Project**: Complete gRPC demo project with HTTP annotations, Makefile automation, and
  generated OpenAPI specification
• **Documentation**: Design and implementation plans, updated README and architecture docs with gRPC
  support details
Diagram
flowchart LR
  A["gRPC Project<br/>with .proto files"] -->|Detector| B["Identify Services<br/>& HTTP Annotations"]
  B -->|Patcher| C["Verify protoc<br/>& Plugin Installation"]
  C -->|Generator| D["Execute protoc<br/>with Plugin"]
  D -->|Output| E["OpenAPI Spec<br/>JSON/YAML"]
  F["CLI Flag<br/>--proto-import-path"] -.->|Configure| C
Loading

Grey Divider

File Changes

1. internal/extractor/grpcprotoc/generator_test.go 🧪 Tests +751/-0

Generator component unit tests with comprehensive coverage

• Comprehensive test suite for the Generator component with 751 lines of test coverage
• Tests cover successful JSON/YAML generation, protoc argument building, import paths, and error
 handling
• Includes mock executor implementation for testing without actual protoc execution
• Tests verify output file discovery, format options, and error cases (missing files, failed
 commands)

internal/extractor/grpcprotoc/generator_test.go


2. internal/extractor/grpcprotoc/detector_test.go 🧪 Tests +390/-0

Detector component unit tests and validation

• 390 lines of unit tests for the Detector component
• Tests cover buf project rejection, proto file discovery, google.api.http annotation detection
• Validates import path detection, vendor/hidden directory skipping, and multiple proto files
• Tests error cases and invalid project paths

internal/extractor/grpcprotoc/detector_test.go


3. internal/extractor/grpcprotoc/generator.go ✨ Enhancement +320/-0

Generator implementation for OpenAPI spec generation

• Core Generator implementation (320 lines) that executes protoc with protoc-gen-connect-openapi
 plugin
• Builds protoc command arguments with import paths, output directory, and format options
• Discovers generated OpenAPI files and handles both JSON and YAML output formats
• Includes comprehensive error handling and logging for protoc execution failures

internal/extractor/grpcprotoc/generator.go


View more (24)
4. internal/extractor/grpcprotoc/patcher_test.go 🧪 Tests +302/-0

Patcher component unit tests for tool verification

• 302 lines of unit tests for the Patcher component
• Tests verify protoc and protoc-gen-connect-openapi installation checks
• Covers success cases, missing tool detection, and command failure scenarios
• Validates error messages include installation hints for missing tools

internal/extractor/grpcprotoc/patcher_test.go


5. internal/extractor/grpcprotoc/detector.go ✨ Enhancement +251/-0

Detector implementation for gRPC-protoc project identification

• Core Detector implementation (251 lines) for identifying gRPC-protoc projects
• Rejects buf-managed projects with clear error message
• Discovers all .proto files, identifies service definitions, and detects google.api.http
 annotations
• Automatically detects import paths from common proto directories (proto/, third_party/, protos/)

internal/extractor/grpcprotoc/detector.go


6. internal/extractor/grpcprotoc/patcher.go ✨ Enhancement +137/-0

Patcher implementation for tool availability verification

Patcher implementation (137 lines) that verifies protoc and plugin installation
• Checks protoc --version and protoc-gen-connect-openapi --version
• Returns helpful error messages with installation instructions when tools are missing
• Provides version information for both tools

internal/extractor/grpcprotoc/patcher.go


7. integration-tests/grpc_protoc_test.go 🧪 Tests +118/-0

End-to-end integration test for gRPC-protoc workflow

• End-to-end test (118 lines) for the complete gRPC-protoc extraction workflow
• Tests detection, generation, and validation of OpenAPI specs from demo project
• Verifies REST paths from google.api.http annotations are present in generated spec
• Validates YAML format output and spec file creation

integration-tests/grpc_protoc_test.go


8. cmd/generate.go ✨ Enhancement +9/-4

CLI flag support for protoc import paths

• Added generateProtoImportPaths flag variable to store additional import paths
• Added --proto-import-path CLI flag allowing multiple specifications for protoc -I flags
• Integrated proto import paths into GenerateOptions passed to extractor

cmd/generate.go


9. internal/extractor/grpcprotoc/extractor.go ✨ Enhancement +57/-0

Extractor interface implementation for gRPC-protoc

Extractor interface implementation (57 lines) that orchestrates Detector, Patcher, and Generator
• Implements standard extractor pattern: Detect(), Patch(), Generate(), Restore()
• Provides lazy initialization of components and delegates to appropriate implementations

internal/extractor/grpcprotoc/extractor.go


10. internal/extractor/types.go ✨ Enhancement +6/-5

GenerateOptions struct extension for proto imports

• Added ProtoImportPaths field to GenerateOptions struct for protoc -I flags
• Field supports multiple import paths for complex proto project structures

internal/extractor/types.go


11. internal/extractor/grpcprotoc/grpcprotoc_test.go 🧪 Tests +41/-0

Package-level unit tests for gRPC-protoc types

• Basic unit tests (41 lines) for Info struct and framework constants
• Validates FrameworkName constant and Info field initialization

internal/extractor/grpcprotoc/grpcprotoc_test.go


12. internal/extractor/grpcprotoc/grpcprotoc.go ✨ Enhancement +20/-0

Core types and constants for gRPC-protoc extractor

• Package constants and types definition (20 lines)
• Defines FrameworkName constant, BuildToolProtoc identifier, and Info struct
• Info struct holds proto files, service files, import paths, and feature detection flags

internal/extractor/grpcprotoc/grpcprotoc.go


13. internal/extractor/builtin/register.go ✨ Enhancement +2/-0

Register gRPC-protoc extractor in built-in registry

• Registered grpcprotoc.Extractor in the built-in extractors registry
• Added import for grpcprotoc package

internal/extractor/builtin/register.go


14. docs/plans/2026-03-08-grpc-protoc-design.md 📝 Documentation +700/-0

Design documentation for gRPC-protoc framework support

• Comprehensive design document (700 lines) for gRPC-protoc framework support
• Details architecture, API reference, implementation patterns, and testing strategy
• Includes component diagrams, code examples, and future improvement roadmap
• Documents buf project rejection rationale and Phase 2 plans

docs/plans/2026-03-08-grpc-protoc-design.md


15. integration-tests/grpc-protoc-demo/third_party/google/api/http.proto ⚙️ Configuration changes +138/-0

Google API HTTP annotations proto definitions

• Google API HTTP annotations proto file (138 lines) from googleapis
• Defines Http, HttpRule, and CustomHttpPattern messages for gRPC-Gateway style REST mappings
• Enables mapping of gRPC methods to HTTP REST endpoints with path templates

integration-tests/grpc-protoc-demo/third_party/google/api/http.proto


16. integration-tests/grpc-protoc-demo/proto/common.proto ⚙️ Configuration changes +45/-0

Common proto message definitions for demo project

• Common proto messages (45 lines) for demo project
• Defines PageRequest, PageMetadata, ApiResponse, and FileInfo message types
• Provides reusable types for pagination and API responses

integration-tests/grpc-protoc-demo/proto/common.proto


17. docs/plans/2026-03-08-grpc-protoc-impl.md 📝 Documentation +1209/-0

gRPC-protoc implementation plan and architecture documentation

• Comprehensive implementation plan for gRPC-protoc framework support with detailed task breakdown
• Documents architecture pattern (Detector → Patcher → Generator) and key design decisions
• Includes step-by-step implementation guide with code examples and test cases
• Covers package structure, type definitions, and integration with builtin registry

docs/plans/2026-03-08-grpc-protoc-impl.md


18. integration-tests/grpc-protoc-demo/openapi.yaml 🧪 Tests +545/-0

Generated OpenAPI specification for gRPC demo project

• Generated OpenAPI 3.1.0 specification for demo gRPC user service
• Defines 5 service endpoints (CreateUser, GetUser, ListUsers, UpdateProfile, UploadFile)
• Includes complete schema definitions for request/response messages and common types
• Demonstrates HTTP annotations support with Connect protocol headers

integration-tests/grpc-protoc-demo/openapi.yaml


19. integration-tests/grpc-protoc-demo/proto/user.proto 🧪 Tests +153/-0

gRPC user service proto definitions with HTTP annotations

• Defines UserService with 5 RPC methods for user management operations
• Includes google.api.http annotations for REST-style HTTP mappings (GET/POST/PUT)
• Defines message types for CRUD operations with pagination and file upload support
• Imports common types from common.proto and Google API annotations

integration-tests/grpc-protoc-demo/proto/user.proto


20. CLAUDE.md 📝 Documentation +30/-16

Architecture documentation updated for multi-framework support

• Updated architecture overview to reflect multi-framework support (Spring Boot, go-zero,
 gRPC-protoc)
• Expanded package structure documentation to include gRPC-protoc, gozero, and publisher packages
• Updated workflow description from "Restore" to "Publish" for final step
• Added detailed descriptions of new extractor implementations and their components

CLAUDE.md


21. integration-tests/grpc-protoc-demo/README.md 📝 Documentation +105/-0

gRPC demo project documentation and setup guide

• Comprehensive guide for gRPC demo project structure and setup
• Documents prerequisites, installation steps, and usage with spec-forge
• Explains differences from buf-managed projects and spec-forge integration notes
• Includes API overview table showing gRPC methods and HTTP mappings

integration-tests/grpc-protoc-demo/README.md


22. integration-tests/grpc-protoc-demo/Makefile ⚙️ Configuration changes +55/-0

Makefile for gRPC protoc project build automation

• Provides make proto target for generating Go code from proto files
• Provides make openapi target for generating OpenAPI specs with protoc-gen-connect-openapi
• Includes check-tools and install-tools targets for dependency verification
• Supports configurable import paths via IMPORT_PATHS and EXTRA_IMPORT_PATHS variables

integration-tests/grpc-protoc-demo/Makefile


23. integration-tests/README.md 📝 Documentation +18/-0

Integration tests documentation updated for gRPC support

• Added grpc-protoc-demo to list of demo projects with protoc framework
• Added installation instructions for protoc and protoc-gen-connect-openapi
• Added test case documentation for TestE2E_GrpcProtoc_Generate
• Documented gRPC-protoc test verification points (detection, HTTP annotations, OpenAPI generation)

integration-tests/README.md


24. README.md 📝 Documentation +25/-1

README updated with gRPC-protoc framework support documentation

• Added gRPC (protoc) to supported frameworks table with ✅ Supported status
• Added comprehensive usage section for gRPC projects with CLI examples
• Documented requirements (protoc and protoc-gen-connect-openapi installation)
• Included note about buf-managed projects requiring alternative workflow

README.md


25. integration-tests/grpc-protoc-demo/third_party/google/api/annotations.proto 🧪 Tests +31/-0

Google API annotations proto for HTTP REST mappings

• Google API annotations proto file for HTTP method options
• Defines HttpRule extension for google.protobuf.MethodOptions
• Enables google.api.http annotations in service definitions for REST mappings
• Licensed under Apache 2.0 from Google LLC

integration-tests/grpc-protoc-demo/third_party/google/api/annotations.proto


26. integration-tests/grpc-protoc-demo/go.mod Dependencies +8/-0

Go module dependencies for gRPC demo project

• Go module definition for gRPC demo project
• Declares dependencies on protoc-gen-connect-openapi v0.16.0 and google.golang.org/protobuf
 v1.36.5
• Targets Go 1.24 compatibility

integration-tests/grpc-protoc-demo/go.mod


27. integration-tests/grpc-protoc-demo/go.sum Dependencies +2/-0

Go module checksums for gRPC demo dependencies

• Checksum file for Go module dependencies
• Contains hashes for protoc-gen-connect-openapi and google.golang.org/protobuf modules

integration-tests/grpc-protoc-demo/go.sum


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Mar 8, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. grpcprotoc mis-detects projects 🐞 Bug ✓ Correctness
Description
The grpc-protoc Detector succeeds for any project containing at least one .proto file even if no
service definitions exist, which can misclassify non-gRPC projects. Because framework detection
iterates extractors in map iteration order, adding this broad detector can also make framework
selection nondeterministic and lead to generate failures (ErrNoServiceProtoFiles) depending on which
extractor is tried first.
Code

internal/extractor/grpcprotoc/detector.go[R69-120]

+	// Find all .proto files
+	protoFiles, err := d.findProtoFiles(absPath)
+	if err != nil {
+		slog.Error("failed to find .proto files", "path", absPath, "error", err)
+		return nil, fmt.Errorf("failed to find .proto files: %w", err)
+	}
+	slog.Debug("found .proto files", "count", len(protoFiles), "files", protoFiles)
+
+	// Reject if no .proto files found
+	if len(protoFiles) == 0 {
+		slog.Warn("no .proto files found in project", "path", absPath)
+		return nil, &ErrNotProtocProject{Reason: "no .proto files found in project"}
+	}
+	slog.Info("found .proto files", "count", len(protoFiles))
+
+	// Detect import paths
+	importPaths := d.detectImportPaths(absPath, protoFiles)
+	slog.Debug("detected import paths", "paths", importPaths)
+
+	// Check for google.api.http annotations
+	hasGoogleAPI := d.checkGoogleAPIAnnotations(protoFiles)
+	if hasGoogleAPI {
+		slog.Info("detected google.api.http annotations in project")
+	}
+
+	// Find proto files with service definitions (main entry points)
+	serviceProtoFiles := d.findServiceProtoFiles(protoFiles)
+	slog.Debug("found service proto files", "count", len(serviceProtoFiles), "files", serviceProtoFiles)
+
+	// Build info
+	grpcInfo := &Info{
+		ProtoFiles:        protoFiles,
+		ServiceProtoFiles: serviceProtoFiles,
+		ProtoRoot:         absPath,
+		HasGoogleAPI:      hasGoogleAPI,
+		HasBuf:            false,
+		ImportPaths:       importPaths,
+	}
+
+	info := &extractor.ProjectInfo{
+		Framework:     FrameworkName,
+		BuildTool:     BuildToolProtoc,
+		BuildFilePath: absPath,
+		FrameworkData: grpcInfo,
+	}
+
+	slog.Info("gRPC-protoc project detection completed successfully",
+		"protoFiles", len(protoFiles),
+		"hasGoogleAPI", hasGoogleAPI,
+		"importPaths", len(importPaths))
+
+	return info, nil
Evidence
Detector only rejects when no .proto files exist; it computes ServiceProtoFiles but does not require
at least one service file before returning success. Generator later hard-fails when
ServiceProtoFiles is empty. builtin.DetectFramework tries extractors in the order returned from
iterating a map, so which extractor gets tried first is not stable; adding a broad detector
increases the chance of selecting grpc-protoc for unrelated projects.

internal/extractor/grpcprotoc/detector.go[69-120]
internal/extractor/grpcprotoc/generator.go[110-115]
internal/extractor/builtin/builtin.go[27-33]
internal/extractor/builtin/builtin.go[36-47]
internal/extractor/builtin/register.go[9-14]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The grpc-protoc detector currently returns success for any repo containing `.proto` files, even if it has no `service` definitions. This can cause spec-forge to select the grpc-protoc extractor for unrelated projects and then fail later during generation. This risk is amplified because builtin framework detection iterates extractors from a map, so the “first successful detector” is not deterministic.

### Issue Context
- Detector computes `serviceProtoFiles` but never rejects when the slice is empty.
- Generator fails hard if `ServiceProtoFiles` is empty.
- builtin detection order comes from map iteration.

### Fix Focus Areas
- internal/extractor/grpcprotoc/detector.go[69-120]
- internal/extractor/grpcprotoc/generator.go[110-115]
- internal/extractor/builtin/builtin.go[27-47]
- internal/extractor/builtin/register.go[9-14]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Spec file discovery may pick wrong file 🐞 Bug ⛯ Reliability
Description
grpcprotoc Generator sets GenerateOptions.OutputFile but never uses it to control output naming, and
it locates results by returning the first matching .openapi.* file found in outputDir/service dirs
or (fallback) anywhere under the project root. In projects with multiple generated specs or
pre-existing .openapi files, this can return a stale/unrelated spec and lead to incorrect
publish/enrich behavior.
Code

internal/extractor/grpcprotoc/generator.go[R63-307]

+	// Apply defaults
+	if opts.Timeout <= 0 {
+		opts.Timeout = 5 * time.Minute
+	}
+	if opts.Format == "" {
+		opts.Format = defaultFormat
+	}
+	if opts.OutputFile == "" {
+		opts.OutputFile = defaultOutputFileName
+	}
+	slog.Debug("generation options",
+		"timeout", opts.Timeout,
+		"format", opts.Format,
+		"outputFile", opts.OutputFile,
+		"outputDir", opts.OutputDir)
+
+	absPath, err := filepath.Abs(projectPath)
+	if err != nil {
+		slog.Error("failed to resolve project path", "path", projectPath, "error", err)
+		return nil, fmt.Errorf("failed to resolve project path: %w", err)
+	}
+
+	// Get gRPC-protoc info from project info
+	info, ok := projectInfo.FrameworkData.(*Info)
+	if !ok || info == nil {
+		slog.Debug("framework data not set, detecting project")
+		// If FrameworkData is not set, detect it
+		detectedInfo, detectErr := g.detector.Detect(absPath)
+		if detectErr != nil {
+			slog.Error("failed to detect gRPC-protoc project", "path", absPath, "error", detectErr)
+			return nil, fmt.Errorf("failed to detect gRPC-protoc project: %w", detectErr)
+		}
+		var typeOk bool
+		info, typeOk = detectedInfo.FrameworkData.(*Info)
+		if !typeOk {
+			slog.Error("failed to get gRPC-protoc info from detected project")
+			return nil, errors.New("failed to get gRPC-protoc info from detected project")
+		}
+	}
+
+	// Check if there are any proto files
+	if len(info.ProtoFiles) == 0 {
+		slog.Error("no proto files found in project", "path", absPath)
+		return nil, ErrNoProtoFiles
+	}
+	slog.Debug("proto files found", "count", len(info.ProtoFiles), "files", info.ProtoFiles)
+
+	// Check if there are service proto files
+	if len(info.ServiceProtoFiles) == 0 {
+		slog.Error("no service proto files found in project", "path", absPath)
+		return nil, ErrNoServiceProtoFiles
+	}
+	slog.Debug("service proto files found", "count", len(info.ServiceProtoFiles), "files", info.ServiceProtoFiles)
+
+	// Determine output directory
+	outputDir := opts.OutputDir
+	if outputDir == "" {
+		outputDir = absPath
+	}
+
+	// Convert outputDir to absolute path if it's not already
+	if !filepath.IsAbs(outputDir) {
+		var absErr error
+		outputDir, absErr = filepath.Abs(filepath.Join(absPath, outputDir))
+		if absErr != nil {
+			slog.Error("failed to resolve output directory", "dir", outputDir, "error", absErr)
+			return nil, fmt.Errorf("failed to resolve output directory: %w", absErr)
+		}
+	}
+
+	// Ensure output directory exists
+	if mkdirErr := os.MkdirAll(outputDir, 0o755); mkdirErr != nil {
+		slog.Error("failed to create output directory", "dir", outputDir, "error", mkdirErr)
+		return nil, fmt.Errorf("failed to create output directory %s: %w", outputDir, mkdirErr)
+	}
+
+	// Build protoc command arguments
+	args := g.buildProtocArgs(info, outputDir, opts)
+
+	slog.Debug("executing protoc command",
+		"command", protocCommand,
+		"args", args,
+		"workingDir", absPath,
+		"timeout", opts.Timeout)
+
+	result, execErr := g.executor.Execute(ctx, &executor.ExecuteOptions{
+		Command:    protocCommand,
+		Args:       args,
+		WorkingDir: absPath,
+		Timeout:    opts.Timeout,
+	})
+	if execErr != nil {
+		slog.Error("protoc command failed", "error", execErr, "command", protocCommand, "args", args)
+		return nil, fmt.Errorf("protoc command failed: %w", execErr)
+	}
+
+	if result.ExitCode != 0 {
+		out := combineOutput(result.Stdout, result.Stderr)
+		slog.Error("protoc command failed", "exitCode", result.ExitCode, "output", out)
+		return nil, fmt.Errorf("protoc command failed with exit code %d:\n%s", result.ExitCode, out)
+	}
+
+	slog.Debug("protoc command succeeded")
+
+	// Find the generated output file
+	outputPath, findErr := g.findOutputFile(info, outputDir, opts.Format)
+	if findErr != nil {
+		slog.Error("failed to find generated OpenAPI file", "outputDir", outputDir, "error", findErr)
+		return nil, fmt.Errorf("%w: %s", ErrOutputFileNotFound, outputDir)
+	}
+
+	generateResult := &extractor.GenerateResult{
+		SpecFilePath: outputPath,
+		Format:       opts.Format,
+	}
+	slog.Info("OpenAPI spec generation completed", "output", outputPath, "format", opts.Format)
+
+	return generateResult, nil
+}
+
+// buildProtocArgs constructs the protoc command arguments.
+func (g *Generator) buildProtocArgs(info *Info, outputDir string, opts *extractor.GenerateOptions) []string {
+	var args []string
+
+	// Add import paths (-I flags)
+	seenPaths := make(map[string]bool)
+
+	// Add import paths detected from project (convert to relative paths if needed)
+	for _, path := range info.ImportPaths {
+		relPath := g.toRelativePath(path, info.ProtoRoot)
+		if !seenPaths[relPath] {
+			seenPaths[relPath] = true
+			args = append(args, "-I"+relPath)
+		}
+	}
+
+	// Add extra import paths from CLI flags (--proto-import-path)
+	for _, path := range opts.ProtoImportPaths {
+		if !seenPaths[path] {
+			seenPaths[path] = true
+			args = append(args, "-I"+path)
+		}
+	}
+
+	// Add connect-openapi output
+	args = append(args, "--connect-openapi_out="+outputDir)
+
+	// Add format option for YAML
+	if opts.Format == "yaml" || opts.Format == "yml" {
+		args = append(args, "--connect-openapi_opt=format=yaml")
+	}
+
+	// Enable google.api.http annotations support if detected
+	if info.HasGoogleAPI {
+		args = append(args, "--connect-openapi_opt=features=google.api.http")
+	}
+
+	// Add only service proto files (those with service definitions)
+	// to avoid duplicate definition errors from importing common proto files
+	for _, protoFile := range info.ServiceProtoFiles {
+		relPath := g.toRelativePath(protoFile, info.ProtoRoot)
+		args = append(args, relPath)
+	}
+
+	return args
+}
+
+// toRelativePath converts an absolute path to a relative path from the base directory.
+// If the path is not under the base directory, it returns the original path.
+func (g *Generator) toRelativePath(path, base string) string {
+	if filepath.IsAbs(path) && filepath.IsAbs(base) {
+		rel, err := filepath.Rel(base, path)
+		if err == nil {
+			return rel
+		}
+	}
+	return path
+}
+
+// findOutputFile locates the generated OpenAPI file.
+// protoc-gen-connect-openapi generates files in the same directory as the input proto files.
+func (g *Generator) findOutputFile(info *Info, outputDir, format string) (string, error) {
+	// Determine expected extension
+	expectedExt := ".openapi.json"
+	if format == "yaml" || format == "yml" {
+		expectedExt = ".openapi.yaml"
+	}
+
+	// First, check the output directory itself
+	if outputPath, err := g.findFileWithExt(outputDir, expectedExt); err == nil {
+		return outputPath, nil
+	}
+
+	// Then check directories containing service proto files
+	for _, serviceFile := range info.ServiceProtoFiles {
+		protoDir := filepath.Dir(serviceFile)
+		// Convert to relative path if needed
+		relDir := g.toRelativePath(protoDir, info.ProtoRoot)
+		// Make it absolute for searching
+		searchDir := filepath.Join(info.ProtoRoot, relDir)
+		if outputPath, err := g.findFileWithExt(searchDir, expectedExt); err == nil {
+			return outputPath, nil
+		}
+	}
+
+	// Finally, search recursively from the project root
+	var foundPath string
+	err := filepath.Walk(info.ProtoRoot, func(path string, file os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if !file.IsDir() && strings.HasSuffix(path, expectedExt) {
+			foundPath = path
+			return filepath.SkipAll
+		}
+		// Also check for any .openapi.json or .openapi.yaml files
+		if !file.IsDir() && (strings.HasSuffix(path, ".openapi.json") || strings.HasSuffix(path, ".openapi.yaml")) {
+			foundPath = path
+			return filepath.SkipAll
+		}
+		return nil
+	})
+	if err == nil && foundPath != "" {
+		return foundPath, nil
+	}
+
+	return "", ErrOutputFileNotFound
+}
+
+// findFileWithExt searches for a file with the given extension in the directory.
+func (g *Generator) findFileWithExt(dir, ext string) (string, error) {
+	entries, readErr := os.ReadDir(dir)
+	if readErr != nil {
+		return "", fmt.Errorf("failed to read directory: %w", readErr)
+	}
+
+	for _, entry := range entries {
+		name := entry.Name()
+		if !entry.IsDir() && strings.HasSuffix(name, ext) {
+			return filepath.Join(dir, name), nil
+		}
+	}
+
+	return "", ErrOutputFileNotFound
+}
Evidence
The generator applies a default OutputFile but never includes it in protoc args nor uses it when
selecting the resulting file. Output selection returns the first matching extension in a directory
listing (order-dependent) and can also fall back to any .openapi.json/.openapi.yaml anywhere under
the project root, which can easily match existing artifacts not produced by the current run.

internal/extractor/grpcprotoc/generator.go[63-77]
internal/extractor/grpcprotoc/generator.go[183-205]
internal/extractor/grpcprotoc/generator.go[242-290]
internal/extractor/grpcprotoc/generator.go[292-307]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The grpc-protoc generator currently (a) sets `opts.OutputFile` but does not use it to control output naming, and (b) discovers the output by picking the first `.openapi.(json|yaml)` file in certain directories or anywhere under the project root. This can return an unrelated/stale spec when multiple `.openapi.*` files exist.

### Issue Context
- `OutputFile` is part of the common extractor interface and is expected to influence the final artifact name.
- Directory iteration order is not guaranteed, and recursive fallback can match pre-existing artifacts.

### Fix Focus Areas
- internal/extractor/grpcprotoc/generator.go[63-77]
- internal/extractor/grpcprotoc/generator.go[183-228]
- internal/extractor/grpcprotoc/generator.go[242-307]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link
Copy Markdown
Contributor

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 adds a new gRPC-protoc framework extractor that enables spec-forge to generate OpenAPI specifications from native protoc projects (not buf-managed). It follows the established Detector → Patcher → Generator pattern used by the existing Spring Boot and go-zero extractors.

Changes:

  • New internal/extractor/grpcprotoc/ package implementing project detection (.proto files, buf rejection), tool availability checking (protoc, protoc-gen-connect-openapi), and OpenAPI generation via protoc
  • CLI flag --proto-import-path for additional protoc import paths, wired through GenerateOptions.ProtoImportPaths
  • Integration test demo project and E2E test with HTTP annotation support

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/extractor/grpcprotoc/grpcprotoc.go Package types: Info struct, FrameworkName, BuildToolProtoc constants
internal/extractor/grpcprotoc/detector.go Proto file discovery, buf.yaml rejection, google.api.http detection, service file identification
internal/extractor/grpcprotoc/patcher.go Checks protoc and protoc-gen-connect-openapi installation
internal/extractor/grpcprotoc/generator.go Builds and executes protoc command, locates generated output file
internal/extractor/grpcprotoc/extractor.go Extractor interface implementation with lazy-init
internal/extractor/grpcprotoc/*_test.go Unit tests for all components
internal/extractor/builtin/register.go Registers grpcprotoc extractor in builtin registry
cmd/generate.go Adds --proto-import-path CLI flag
internal/extractor/types.go Adds ProtoImportPaths field to GenerateOptions
integration-tests/grpc_protoc_test.go E2E test for gRPC-protoc generation
integration-tests/grpc-protoc-demo/ Demo project with proto files and HTTP annotations
README.md Documents gRPC-protoc support and usage
CLAUDE.md Updates architecture overview
docs/plans/2026-03-08-*.md Design and implementation documentation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/extractor/grpcprotoc/detector.go Outdated
Comment thread internal/extractor/grpcprotoc/generator.go Outdated
Comment thread internal/extractor/grpcprotoc/grpcprotoc.go Outdated
Comment thread internal/extractor/grpcprotoc/patcher_test.go Outdated
Comment thread internal/extractor/grpcprotoc/detector.go
spencercjh and others added 14 commits March 8, 2026 09:11
Add package constants and Info type for gRPC-protoc extractor.

Signed-off-by: Claude <claude@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Add detector that:
- Rejects buf-managed projects with clear error
- Finds all .proto files
- Detects import paths
- Identifies google.api.http annotations

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Add patcher that verifies:
- protoc is installed
- protoc-gen-connect-openapi is installed
- Returns clear installation hints if missing

Signed-off-by: Claude <claude@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Add generator that:
- Builds protoc command with import paths
- Executes protoc-gen-connect-openapi
- Returns path to generated OpenAPI file

Signed-off-by: Claude <claude@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Add grpcprotoc to builtin extractor registry.
Implements full Extractor interface.

Signed-off-by: Claude <claude@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Add flag for specifying additional protoc import paths.

Co-Authored-By: Claude <claude@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Document gRPC-protoc extractor usage, requirements, and CLI flags.

Signed-off-by: Claude <claude@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
- Add ServiceProtoFiles field to Info struct to identify main entry points
- Update Detector to find proto files with service definitions
- Update Generator to:
  - Only compile service proto files (avoids duplicate definition errors)
  - Handle relative output directories correctly by converting to absolute paths
  - Search recursively for generated OpenAPI files
- Add integration-tests/grpc-protoc-demo with sample UserService proto
- Include generated OpenAPI spec for demo project

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
- Add google/api/annotations.proto and http.proto to demo project
- Update user.proto with google.api.http annotations for all RPC methods
- Generator now conditionally adds --connect-openapi_opt=features=google.api.http
  when HasGoogleAPI is detected, generating REST endpoints alongside gRPC

The generated OpenAPI spec now includes both gRPC-style paths and REST endpoints:
- GET /v1/users - ListUsers
- POST /v1/users - CreateUser
- GET /v1/users/{id} - GetUser
- PUT /v1/users/{id} - UpdateProfile
- POST /v1/users/{user_id}/files - UploadFile

Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
- Add TestE2E_GrpcProtoc_Generate to verify gRPC-protoc detection and generation
- Verify HTTP annotations detection (HasGoogleAPI)
- Verify generated spec contains REST paths from google.api.http annotations
- Fix existing tests to properly extract FrameworkData for spring.Info

Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
- Add grpc_protoc_test.go with dedicated e2e test for gRPC-protoc
- Update integration-tests/README.md with grpc-protoc-demo project
- Update grpc-protoc-demo/README.md with:
  - third_party directory structure
  - HTTP annotations usage
  - spec-forge integration notes
- Update CLAUDE.md with:
  - grpcprotoc package in architecture
  - publisher package
- Remove duplicate tests from spring_maven_test.go (now in separate files)

Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
…mplementation

- Add ServiceProtoFiles field to Info struct documentation
- Document conditional HTTP annotations support (only when HasGoogleAPI is true)
- Update demo project structure to include third_party/google/api
- Mark both documents as completed
- Add implementation summary with key design decisions

Signed-off-by: spencercjh <spencercjh@gmail.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Copilot AI review requested due to automatic review settings March 8, 2026 01:12
Copy link
Copy Markdown
Contributor

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

Copilot reviewed 27 out of 28 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/extractor/grpcprotoc/detector.go Outdated
spencercjh and others added 6 commits March 8, 2026 09:25
The hasGoogleAPIImport method had two identical conditions
checking for 'google/api/annotations.proto'. Since protobuf
only uses double quotes for imports, the single-quote
check is unnecessary and has been removed.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Remove redundant fallback check that could match files with unexpected
format. Now the method only looks for files with the expected extension
based on the requested format.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Keep only ExtractorName to extractor.go to follow the existing
convention (spring, gozero). Remove FrameworkName from
grpcprotoc.go and update all references.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Replace custom contains function with standard library strings.Contains
for consistency and clarity.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
The detector now rejects projects that have .proto files but no service definitions. This prevents the grpc-protoc extractor from being selected for non-gRPC projects that leading to confusing errors during generation.

- Update detector to return ErrNotProtocProject when no service definitions found
- Update test cases to include service definitions in proto files
- Use proper proto file formatting with newlines for service definitions

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
…tputFile

- Add output_name option to protoc-gen-connect-openapi when OutputFile is specified
- Remove recursive search fallback in findOutputFile to avoid finding unrelated files
- Only search output directory and service proto file directories

Signed-off-by: spencercjh <spencercjh@gmail.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
Copilot AI review requested due to automatic review settings March 8, 2026 05:00
Copy link
Copy Markdown
Contributor

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

Copilot reviewed 27 out of 28 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/extractor/grpcprotoc/detector.go
Comment thread internal/extractor/grpcprotoc/generator.go Outdated
…dundant search in findOutputFile

- Add proper comment handling (// and /* */) in hasServiceDefinition
- Use set to skip already-searched directories in findOutputFile
- Address code review feedback from PR #14

Signed-off-by: spencercjh <spencercjh@gmail.com>
@spencercjh spencercjh merged commit 515b70a into main Mar 8, 2026
4 checks passed
@spencercjh spencercjh deleted the feat/grpc-support branch March 8, 2026 05:29
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.

2 participants