- Add TAI_TEST_HOST_IP environment variable to CI workflows for unit tests, allowing better connectivity to the gRPC server from Docker containers. - Update the `run.go` file to parse command-line arguments correctly. - Modify the test utility to return the gRPC address reachable from Docker, improving integration test reliability. - Refactor integration tests to utilize the new relay address function, ensuring proper communication with the Yao gRPC server. These changes improve the testing framework and enhance the configuration for better service interaction during CI runs.
330 lines
7.6 KiB
Go
330 lines
7.6 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/fatih/color"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/spf13/cobra"
|
|
"github.com/yaoapp/gou/helper"
|
|
"github.com/yaoapp/gou/plugin"
|
|
"github.com/yaoapp/gou/process"
|
|
"github.com/yaoapp/kun/exception"
|
|
"github.com/yaoapp/yao/config"
|
|
"github.com/yaoapp/yao/engine"
|
|
ischedule "github.com/yaoapp/yao/schedule"
|
|
"github.com/yaoapp/yao/share"
|
|
taigrpc "github.com/yaoapp/yao/tai/grpc"
|
|
itask "github.com/yaoapp/yao/task"
|
|
)
|
|
|
|
var runSilent = false
|
|
var runAuthPath string
|
|
|
|
var runCmd = &cobra.Command{
|
|
Use: "run",
|
|
Short: L("Execute process"),
|
|
Long: L("Execute process"),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
|
|
// Resolve credential: --auth flag > ~/.yao/credentials > nil (local mode)
|
|
cred := resolveCredential()
|
|
|
|
if cred != nil {
|
|
runGRPC(cred, args)
|
|
return
|
|
}
|
|
|
|
runLocal(args)
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
runCmd.PersistentFlags().BoolVarP(&runSilent, "silent", "s", false, L("Silent mode"))
|
|
runCmd.PersistentFlags().StringVar(&runAuthPath, "auth", "", L("Path to credentials file"))
|
|
}
|
|
|
|
// resolveCredential loads credential from --auth flag or default path.
|
|
func resolveCredential() *Credential {
|
|
if runAuthPath != "" {
|
|
cred, err := LoadCredentialFrom(runAuthPath)
|
|
if err != nil {
|
|
color.Red(" %s %s\n", L("Failed to load credentials:"), err)
|
|
os.Exit(1)
|
|
}
|
|
return cred
|
|
}
|
|
|
|
cred, _ := LoadCredential()
|
|
return cred
|
|
}
|
|
|
|
// runGRPC executes a process via the remote gRPC server.
|
|
func runGRPC(cred *Credential, args []string) {
|
|
if len(args) < 1 {
|
|
if !runSilent {
|
|
color.Red(L("Not enough arguments\n"))
|
|
color.White(share.BUILDNAME + " help\n")
|
|
} else {
|
|
fmt.Print(L("Not enough arguments\n"))
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|
|
if cred.GRPCAddr == "" {
|
|
color.Red(" %s\n", L("No gRPC address in credentials. Please re-login."))
|
|
os.Exit(1)
|
|
}
|
|
|
|
name := args[0]
|
|
if !runSilent {
|
|
color.Green(L("Run: %s gRPC: %s\n"), name, cred.GRPCAddr)
|
|
}
|
|
|
|
pargs := parseRunArgs(args)
|
|
|
|
argsJSON, err := jsoniter.Marshal(pargs)
|
|
if err != nil {
|
|
color.Red(" %s %s\n", L("Arguments:"), err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
tm := taigrpc.NewTokenManager(cred.AccessToken, cred.RefreshToken, "", "")
|
|
client, err := taigrpc.Dial(cred.GRPCAddr, tm)
|
|
if err != nil {
|
|
color.Red(" %s %s\n", L("gRPC connect failed:"), err.Error())
|
|
os.Exit(1)
|
|
}
|
|
defer client.Close()
|
|
|
|
data, err := client.Run(context.Background(), name, argsJSON, 0)
|
|
if err != nil {
|
|
if !runSilent {
|
|
color.Red(" %s %s\n", L("Process:"), err.Error())
|
|
} else {
|
|
fmt.Printf("%s\n", err.Error())
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
|
|
if !runSilent {
|
|
color.White("--------------------------------------\n")
|
|
color.White(L("%s Response\n"), name)
|
|
color.White("--------------------------------------\n")
|
|
var res interface{}
|
|
if jsoniter.Unmarshal(data, &res) == nil {
|
|
helper.Dump(res)
|
|
} else {
|
|
fmt.Printf("%s\n", data)
|
|
}
|
|
color.White("--------------------------------------\n")
|
|
fmt.Printf("\033[32m✨DONE✨\033[0m \033[90mgRPC: %s\033[0m\n", cred.GRPCAddr)
|
|
} else {
|
|
fmt.Printf("%s\n", data)
|
|
}
|
|
}
|
|
|
|
// runLocal executes a process locally (existing behavior).
|
|
func runLocal(args []string) {
|
|
defer share.SessionStop()
|
|
defer plugin.KillAll()
|
|
|
|
defer func() {
|
|
err := exception.Catch(recover())
|
|
if err != nil {
|
|
if !runSilent {
|
|
color.Red(L("Fatal: %s\n"), err.Error())
|
|
return
|
|
}
|
|
fmt.Printf("%s\n", err.Error())
|
|
}
|
|
}()
|
|
|
|
// Auto-detect app root if not specified
|
|
if appPath == "" {
|
|
cwd, err := os.Getwd()
|
|
if err == nil {
|
|
if root, err := findAppRootFromPath(cwd); err == nil {
|
|
appPath = root
|
|
}
|
|
}
|
|
}
|
|
|
|
Boot()
|
|
|
|
// Set Runtime Mode
|
|
config.Conf.Runtime.Mode = "standard"
|
|
|
|
cfg := config.Conf
|
|
cfg.Session.IsCLI = true
|
|
if len(args) < 1 {
|
|
if !runSilent {
|
|
color.Red(L("Not enough arguments\n"))
|
|
color.White(share.BUILDNAME + " help\n")
|
|
return
|
|
}
|
|
fmt.Print(L("Not enough arguments\n"))
|
|
return
|
|
}
|
|
|
|
loadWarnings, err := engine.Load(cfg, engine.LoadOption{Action: "run"})
|
|
if err != nil {
|
|
if !runSilent {
|
|
color.Red(L("Engine: %s\n"), err.Error())
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%s\n", err.Error())
|
|
return
|
|
}
|
|
|
|
name := args[0]
|
|
if !runSilent {
|
|
color.Green(L("Run: %s\n"), name)
|
|
}
|
|
|
|
pargs := parseRunArgs(args)
|
|
|
|
// Start Tasks
|
|
itask.Start()
|
|
defer itask.Stop()
|
|
|
|
// Start Schedules
|
|
ischedule.Start()
|
|
defer ischedule.Stop()
|
|
|
|
p := process.NewWithContext(context.Background(), name, pargs...)
|
|
res, err := p.Exec()
|
|
if err != nil {
|
|
if !runSilent {
|
|
color.Red(L("Process: %s\n"), fmt.Sprintf("%s", strings.TrimPrefix(err.Error(), "Exception|404:")))
|
|
return
|
|
}
|
|
fmt.Printf("%s\n", err.Error())
|
|
return
|
|
}
|
|
|
|
if !runSilent {
|
|
|
|
if len(loadWarnings) > 0 {
|
|
fmt.Println(color.YellowString("---------------------------------"))
|
|
fmt.Println(color.YellowString(L("Warnings")))
|
|
fmt.Println(color.YellowString("---------------------------------"))
|
|
for _, warning := range loadWarnings {
|
|
fmt.Println(color.YellowString("[%s] %s", warning.Widget, warning.Error))
|
|
}
|
|
fmt.Printf("\n")
|
|
}
|
|
|
|
color.White("--------------------------------------\n")
|
|
color.White(L("%s Response\n"), name)
|
|
color.White("--------------------------------------\n")
|
|
helper.Dump(res)
|
|
color.White("--------------------------------------\n")
|
|
color.Green(L("✨DONE✨\n"))
|
|
return
|
|
}
|
|
|
|
// Silent mode output
|
|
switch res.(type) {
|
|
|
|
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool:
|
|
fmt.Printf("%v\n", res)
|
|
return
|
|
|
|
case string, []byte:
|
|
fmt.Printf("%s\n", res)
|
|
return
|
|
|
|
default:
|
|
txt, err := jsoniter.Marshal(res)
|
|
if err != nil {
|
|
fmt.Printf("%s\n", err.Error())
|
|
}
|
|
fmt.Printf("%s\n", txt)
|
|
}
|
|
}
|
|
|
|
// parseRunArgs parses the CLI arguments into process arguments, handling :: prefixed JSON.
|
|
func parseRunArgs(args []string) []interface{} {
|
|
pargs := []interface{}{}
|
|
for i, arg := range args {
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(arg, "::") {
|
|
raw := strings.TrimPrefix(arg, "::")
|
|
var v interface{}
|
|
err := jsoniter.Unmarshal([]byte(raw), &v)
|
|
if err != nil {
|
|
color.Red(L("Arguments: %s\n"), err.Error())
|
|
return pargs
|
|
}
|
|
pargs = append(pargs, v)
|
|
if !runSilent {
|
|
color.White("args[%d]: %s\n", i-1, raw)
|
|
}
|
|
} else if strings.HasPrefix(arg, "\\::") {
|
|
cleaned := "::" + strings.TrimPrefix(arg, "\\::")
|
|
pargs = append(pargs, cleaned)
|
|
if !runSilent {
|
|
color.White("args[%d]: %s\n", i-1, cleaned)
|
|
}
|
|
} else {
|
|
pargs = append(pargs, arg)
|
|
if !runSilent {
|
|
color.White("args[%d]: %s\n", i-1, arg)
|
|
}
|
|
}
|
|
}
|
|
return pargs
|
|
}
|
|
|
|
// findAppRootFromPath finds the Yao application root directory by looking for app.yao
|
|
// It traverses up from the given path until it finds app.yao or reaches the filesystem root
|
|
func findAppRootFromPath(startPath string) (string, error) {
|
|
// Get absolute path
|
|
absPath, err := filepath.Abs(startPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
|
}
|
|
|
|
// If it's a file, start from its directory
|
|
info, err := os.Stat(absPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("path not found: %s", absPath)
|
|
}
|
|
|
|
var dir string
|
|
if info.IsDir() {
|
|
dir = absPath
|
|
} else {
|
|
dir = filepath.Dir(absPath)
|
|
}
|
|
|
|
// Traverse up to find app.yao
|
|
for {
|
|
// Check for app.yao, app.json, or app.jsonc
|
|
for _, appFile := range []string{"app.yao", "app.json", "app.jsonc"} {
|
|
appFilePath := filepath.Join(dir, appFile)
|
|
if _, err := os.Stat(appFilePath); err == nil {
|
|
return dir, nil
|
|
}
|
|
}
|
|
|
|
// Move to parent directory
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
// Reached root, no app.yao found
|
|
break
|
|
}
|
|
dir = parent
|
|
}
|
|
|
|
return "", fmt.Errorf("no app.yao found in path hierarchy of %s", startPath)
|
|
}
|