Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions cli/core/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,17 +246,48 @@ func HasPythonEntryFile(directory string) bool {

// HasGoEntryFile checks if common Go entry files exist in the given directory
func HasGoEntryFile(directory string) bool {
entryFile, err := FindGoEntryFile(directory)
return err == nil && entryFile != ""
}

var safeGoCmdEntrypointPattern = regexp.MustCompile(`^cmd/[A-Za-z0-9_.-]+/main\.go$`)

// FindGoEntryFile finds a single automatic Go entrypoint in the given directory.
func FindGoEntryFile(directory string) (string, error) {
files := []string{
"main.go",
"src/main.go",
"cmd/main.go",
}
for _, f := range files {
if _, err := os.Stat(filepath.Join(directory, f)); err == nil {
return true
return f, nil
}
}
return false

matches, err := filepath.Glob(filepath.Join(directory, "cmd", "*", "main.go"))
if err != nil {
return "", fmt.Errorf("error finding Go entrypoint: %w", err)
}
candidates := make([]string, 0, len(matches))
for _, match := range matches {
rel, err := filepath.Rel(directory, match)
if err != nil {
return "", fmt.Errorf("error finding Go entrypoint: %w", err)
}
rel = filepath.ToSlash(rel)
if !safeGoCmdEntrypointPattern.MatchString(rel) {
return "", fmt.Errorf("unsupported Go entrypoint path %q; automatic cmd/*/main.go detection only supports command directory names with letters, numbers, dots, underscores, and hyphens; configure [entrypoint] prod = \"go run ./cmd/<name>\" in blaxel.toml", rel)
}
candidates = append(candidates, rel)
}
if len(candidates) == 0 {
return "", nil
}
if len(candidates) > 1 {
return "", fmt.Errorf("multiple Go entrypoints found under cmd/*/main.go (%s); configure [entrypoint] prod = \"go run ./cmd/<name>\" in blaxel.toml", strings.Join(candidates, ", "))
}
return candidates[0], nil
}

// HasTypeScriptEntryFile checks if common TypeScript/JavaScript entry files exist in the given directory
Expand Down
74 changes: 74 additions & 0 deletions cli/core/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,87 @@ func TestHasGoEntryFile(t *testing.T) {
assert.True(t, HasGoEntryFile(dir))
})

t.Run("finds cmd named main.go", func(t *testing.T) {
dir := filepath.Join(tempDir, "cmd_named_main")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "dummy_mcp"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "dummy_mcp", "main.go"), []byte("package main"), 0644))

assert.True(t, HasGoEntryFile(dir))
})

t.Run("returns false when no entry file", func(t *testing.T) {
dir := filepath.Join(tempDir, "no_entry")
require.NoError(t, os.MkdirAll(dir, 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "utils.go"), []byte("package utils"), 0644))

assert.False(t, HasGoEntryFile(dir))
})

t.Run("returns false when cmd entrypoint is ambiguous", func(t *testing.T) {
dir := filepath.Join(tempDir, "ambiguous_cmd")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "api"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "worker"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "api", "main.go"), []byte("package main"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "worker", "main.go"), []byte("package main"), 0644))

assert.False(t, HasGoEntryFile(dir))
})
}

func TestFindGoEntryFile(t *testing.T) {
tempDir, err := os.MkdirTemp("", "find_go_entry_test")
require.NoError(t, err)
defer func() { _ = os.RemoveAll(tempDir) }()

t.Run("prefers conventional main.go", func(t *testing.T) {
dir := filepath.Join(tempDir, "prefer_conventional")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "dummy_mcp"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "dummy_mcp", "main.go"), []byte("package main"), 0644))

entryFile, err := FindGoEntryFile(dir)
require.NoError(t, err)
assert.Equal(t, "main.go", entryFile)
})

t.Run("finds single cmd named entrypoint", func(t *testing.T) {
dir := filepath.Join(tempDir, "cmd_named")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "dummy_mcp"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "dummy_mcp", "main.go"), []byte("package main"), 0644))

entryFile, err := FindGoEntryFile(dir)
require.NoError(t, err)
assert.Equal(t, "cmd/dummy_mcp/main.go", entryFile)
})

t.Run("errors on multiple cmd named entrypoints", func(t *testing.T) {
dir := filepath.Join(tempDir, "multiple_cmd")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "api"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "worker"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "api", "main.go"), []byte("package main"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "worker", "main.go"), []byte("package main"), 0644))

entryFile, err := FindGoEntryFile(dir)
assert.Empty(t, entryFile)
require.Error(t, err)
assert.Contains(t, err.Error(), "multiple Go entrypoints")
assert.Contains(t, err.Error(), "cmd/api/main.go")
assert.Contains(t, err.Error(), "cmd/worker/main.go")
assert.Contains(t, err.Error(), "[entrypoint]")
})

t.Run("rejects unsafe cmd named entrypoint", func(t *testing.T) {
dir := filepath.Join(tempDir, "unsafe_cmd")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "dummy;echo pwned"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "dummy;echo pwned", "main.go"), []byte("package main"), 0644))

entryFile, err := FindGoEntryFile(dir)
assert.Empty(t, entryFile)
require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported Go entrypoint path")
assert.Contains(t, err.Error(), "cmd/dummy;echo pwned/main.go")
assert.Contains(t, err.Error(), "[entrypoint]")
})
}

func TestHasTypeScriptEntryFile(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cli/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ func ValidateBuildConfig(cwd, folder string, config core.Config) string {
fmt.Fprintf(&warningMsg, " • Add automatic entrypoint %s OR\n", pythonFiles)
warningMsg.WriteString(entrypointSection)
case "go":
goFiles := codeColor.Sprint("main.go, src/main.go, cmd/main.go")
goFiles := codeColor.Sprint("main.go, src/main.go, cmd/main.go, cmd/<name>/main.go")
fmt.Fprintf(&warningMsg, " • Add automatic entrypoint %s OR\n", goFiles)
warningMsg.WriteString(entrypointSection)
case "typescript":
Expand Down
16 changes: 16 additions & 0 deletions cli/server/commands_go.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/blaxel-ai/toolkit/cli/core"
Expand Down Expand Up @@ -77,5 +78,20 @@ func findGoRootCmdAsString(cfg RootCmdConfig) ([]string, error) {
}
return strings.Split(cfg.Entrypoint.Production, " "), nil
}
entryFile, err := core.FindGoEntryFile(cfg.Folder)
if err != nil {
return nil, err
}
if entryFile != "" {
return []string{"go", "run", goRunTargetFromEntryFile(entryFile)}, nil
Comment thread
mstolarzblaxelai marked this conversation as resolved.
}
return nil, fmt.Errorf("entrypoint not found in config")
}

func goRunTargetFromEntryFile(entryFile string) string {
entryDir := filepath.Dir(entryFile)
if entryDir == "." {
return "."
}
return "./" + filepath.ToSlash(entryDir)
}
74 changes: 74 additions & 0 deletions cli/server/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,80 @@ func TestFindRootCmdAsStringWithAutoDetection(t *testing.T) {
assert.NotEmpty(t, cmd)
}
})

t.Run("detects go with cmd named main.go", func(t *testing.T) {
dir := filepath.Join(tempDir, "go_cmd_named")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "dummy_mcp"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/dummy"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "dummy_mcp", "main.go"), []byte("package main"), 0644))

cfg := RootCmdConfig{
Folder: dir,
Hotreload: false,
}

cmd, err := FindRootCmdAsString(cfg)
require.NoError(t, err)
assert.Equal(t, []string{"go", "run", "./cmd/dummy_mcp"}, cmd)
})

t.Run("errors when go cmd entrypoint is ambiguous", func(t *testing.T) {
dir := filepath.Join(tempDir, "go_cmd_ambiguous")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "api"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "worker"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/dummy"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "api", "main.go"), []byte("package main"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "worker", "main.go"), []byte("package main"), 0644))

cfg := RootCmdConfig{
Folder: dir,
Hotreload: false,
}

_, err := FindRootCmdAsString(cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "multiple Go entrypoints")
assert.Contains(t, err.Error(), "cmd/api/main.go")
assert.Contains(t, err.Error(), "cmd/worker/main.go")
})

t.Run("rejects unsafe go cmd entrypoint before command construction", func(t *testing.T) {
dir := filepath.Join(tempDir, "go_cmd_unsafe")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "dummy;echo pwned"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/dummy"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "dummy;echo pwned", "main.go"), []byte("package main"), 0644))

cfg := RootCmdConfig{
Folder: dir,
Hotreload: false,
}

_, err := FindRootCmdAsString(cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported Go entrypoint path")
assert.Contains(t, err.Error(), "cmd/dummy;echo pwned/main.go")
})

t.Run("uses explicit go entrypoint before auto detection", func(t *testing.T) {
dir := filepath.Join(tempDir, "go_explicit")
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "api"), 0755))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "cmd", "worker"), 0755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/dummy"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "api", "main.go"), []byte("package main"), 0644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "cmd", "worker", "main.go"), []byte("package main"), 0644))

cfg := RootCmdConfig{
Folder: dir,
Hotreload: false,
Entrypoint: core.Entrypoints{
Production: "go run ./cmd/api",
},
}

cmd, err := FindRootCmdAsString(cfg)
require.NoError(t, err)
assert.Equal(t, []string{"go", "run", "./cmd/api"}, cmd)
})
}

func TestGetServerEnvironmentWithSecrets(t *testing.T) {
Expand Down
Loading