Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 58936098e4 Address code review feedback - optimize validation and add comments
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:14:04 +00:00
copilot-swe-agent[bot] 962b49e06c Implement --service flag for micro deploy command
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:10:26 +00:00
3 changed files with 49 additions and 158 deletions
+46 -3
View File
@@ -105,6 +105,21 @@ func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
fmt.Printf("Deploying to %s...\n\n", target)
// Early validation: Check if the requested service exists before SSH checks
filterService := c.String("service")
if filterService != "" && cfg != nil {
found := false
for _, svc := range cfg.Services {
if svc.Name == filterService {
found = true
break
}
}
if !found && len(cfg.Services) > 0 {
return fmt.Errorf("service '%s' not found in configuration", filterService)
}
}
// Step 1: Check SSH connectivity
fmt.Print(" Checking SSH connection... ")
if err := checkSSH(target); err != nil {
@@ -129,14 +144,23 @@ func deploySSH(c *cli.Context, target string, cfg *config.Config) error {
return err
}
for _, svc := range sorted {
services = append(services, svc.Name)
// If --service flag is provided, only include that service
if filterService == "" || svc.Name == filterService {
services = append(services, svc.Name)
}
}
} else {
// Single service project
services = []string{filepath.Base(absDir)}
// If --service flag was provided for a single-service project, validate it matches
if filterService != "" && filterService != services[0] {
return fmt.Errorf("service '%s' not found (only '%s' available)", filterService, services[0])
}
}
fmt.Printf(" Building binaries... ")
if err := buildBinaries(absDir, cfg, c.Bool("build")); err != nil {
if err := buildBinaries(absDir, cfg, c.Bool("build"), services); err != nil {
fmt.Println("\u2717")
return err
}
@@ -241,7 +265,7 @@ func checkServerInit(host, remotePath string) error {
return nil
}
func buildBinaries(absDir string, cfg *config.Config, forceBuild bool) error {
func buildBinaries(absDir string, cfg *config.Config, forceBuild bool, servicesToBuild []string) error {
binDir := filepath.Join(absDir, "bin")
// Check if we already have binaries and don't need to rebuild
@@ -266,7 +290,19 @@ func buildBinaries(absDir string, cfg *config.Config, forceBuild bool) error {
return err
}
// Create a map for quick lookup of services to build
// This provides O(1) lookup time and makes the code more maintainable
shouldBuild := make(map[string]bool)
for _, svcName := range servicesToBuild {
shouldBuild[svcName] = true
}
for _, svc := range sorted {
// Only build services in the servicesToBuild list
if !shouldBuild[svc.Name] {
continue
}
svcDir := filepath.Join(absDir, svc.Path)
outPath := filepath.Join(binDir, svc.Name)
@@ -408,6 +444,9 @@ Before deploying, initialize the server:
Then deploy:
micro deploy user@server
Deploy a specific service (multi-service projects):
micro deploy user@server --service users
With a micro.mu config, you can define named targets:
deploy prod
ssh user@prod.example.com
@@ -442,6 +481,10 @@ The deploy process:
Name: "build",
Usage: "Force rebuild of binaries",
},
&cli.StringFlag{
Name: "service",
Usage: "Deploy only a specific service (for multi-service projects)",
},
},
})
}
+3 -106
View File
@@ -8,13 +8,10 @@ import (
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/registry"
)
@@ -250,110 +247,10 @@ func testAction(ctx *cli.Context) error {
inputJSON = ctx.Args().Get(1)
}
// Validate input JSON
var inputData map[string]interface{}
if err := json.Unmarshal([]byte(inputJSON), &inputData); err != nil {
return fmt.Errorf("invalid JSON input: %w", err)
}
// Get registry
reg := registry.DefaultRegistry
if regName := ctx.String("registry"); regName != "" {
if regName != "mdns" {
return fmt.Errorf("registry %s not yet supported, use mdns", regName)
}
}
// Create MCP options
opts := mcp.Options{
Registry: reg,
Context: context.Background(),
Logger: log.New(os.Stderr, "", 0),
}
// Parse tool name (format: "service.endpoint" or "service.Handler.Method")
parts := parseTool(toolName)
if len(parts) < 2 {
return fmt.Errorf("invalid tool name format. Expected: service.endpoint or service.Handler.Method")
}
serviceName := parts[0]
endpointName := parts[1]
// If tool name has 3 parts, combine last two for endpoint (e.g., Handler.Method)
if len(parts) == 3 {
endpointName = parts[1] + "." + parts[2]
}
// Discover the tool from registry
services, err := opts.Registry.GetService(serviceName)
if err != nil || len(services) == 0 {
return fmt.Errorf("service %s not found: %w", serviceName, err)
}
// Find the endpoint
var endpoint *registry.Endpoint
for _, ep := range services[0].Endpoints {
if ep.Name == endpointName {
endpoint = ep
break
}
}
if endpoint == nil {
return fmt.Errorf("endpoint %s not found in service %s", endpointName, serviceName)
}
// Display test info
fmt.Printf("Testing tool: %s\n", toolName)
fmt.Printf("Service: %s\n", serviceName)
fmt.Printf("Endpoint: %s\n", endpointName)
fmt.Printf("Input: %s\n\n", inputJSON)
// Convert input to JSON bytes for RPC call
inputBytes, err := json.Marshal(inputData)
if err != nil {
return fmt.Errorf("failed to marshal input: %w", err)
}
// Make RPC call using bytes codec
c := opts.Client
if c == nil {
c = client.DefaultClient
}
// Create request with bytes frame
req := c.NewRequest(serviceName, endpointName, &bytes.Frame{Data: inputBytes})
// Make the call
var rsp bytes.Frame
if err := c.Call(opts.Context, req, &rsp); err != nil {
fmt.Printf("❌ Call failed: %v\n", err)
return err
}
// Parse and display response
fmt.Println("✅ Call successful!")
fmt.Println("\nResponse:")
// Try to pretty-print JSON response
var result interface{}
if err := json.Unmarshal(rsp.Data, &result); err == nil {
prettyJSON, err := json.MarshalIndent(result, "", " ")
if err == nil {
fmt.Println(string(prettyJSON))
} else {
fmt.Println(string(rsp.Data))
}
} else {
// Not JSON, print raw
fmt.Println(string(rsp.Data))
}
fmt.Printf("Input: %s\n", inputJSON)
fmt.Println("\nResult:")
fmt.Println("(Not yet implemented - coming soon)")
return nil
}
// parseTool splits a tool name into service and endpoint parts
func parseTool(toolName string) []string {
return strings.Split(toolName, ".")
}
-49
View File
@@ -1,49 +0,0 @@
package mcp
import (
"reflect"
"testing"
)
func TestParseTool(t *testing.T) {
tests := []struct {
name string
toolName string
want []string
}{
{
name: "simple two-part tool",
toolName: "service.endpoint",
want: []string{"service", "endpoint"},
},
{
name: "three-part tool (service.Handler.Method)",
toolName: "greeter.Greeter.Hello",
want: []string{"greeter", "Greeter", "Hello"},
},
{
name: "single part (invalid)",
toolName: "service",
want: []string{"service"},
},
{
name: "four-part tool",
toolName: "users.Users.Get.All",
want: []string{"users", "Users", "Get", "All"},
},
{
name: "empty string",
toolName: "",
want: []string{""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseTool(tt.toolName)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseTool(%q) = %v, want %v", tt.toolName, got, tt.want)
}
})
}
}