Skip to content
Closed
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
8 changes: 6 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,15 @@ func newRootCommand() *cobra.Command {
if isLoadEnvAndSettings(cmd) {
// Set execution context (project root + workflow directory if applicable)
projectRootFlag := runtimeContext.Viper.GetString(settings.Flags.ProjectRoot.Name)
if err := context.SetExecutionContext(cmd, args, projectRootFlag, rootLogger); err != nil {
projectRoot, err := context.SetExecutionContext(cmd, args, projectRootFlag, rootLogger)
if err != nil {
return err
}

err := runtimeContext.AttachSettings(cmd)
// Store project root in runtime context for path resolution
runtimeContext.ProjectRootDir = projectRoot

err = runtimeContext.AttachSettings(cmd)
if err != nil {
return fmt.Errorf("%w", err)
}
Expand Down
86 changes: 44 additions & 42 deletions cmd/workflow/simulate/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common"
"github.com/smartcontractkit/cre-cli/internal/runtime"
"github.com/smartcontractkit/cre-cli/internal/settings"
"github.com/smartcontractkit/cre-cli/internal/transformation"
"github.com/smartcontractkit/cre-cli/internal/validation"
)

Expand Down Expand Up @@ -96,14 +97,16 @@ func New(runtimeContext *runtime.Context) *cobra.Command {
}

type handler struct {
log *zerolog.Logger
validated bool
log *zerolog.Logger
projectRootDir string
validated bool
}

func newHandler(ctx *runtime.Context) *handler {
return &handler{
log: ctx.Logger,
validated: false,
log: ctx.Logger,
projectRootDir: ctx.ProjectRootDir,
validated: false,
}
}

Expand Down Expand Up @@ -236,7 +239,7 @@ func (h *handler) Execute(inputs Inputs) error {
// if logger instance is set to DEBUG, that means verbosity flag is set by the user
verbosity := h.log.GetLevel() == zerolog.DebugLevel

return run(ctx, wasmFileBinary, config, secrets, inputs, verbosity)
return run(ctx, wasmFileBinary, config, secrets, inputs, verbosity, h.projectRootDir)
}

// run instantiates the engine, starts it and blocks until the context is canceled.
Expand All @@ -245,6 +248,7 @@ func run(
binary, config, secrets []byte,
inputs Inputs,
verbosity bool,
projectRootDir string,
) error {
logCfg := logger.Config{Level: getLevel(verbosity, zapcore.InfoLevel)}
baseLggr, err := logCfg.New()
Expand Down Expand Up @@ -352,9 +356,9 @@ func run(

getTriggerCaps := func() *ManualTriggers { return triggerCaps }
if inputs.NonInteractive {
triggerInfoAndBeforeStart.BeforeStart = makeBeforeStartNonInteractive(triggerInfoAndBeforeStart, inputs, getTriggerCaps)
triggerInfoAndBeforeStart.BeforeStart = makeBeforeStartNonInteractive(triggerInfoAndBeforeStart, inputs, getTriggerCaps, projectRootDir)
} else {
triggerInfoAndBeforeStart.BeforeStart = makeBeforeStartInteractive(triggerInfoAndBeforeStart, inputs, getTriggerCaps)
triggerInfoAndBeforeStart.BeforeStart = makeBeforeStartInteractive(triggerInfoAndBeforeStart, inputs, getTriggerCaps, projectRootDir)
}

waitFn := func(context.Context, simulator.RunnerConfig, *capabilities.Registry, []services.Service) {
Expand Down Expand Up @@ -480,7 +484,7 @@ type TriggerInfoAndBeforeStart struct {
}

// makeBeforeStartInteractive builds the interactive BeforeStart closure
func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs, triggerCapsGetter func() *ManualTriggers) func(context.Context, simulator.RunnerConfig, *capabilities.Registry, []services.Service, []*pb.TriggerSubscription) {
func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs, triggerCapsGetter func() *ManualTriggers, projectRootDir string) func(context.Context, simulator.RunnerConfig, *capabilities.Registry, []services.Service, []*pb.TriggerSubscription) {
return func(
ctx context.Context,
cfg simulator.RunnerConfig,
Expand Down Expand Up @@ -518,7 +522,7 @@ func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs
return triggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, time.Now())
}
case trigger == "http-trigger@1.0.0-alpha":
payload, err := getHTTPTriggerPayload()
payload, err := getHTTPTriggerPayload(projectRootDir)
if err != nil {
fmt.Printf("failed to get HTTP trigger payload: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -561,7 +565,7 @@ func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs
}

// makeBeforeStartNonInteractive builds the non-interactive BeforeStart closure
func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs, triggerCapsGetter func() *ManualTriggers) func(context.Context, simulator.RunnerConfig, *capabilities.Registry, []services.Service, []*pb.TriggerSubscription) {
func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs, triggerCapsGetter func() *ManualTriggers, projectRootDir string) func(context.Context, simulator.RunnerConfig, *capabilities.Registry, []services.Service, []*pb.TriggerSubscription) {
return func(
ctx context.Context,
cfg simulator.RunnerConfig,
Expand Down Expand Up @@ -597,7 +601,7 @@ func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inp
fmt.Println("--http-payload is required for http-trigger@1.0.0-alpha in non-interactive mode")
os.Exit(1)
}
payload, err := getHTTPTriggerPayloadFromInput(inputs.HTTPPayload)
payload, err := getHTTPTriggerPayloadFromInput(inputs.HTTPPayload, projectRootDir)
if err != nil {
fmt.Printf("failed to parse HTTP trigger payload: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -711,7 +715,7 @@ func getUserTriggerChoice(ctx context.Context, triggerSub []*pb.TriggerSubscript
}

// getHTTPTriggerPayload prompts user for HTTP trigger data
func getHTTPTriggerPayload() (*httptypedapi.Payload, error) {
func getHTTPTriggerPayload(projectRootDir string) (*httptypedapi.Payload, error) {
fmt.Println("\n🔍 HTTP Trigger Configuration:")
fmt.Println("Please provide JSON input for the HTTP trigger.")
fmt.Println("You can enter a file path or JSON directly.")
Expand All @@ -731,17 +735,23 @@ func getHTTPTriggerPayload() (*httptypedapi.Payload, error) {

var jsonData map[string]interface{}

// Check if input is a file path
if _, err := os.Stat(input); err == nil {
// Check if input is a file path (not inline JSON)
if !strings.HasPrefix(input, "{") && !strings.HasPrefix(input, "[") {
// Resolve path relative to project root
resolvedPath, err := transformation.ResolvePathRelativeTo(input, projectRootDir)
if err != nil {
return nil, fmt.Errorf("failed to resolve file path: %w", err)
}

// It's a file path
data, err := os.ReadFile(input)
data, err := os.ReadFile(resolvedPath)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", input, err)
return nil, fmt.Errorf("failed to read file %s: %w", resolvedPath, err)
}
if err := json.Unmarshal(data, &jsonData); err != nil {
return nil, fmt.Errorf("failed to parse JSON from file %s: %w", input, err)
return nil, fmt.Errorf("failed to parse JSON from file %s: %w", resolvedPath, err)
}
fmt.Printf("Loaded JSON from file: %s\n", input)
fmt.Printf("Loaded JSON from file: %s\n", resolvedPath)
} else {
// It's direct JSON input
if err := json.Unmarshal([]byte(input), &jsonData); err != nil {
Expand Down Expand Up @@ -859,42 +869,34 @@ func getEVMTriggerLog(ctx context.Context, ethClient *ethclient.Client) (*evm.Lo
}

// getHTTPTriggerPayloadFromInput builds an HTTP trigger payload from a JSON string or a file path (optionally prefixed with '@')
func getHTTPTriggerPayloadFromInput(input string) (*httptypedapi.Payload, error) {
func getHTTPTriggerPayloadFromInput(input string, projectRootDir string) (*httptypedapi.Payload, error) {
trimmed := strings.TrimSpace(input)
if trimmed == "" {
return nil, fmt.Errorf("empty http payload input")
}

// Strip optional "@" prefix for file paths
trimmed = strings.TrimPrefix(trimmed, "@")

var raw []byte
if strings.HasPrefix(trimmed, "@") {
path := strings.TrimPrefix(trimmed, "@")
data, err := os.ReadFile(path)
// Check if it looks like a file path (not inline JSON)
if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") {
// Resolve path relative to project root
resolvedPath, err := transformation.ResolvePathRelativeTo(trimmed, projectRootDir)
if err != nil {
return nil, fmt.Errorf("failed to resolve file path: %w", err)
}

data, err := os.ReadFile(resolvedPath)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", path, err)
return nil, fmt.Errorf("failed to read file %s: %w", resolvedPath, err)
}
raw = data
} else {
if _, err := os.Stat(trimmed); err == nil {
data, err := os.ReadFile(trimmed)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", trimmed, err)
}
raw = data
} else {
raw = []byte(trimmed)
}
// Inline JSON
raw = []byte(trimmed)
}

//var jsonData map[string]interface{}
//if err := json.Unmarshal(raw, &jsonData); err != nil {
// return nil, fmt.Errorf("failed to parse JSON: %w", err)
//}

//structPB, err := structpb.NewStruct(jsonData)
//if err != nil {
// return nil, fmt.Errorf("failed to convert to protobuf struct: %w", err)
//}

return &httptypedapi.Payload{Input: raw}, nil
}

Expand Down
24 changes: 14 additions & 10 deletions internal/context/project_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,49 @@ import (
)

// SetExecutionContext sets the appropriate execution context for commands
// It first sets the project context, then if it's a workflow command with exactly one argument,
// it changes to the specific workflow directory
func SetExecutionContext(cmd *cobra.Command, args []string, projectRootFlag string, logger *zerolog.Logger) error {
func SetExecutionContext(cmd *cobra.Command, args []string, projectRootFlag string, logger *zerolog.Logger) (string, error) {
// Check if project-root flag is set
var projectPath string
if projectRootFlag != "" {
// Resolve the path (handles both relative and absolute paths)
resolvedPath, err := filepath.Abs(projectRootFlag)
if err != nil {
return fmt.Errorf("failed to resolve project root path '%s': %w", projectRootFlag, err)
return "", fmt.Errorf("failed to resolve project root path '%s': %w", projectRootFlag, err)
}

// Check if path exists
if _, err := os.Stat(resolvedPath); os.IsNotExist(err) {
return fmt.Errorf("project root path does not exist: %s", resolvedPath)
return "", fmt.Errorf("project root path does not exist: %s", resolvedPath)
} else if err != nil {
return fmt.Errorf("failed to check project root path '%s': %w", resolvedPath, err)
return "", fmt.Errorf("failed to check project root path '%s': %w", resolvedPath, err)
}

projectPath = resolvedPath
}

// First, set the project context (change to project root)
if err := SetProjectContext(projectPath); err != nil {
return fmt.Errorf("failed to set project context: %w", err)
return "", fmt.Errorf("failed to set project context: %w", err)
}

// Capture the project root directory
projectRoot, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get project root directory: %w", err)
}

// Then, if it's a workflow command with exactly one argument, change to the workflow directory
if IsWorkflowCommand(cmd) && len(args) == 1 {
workflowDir, err := transformation.ResolveWorkflowPath(args[0])
if err != nil {
return fmt.Errorf("failed to resolve workflow directory path '%s': %w", args[0], err)
return "", fmt.Errorf("failed to resolve workflow directory path '%s': %w", args[0], err)
}
if err := os.Chdir(workflowDir); err != nil {
return fmt.Errorf("failed to change directory to %s: %w", workflowDir, err)
return "", fmt.Errorf("failed to change directory to %s: %w", workflowDir, err)
}
}

return nil
return projectRoot, nil
}

// SetProjectContext sets the current working directory to the project root
Expand Down
11 changes: 10 additions & 1 deletion internal/context/project_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ test-profile:
}

cmd := tt.cmdSetup()
err := SetExecutionContext(cmd, tt.args, projectRootFlag, &logger)
projectRoot, err := SetExecutionContext(cmd, tt.args, projectRootFlag, &logger)

if tt.expectError {
require.Error(t, err)
Expand All @@ -320,6 +320,15 @@ test-profile:

require.NoError(t, err)

// Verify project root was returned correctly
if expectedDir != "" {
resolvedProjectRoot, err := filepath.EvalSymlinks(projectRoot)
require.NoError(t, err)
resolvedExpectedDir, err := filepath.EvalSymlinks(expectedDir)
require.NoError(t, err)
assert.Equal(t, resolvedExpectedDir, resolvedProjectRoot, "returned project root should match expected directory")
}

// Verify we're in the correct directory
if expectedDir != "" {
currentDir, err := os.Getwd()
Expand Down
1 change: 1 addition & 0 deletions internal/runtime/runtime_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Context struct {
Settings *settings.Settings
Credentials *credentials.Credentials
EnvironmentSet *environments.EnvironmentSet
ProjectRootDir string
}

func NewContext(logger *zerolog.Logger, viper *viper.Viper) *Context {
Expand Down
23 changes: 23 additions & 0 deletions internal/transformation/path_resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,29 @@ func ResolveWorkflowPath(workflowPath string) (string, error) {
return absPath, nil
}

// ResolvePathRelativeTo resolves a file path relative to a base directory.
// If the path is already absolute, it returns it unchanged.
// If the path is relative, it resolves it against the baseDir.
func ResolvePathRelativeTo(path string, baseDir string) (string, error) {
if path == "" {
return "", nil
}

// If already absolute, return as-is
if filepath.IsAbs(path) {
return path, nil
}

// Resolve relative path against base directory
absPath := filepath.Join(baseDir, path)
absPath, err := filepath.Abs(absPath)
if err != nil {
return "", errors.New("failed to resolve absolute path: " + err.Error())
}

return absPath, nil
}

// resolveAbsolutePath expands ~ and converts a path to an absolute path.
func resolveAbsolutePath(input string) (string, error) {
// Expand ~ to home directory
Expand Down
Loading
Loading