-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/manager #20
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
Feat/manager #20
Conversation
Summary of ChangesHello @kooksee, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
…1 22:56:12\n\n* chore: quick update feat/manager at 2026-01-21 23:01:56
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces several significant enhancements, including a new Web UI for configuration management, init and doctor commands for project setup and environment diagnostics, and a comprehensive refactoring of the configuration system into an internal package. The changes improve user experience, streamline project initialization, and centralize configuration definitions. The README.md and README_CN.md files have been updated to reflect these new features and commands, providing clear documentation for users.
| string email = 3 [ | ||
| (retag.tags) = { | ||
| name: "xml" | ||
| value: "email" | ||
| }, | ||
| (retag.tags) = { | ||
| name: "validate" | ||
| value: "required,email" | ||
| } | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| string website = 4 [ | ||
| (retag.tags) = { | ||
| name: "xml" | ||
| value: "website,omitempty" | ||
| }, | ||
| (retag.tags) = { | ||
| name: "validate" | ||
| value: "omitempty,url" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Package webcmd provides a web-based configuration UI for protobuild. | ||
| package webcmd | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "context" | ||
| "embed" | ||
| "encoding/json" | ||
| "fmt" | ||
| "html/template" | ||
| "io/fs" | ||
| "log/slog" | ||
| "net" | ||
| "net/http" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "runtime" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/pubgo/protobuild/internal/config" | ||
| ) | ||
|
|
||
| //go:embed templates/* | ||
| var templateFS embed.FS | ||
|
|
||
| // CommandResult represents the result of a command execution. | ||
| type CommandResult struct { | ||
| Success bool `json:"success"` | ||
| Output string `json:"output"` | ||
| Error string `json:"error,omitempty"` | ||
| } | ||
|
|
||
| // Server represents the web server. | ||
| type Server struct { | ||
| configPath string | ||
| config *config.Config | ||
| mu sync.RWMutex | ||
| server *http.Server | ||
| templates *template.Template | ||
| } | ||
|
|
||
| // NewServer creates a new web server. | ||
| func NewServer(configPath string) (*Server, error) { | ||
| s := &Server{ | ||
| configPath: configPath, | ||
| } | ||
|
|
||
| // Load templates | ||
| tmpl, err := template.ParseFS(templateFS, "templates/*.html") | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to parse templates: %w", err) | ||
| } | ||
| s.templates = tmpl | ||
|
|
||
| // Load initial config | ||
| if err := s.loadConfig(); err != nil { | ||
| // Create default config if not exists | ||
| s.config = config.Default() | ||
| } | ||
|
|
||
| return s, nil | ||
| } | ||
|
|
||
| // loadConfig loads the configuration from file. | ||
| func (s *Server) loadConfig() error { | ||
| s.mu.Lock() | ||
| defer s.mu.Unlock() | ||
|
|
||
| cfg, err := config.Load(s.configPath) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| s.config = cfg | ||
| return nil | ||
| } | ||
|
|
||
| // saveConfig saves the configuration to file. | ||
| func (s *Server) saveConfig() error { | ||
| s.mu.RLock() | ||
| cfg := s.config | ||
| s.mu.RUnlock() | ||
|
|
||
| return config.Save(s.configPath, cfg) | ||
| } | ||
|
|
||
| // Start starts the web server. | ||
| func (s *Server) Start(ctx context.Context, port int) error { | ||
| mux := http.NewServeMux() | ||
|
|
||
| // Static files | ||
| mux.HandleFunc("/", s.handleIndex) | ||
|
|
||
| // API endpoints | ||
| mux.HandleFunc("/api/config", s.handleConfig) | ||
| mux.HandleFunc("/api/config/save", s.handleSaveConfig) | ||
| mux.HandleFunc("/api/command/", s.handleCommand) | ||
| mux.HandleFunc("/api/command-stream/", s.handleCommandStream) | ||
| mux.HandleFunc("/api/proto-files", s.handleProtoFiles) | ||
| mux.HandleFunc("/api/proto-content", s.handleProtoContent) | ||
| mux.HandleFunc("/api/deps/status", s.handleDepsStatus) | ||
| mux.HandleFunc("/api/project/stats", s.handleProjectStats) | ||
|
|
||
| addr := fmt.Sprintf(":%d", port) | ||
| s.server = &http.Server{ | ||
| Addr: addr, | ||
| Handler: mux, | ||
| } | ||
|
|
||
| // Find available port if default is in use | ||
| listener, err := net.Listen("tcp", addr) | ||
| if err != nil { | ||
| // Try to find an available port | ||
| listener, err = net.Listen("tcp", ":0") | ||
| if err != nil { | ||
| return fmt.Errorf("failed to find available port: %w", err) | ||
| } | ||
| } | ||
|
|
||
| actualPort := listener.Addr().(*net.TCPAddr).Port | ||
| url := fmt.Sprintf("http://localhost:%d", actualPort) | ||
|
|
||
| slog.Info("Starting web server", "url", url) | ||
| fmt.Printf("\n🌐 Web UI available at: %s\n\n", url) | ||
|
|
||
| // Open browser | ||
| go func() { | ||
| time.Sleep(500 * time.Millisecond) | ||
| openBrowser(url) | ||
| }() | ||
|
|
||
| // Handle graceful shutdown | ||
| go func() { | ||
| <-ctx.Done() | ||
| shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| s.server.Shutdown(shutdownCtx) | ||
| }() | ||
|
|
||
| return s.server.Serve(listener) | ||
| } | ||
|
|
||
| // handleIndex serves the main page. | ||
| func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { | ||
| if r.URL.Path != "/" { | ||
| http.NotFound(w, r) | ||
| return | ||
| } | ||
|
|
||
| s.mu.RLock() | ||
| cfg := s.config | ||
| s.mu.RUnlock() | ||
|
|
||
| data := map[string]interface{}{ | ||
| "Config": cfg, | ||
| "ConfigPath": s.configPath, | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "text/html; charset=utf-8") | ||
| if err := s.templates.ExecuteTemplate(w, "index.html", data); err != nil { | ||
| http.Error(w, err.Error(), http.StatusInternalServerError) | ||
| } | ||
| } | ||
|
|
||
| // handleConfig returns the current configuration. | ||
| func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method == http.MethodGet { | ||
| // Reload config from file | ||
| s.loadConfig() | ||
|
|
||
| s.mu.RLock() | ||
| cfg := s.config | ||
| s.mu.RUnlock() | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(cfg) | ||
| return | ||
| } | ||
|
|
||
| http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | ||
| } | ||
|
|
||
| // handleSaveConfig saves the configuration. | ||
| func (s *Server) handleSaveConfig(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method != http.MethodPost { | ||
| http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | ||
| return | ||
| } | ||
|
|
||
| var cfg config.Config | ||
| if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { | ||
| http.Error(w, err.Error(), http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| s.mu.Lock() | ||
| s.config = &cfg | ||
| s.mu.Unlock() | ||
|
|
||
| if err := s.saveConfig(); err != nil { | ||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(CommandResult{ | ||
| Success: false, | ||
| Error: err.Error(), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(CommandResult{ | ||
| Success: true, | ||
| Output: "Configuration saved successfully", | ||
| }) | ||
| } | ||
|
|
||
| // handleCommand executes protobuild commands. | ||
| func (s *Server) handleCommand(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method != http.MethodPost { | ||
| http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | ||
| return | ||
| } | ||
|
|
||
| // Extract command from URL path | ||
| cmdName := strings.TrimPrefix(r.URL.Path, "/api/command/") | ||
| if cmdName == "" { | ||
| http.Error(w, "Command name required", http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| // Build command arguments | ||
| args := []string{"-c", s.configPath, cmdName} | ||
|
|
||
| // Parse additional flags from request body | ||
| var flags map[string]interface{} | ||
| if err := json.NewDecoder(r.Body).Decode(&flags); err == nil { | ||
| for key, val := range flags { | ||
| switch v := val.(type) { | ||
| case bool: | ||
| if v { | ||
| args = append(args, "--"+key) | ||
| } | ||
| case string: | ||
| if v != "" { | ||
| args = append(args, "--"+key, v) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Get executable path | ||
| executable, err := os.Executable() | ||
| if err != nil { | ||
| executable = "protobuild" | ||
| } | ||
|
|
||
| // Execute command | ||
| ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) | ||
| defer cancel() | ||
|
|
||
| cmd := exec.CommandContext(ctx, executable, args...) | ||
| cmd.Dir = filepath.Dir(s.configPath) | ||
|
|
||
| output, err := cmd.CombinedOutput() | ||
|
|
||
| result := CommandResult{ | ||
| Success: err == nil, | ||
| Output: string(output), | ||
| } | ||
| if err != nil { | ||
| result.Error = err.Error() | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(result) | ||
| } | ||
|
|
||
| // handleProtoFiles returns a list of proto files in the project. | ||
| func (s *Server) handleProtoFiles(w http.ResponseWriter, r *http.Request) { | ||
| s.mu.RLock() | ||
| cfg := s.config | ||
| s.mu.RUnlock() | ||
|
|
||
| var files []string | ||
| baseDir := filepath.Dir(s.configPath) | ||
|
|
||
| for _, root := range cfg.Root { | ||
| rootPath := filepath.Join(baseDir, root) | ||
| filepath.Walk(rootPath, func(path string, info fs.FileInfo, err error) error { | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| if !info.IsDir() && strings.HasSuffix(path, ".proto") { | ||
| relPath, _ := filepath.Rel(baseDir, path) | ||
| files = append(files, relPath) | ||
| } | ||
| return nil | ||
| }) | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(files) | ||
| } | ||
|
|
||
| // handleDepsStatus returns the status of dependencies. | ||
| func (s *Server) handleDepsStatus(w http.ResponseWriter, r *http.Request) { | ||
| // Get executable path | ||
| executable, err := os.Executable() | ||
| if err != nil { | ||
| executable = "protobuild" | ||
| } | ||
|
|
||
| cmd := exec.Command(executable, "-c", s.configPath, "deps") | ||
| cmd.Dir = filepath.Dir(s.configPath) | ||
| output, _ := cmd.CombinedOutput() | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(map[string]string{ | ||
| "output": string(output), | ||
| }) | ||
| } | ||
|
|
||
| // openBrowser opens the URL in the default browser. | ||
| func openBrowser(url string) error { | ||
| var cmd string | ||
| var args []string | ||
|
|
||
| switch runtime.GOOS { | ||
| case "darwin": | ||
| cmd = "open" | ||
| args = []string{url} | ||
| case "linux": | ||
| cmd = "xdg-open" | ||
| args = []string{url} | ||
| case "windows": | ||
| cmd = "rundll32" | ||
| args = []string{"url.dll,FileProtocolHandler", url} | ||
| default: | ||
| return fmt.Errorf("unsupported platform") | ||
| } | ||
|
|
||
| return exec.Command(cmd, args...).Start() | ||
| } | ||
|
|
||
| // handleCommandStream executes a command and streams output via SSE. | ||
| func (s *Server) handleCommandStream(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method != http.MethodPost { | ||
| http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | ||
| return | ||
| } | ||
|
|
||
| // Extract command from URL path | ||
| cmdName := strings.TrimPrefix(r.URL.Path, "/api/command-stream/") | ||
| if cmdName == "" { | ||
| http.Error(w, "Command name required", http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| // Set SSE headers | ||
| w.Header().Set("Content-Type", "text/event-stream") | ||
| w.Header().Set("Cache-Control", "no-cache") | ||
| w.Header().Set("Connection", "keep-alive") | ||
| w.Header().Set("Access-Control-Allow-Origin", "*") | ||
|
|
||
| flusher, ok := w.(http.Flusher) | ||
| if !ok { | ||
| http.Error(w, "Streaming not supported", http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| // Build command arguments | ||
| args := []string{"-c", s.configPath, cmdName} | ||
|
|
||
| // Get executable path | ||
| executable, err := os.Executable() | ||
| if err != nil { | ||
| executable = "protobuild" | ||
| } | ||
|
|
||
| // Execute command with streaming output | ||
| ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second) | ||
| defer cancel() | ||
|
|
||
| cmd := exec.CommandContext(ctx, executable, args...) | ||
| cmd.Dir = filepath.Dir(s.configPath) | ||
|
|
||
| // Create pipes for stdout and stderr | ||
| stdout, _ := cmd.StdoutPipe() | ||
| stderr, _ := cmd.StderrPipe() | ||
|
|
||
| if err := cmd.Start(); err != nil { | ||
| fmt.Fprintf(w, "data: {\"type\":\"error\",\"data\":\"%s\"}\n\n", err.Error()) | ||
| flusher.Flush() | ||
| return | ||
| } | ||
|
|
||
| // Stream output | ||
| go func() { | ||
| scanner := bufio.NewScanner(stdout) | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| fmt.Fprintf(w, "data: {\"type\":\"stdout\",\"data\":\"%s\"}\n\n", escapeJSON(line)) | ||
| flusher.Flush() | ||
| } | ||
| }() | ||
|
|
||
| go func() { | ||
| scanner := bufio.NewScanner(stderr) | ||
| for scanner.Scan() { | ||
| line := scanner.Text() | ||
| fmt.Fprintf(w, "data: {\"type\":\"stderr\",\"data\":\"%s\"}\n\n", escapeJSON(line)) | ||
| flusher.Flush() | ||
| } | ||
| }() | ||
|
|
||
| err = cmd.Wait() | ||
| if err != nil { | ||
| fmt.Fprintf(w, "data: {\"type\":\"error\",\"data\":\"%s\"}\n\n", err.Error()) | ||
| } else { | ||
| fmt.Fprintf(w, "data: {\"type\":\"done\",\"data\":\"Command completed successfully\"}\n\n") | ||
| } | ||
| flusher.Flush() | ||
| } | ||
|
|
||
| // escapeJSON escapes special characters for JSON string. | ||
| func escapeJSON(s string) string { | ||
| s = strings.ReplaceAll(s, "\\", "\\\\") | ||
| s = strings.ReplaceAll(s, "\"", "\\\"") | ||
| s = strings.ReplaceAll(s, "\n", "\\n") | ||
| s = strings.ReplaceAll(s, "\r", "\\r") | ||
| s = strings.ReplaceAll(s, "\t", "\\t") | ||
| return s | ||
| } | ||
|
|
||
| // handleProtoContent returns the content of a specific proto file. | ||
| func (s *Server) handleProtoContent(w http.ResponseWriter, r *http.Request) { | ||
| filePath := r.URL.Query().Get("file") | ||
| if filePath == "" { | ||
| http.Error(w, "File path required", http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| baseDir := filepath.Dir(s.configPath) | ||
| fullPath := filepath.Join(baseDir, filePath) | ||
|
|
||
| // Security check: ensure the path is within the project | ||
| absBase, _ := filepath.Abs(baseDir) | ||
| absPath, _ := filepath.Abs(fullPath) | ||
| if !strings.HasPrefix(absPath, absBase) { | ||
| http.Error(w, "Invalid file path", http.StatusForbidden) | ||
| return | ||
| } | ||
|
|
||
| // Check file extension | ||
| if !strings.HasSuffix(fullPath, ".proto") { | ||
| http.Error(w, "Only .proto files allowed", http.StatusForbidden) | ||
| return | ||
| } | ||
|
|
||
| content, err := os.ReadFile(fullPath) | ||
| if err != nil { | ||
| http.Error(w, err.Error(), http.StatusNotFound) | ||
| return | ||
| } | ||
|
|
||
| // Get file info | ||
| info, _ := os.Stat(fullPath) | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(map[string]interface{}{ | ||
| "path": filePath, | ||
| "content": string(content), | ||
| "size": info.Size(), | ||
| "modified": info.ModTime().Format(time.RFC3339), | ||
| }) | ||
| } | ||
|
|
||
| // ProjectStats represents project statistics. | ||
| type ProjectStats struct { | ||
| ProtoFiles int `json:"proto_files"` | ||
| TotalLines int `json:"total_lines"` | ||
| MessageCount int `json:"message_count"` | ||
| ServiceCount int `json:"service_count"` | ||
| DependencyCount int `json:"dependency_count"` | ||
| PluginCount int `json:"plugin_count"` | ||
| ProtoRoots []string `json:"proto_roots"` | ||
| VendorDir string `json:"vendor_dir"` | ||
| VendorFiles int `json:"vendor_files"` | ||
| } | ||
|
|
||
| // handleProjectStats returns project statistics. | ||
| func (s *Server) handleProjectStats(w http.ResponseWriter, r *http.Request) { | ||
| s.mu.RLock() | ||
| cfg := s.config | ||
| s.mu.RUnlock() | ||
|
|
||
| stats := ProjectStats{ | ||
| ProtoRoots: cfg.Root, | ||
| VendorDir: cfg.Vendor, | ||
| DependencyCount: len(cfg.Depends), | ||
| PluginCount: len(cfg.Plugins), | ||
| } | ||
|
|
||
| baseDir := filepath.Dir(s.configPath) | ||
|
|
||
| // Count proto files in root directories | ||
| for _, root := range cfg.Root { | ||
| rootPath := filepath.Join(baseDir, root) | ||
| filepath.Walk(rootPath, func(path string, info fs.FileInfo, err error) error { | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| if !info.IsDir() && strings.HasSuffix(path, ".proto") { | ||
| stats.ProtoFiles++ | ||
|
|
||
| // Count lines, messages, and services | ||
| content, err := os.ReadFile(path) | ||
| if err == nil { | ||
| lines := strings.Split(string(content), "\n") | ||
| stats.TotalLines += len(lines) | ||
|
|
||
| for _, line := range lines { | ||
| trimmed := strings.TrimSpace(line) | ||
| if strings.HasPrefix(trimmed, "message ") { | ||
| stats.MessageCount++ | ||
| } else if strings.HasPrefix(trimmed, "service ") { | ||
| stats.ServiceCount++ | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| }) | ||
| } | ||
|
|
||
| // Count vendor files | ||
| if cfg.Vendor != "" { | ||
| vendorPath := filepath.Join(baseDir, cfg.Vendor) | ||
| filepath.Walk(vendorPath, func(path string, info fs.FileInfo, err error) error { | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| if !info.IsDir() && strings.HasSuffix(path, ".proto") { | ||
| stats.VendorFiles++ | ||
| } | ||
| return nil | ||
| }) | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| json.NewEncoder(w).Encode(stats) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| | `web` | Start web-based configuration UI | | ||
| | `web --port 9090` | Start web UI on custom port | | ||
| | `clean` | Clean dependency cache | | ||
| | `clean --dry-run` | Show what would be cleaned without deleting | | ||
| | `init` | Initialize a new protobuild project | | ||
| | `init --template grpc` | Initialize with specific template (basic, grpc, minimal) | | ||
| | `doctor` | Check development environment and dependencies | | ||
| | `doctor --fix` | Auto-install missing Go plugins | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ## Roadmap | ||
| Upcoming features planned for future releases: | ||
| | Feature | Description | Status | | ||
| |---------|-------------|--------| | ||
| | 🔗 **Dependency Graph** | Visualize proto file import dependencies | Planned | | ||
| | ⚠️ **Breaking Change Detection** | Detect incompatible changes between versions | Planned | | ||
| | 📚 **API Documentation Generator** | Auto-generate Markdown/HTML docs from proto comments | Planned | | ||
| | 🎭 **Mock Server** | Auto-start mock gRPC/HTTP server for testing | Planned | | ||
| | 📝 **Proto Templates** | Quick generation of common proto patterns (CRUD, pagination) | Planned | | ||
| | 📊 **Field Statistics** | Analyze field naming conventions and type distribution | Planned | | ||
| | ✏️ **Online Editor** | Edit proto files directly in Web UI | Planned | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - name: go | ||
| out: pkg | ||
| opt: | ||
| - paths=source_relative | ||
| - name: retag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| out: pkg | ||
| opt: | ||
| - paths=source_relative | ||
| - name: retag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - output=pkg | ||
| - name: test | ||
| linter: | ||
| rules: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| enabled_rules: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - core::0131::http-body | ||
| - core::0235::plural-method-name | ||
| disabled_rules: | ||
| - all |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ignore_comment_disables_flag is removed from the example linter configuration. While this field is still present in the internal/config.Linter struct, its removal from the example might imply it's less commonly used or intended to be omitted by default. It's a minor inconsistency in the example, but not a functional issue.
* Feat/manager (#20)\n\n* chore: quick update feat/manager at 2026-01-21 22:56:12\n\n* chore: quick update feat/manager at 2026-01-21 23:01:56 * chore: quick update fix/release_token at 2026-01-21 23:06:18 * chore: quick update fix/release_token at 2026-01-21 23:15:58 * chore: quick update fix/release_token at 2026-01-21 23:17:31
No description provided.