From 60566b705059b49a265848714c1744243e509f3a Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Fri, 23 Jan 2026 09:23:51 -0500 Subject: [PATCH 01/11] Bring your own WASM --- Makefile | 6 +- cmd/common/compile.go | 153 +++++++++ cmd/common/compile_test.go | 128 +++++++ cmd/common/utils.go | 76 ++--- cmd/creinit/creinit.go | 53 ++- .../workflow/wasmBlankTemplate/Makefile.tpl | 9 + .../workflow/wasmBlankTemplate/README.md | 32 ++ .../wasmBlankTemplate/config.production.json | 1 + .../wasmBlankTemplate/config.staging.json | 1 + .../workflow/wasmBlankTemplate/secrets.yaml | 1 + .../workflow/wasmBlankTemplate/wasm/README.md | 3 + cmd/root.go | 35 +- cmd/workflow/convert/convert.go | 145 ++++++++ cmd/workflow/convert/convert_test.go | 120 +++++++ cmd/workflow/deploy/compile.go | 62 +--- cmd/workflow/deploy/compile_test.go | 314 ++++++------------ cmd/workflow/deploy/deploy.go | 2 +- .../testdata/custom_wasm_workflow/Makefile | 4 + .../testdata/custom_wasm_workflow/config.yml | 3 + .../testdata/custom_wasm_workflow/go.mod | 68 ++++ .../testdata/custom_wasm_workflow/go.sum | 158 +++++++++ .../testdata/custom_wasm_workflow/main.go | 74 +++++ .../deploy/testdata/wasm_make_fails/Makefile | 4 + cmd/workflow/simulate/simulate.go | 62 +--- cmd/workflow/simulate/simulate_test.go | 13 +- cmd/workflow/workflow.go | 2 + internal/constants/constants.go | 1 + internal/settings/workflow_settings.go | 67 ++++ internal/testutil/graphql_mock.go | 42 +++ internal/validation/files/path_read.go | 35 +- internal/validation/validation.go | 20 +- test/convert_simulate_helper.go | 77 +++++ test/graphql_mock.go | 14 + ...binding_generation_and_simulate_go_test.go | 39 +-- test/init_and_simulate_ts_test.go | 39 +-- test/init_and_simulate_wasm_test.go | 215 ++++++++++++ test/init_convert_simulate_go_test.go | 116 +++++++ test/init_convert_simulate_ts_test.go | 147 ++++++++ .../workflow_simulator_path.go | 33 +- 39 files changed, 1872 insertions(+), 502 deletions(-) create mode 100644 cmd/common/compile.go create mode 100644 cmd/common/compile_test.go create mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl create mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/README.md create mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/config.production.json create mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/config.staging.json create mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/secrets.yaml create mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/wasm/README.md create mode 100644 cmd/workflow/convert/convert.go create mode 100644 cmd/workflow/convert/convert_test.go create mode 100644 cmd/workflow/deploy/testdata/custom_wasm_workflow/Makefile create mode 100644 cmd/workflow/deploy/testdata/custom_wasm_workflow/config.yml create mode 100644 cmd/workflow/deploy/testdata/custom_wasm_workflow/go.mod create mode 100644 cmd/workflow/deploy/testdata/custom_wasm_workflow/go.sum create mode 100644 cmd/workflow/deploy/testdata/custom_wasm_workflow/main.go create mode 100644 cmd/workflow/deploy/testdata/wasm_make_fails/Makefile create mode 100644 internal/testutil/graphql_mock.go create mode 100644 test/convert_simulate_helper.go create mode 100644 test/graphql_mock.go create mode 100644 test/init_and_simulate_wasm_test.go create mode 100644 test/init_convert_simulate_go_test.go create mode 100644 test/init_convert_simulate_ts_test.go diff --git a/Makefile b/Makefile index d96186c3..1d55f8c6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build build-admin lint test test-e2e clean goreleaser-dev-build install-tools install-foundry run-op gendoc +.PHONY: all build build-admin lint test test-e2e test-quick clean goreleaser-dev-build install-tools install-foundry run-op gendoc # Go parameters COMMIT_SHA = $(shell git rev-parse HEAD) @@ -29,6 +29,10 @@ test: lint test-e2e: $(GOTEST) -v -p 5 ./test/ +# test-quick: run tests with 30s timeout, skipping slow/flaky e2e tests. Use -short so TestE2EInit_ConvertToCustomBuild_TS is skipped. +test-quick: + $(GOTEST) ./... -v -short -skip 'MultiCommandHappyPaths|TestPostToGateway|TestBlankWorkflowSimulation|TestWaitForBackendLinkProcessing|TestTryAutoLink|TestCheckLinkStatusViaGraphQL|Fails to run tests with invalid Go code' -timeout 30s + clean: $(GOCLEAN) rm -f $(BINARY_NAME) diff --git a/cmd/common/compile.go b/cmd/common/compile.go new file mode 100644 index 00000000..2f1798cd --- /dev/null +++ b/cmd/common/compile.go @@ -0,0 +1,153 @@ +package common + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/smartcontractkit/cre-cli/internal/constants" +) + +const ( + makefileName = "Makefile" + defaultWasmOutput = "wasm/workflow.wasm" // hardcoded in Makefile by convert; must match workflow-path default +) + +// getBuildCmd returns a single step that builds the workflow and returns the WASM bytes. +func getBuildCmd(workflowRootFolder, mainFile, language string) (func() ([]byte, error), error) { + tmpPath := filepath.Join(workflowRootFolder, ".cre_build_tmp.wasm") + switch language { + case constants.WorkflowLanguageTypeScript: + cmd := exec.Command("bun", "cre-compile", mainFile, tmpPath) + cmd.Dir = workflowRootFolder + return func() ([]byte, error) { + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("%w\nbuild output:\n%s", err, strings.TrimSpace(string(out))) + } + b, err := os.ReadFile(tmpPath) + _ = os.Remove(tmpPath) + return b, err + }, nil + case constants.WorkflowLanguageGolang: + // Build the package (.) so all .go files (main.go, workflow.go, etc.) are compiled together + cmd := exec.Command( + "go", "build", + "-o", tmpPath, + "-trimpath", + "-ldflags=-buildid= -w -s", + ".", + ) + cmd.Dir = workflowRootFolder + cmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm", "CGO_ENABLED=0") + return func() ([]byte, error) { + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("%w\nbuild output:\n%s", err, strings.TrimSpace(string(out))) + } + b, err := os.ReadFile(tmpPath) + _ = os.Remove(tmpPath) + return b, err + }, nil + case constants.WorkflowLanguageWasm: + makeRoot, err := findMakefileRoot(workflowRootFolder) + if err != nil { + return nil, err + } + makeCmd := exec.Command("make", "build") + makeCmd.Dir = makeRoot + builtPath := filepath.Join(makeRoot, defaultWasmOutput) + return func() ([]byte, error) { + out, err := makeCmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("%w\nbuild output:\n%s", err, strings.TrimSpace(string(out))) + } + return os.ReadFile(builtPath) + }, nil + default: + // Build the package (.) so all .go files are compiled together + cmd := exec.Command( + "go", "build", + "-o", tmpPath, + "-trimpath", + "-ldflags=-buildid= -w -s", + ".", + ) + cmd.Dir = workflowRootFolder + cmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm", "CGO_ENABLED=0") + return func() ([]byte, error) { + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("%w\nbuild output:\n%s", err, strings.TrimSpace(string(out))) + } + b, err := os.ReadFile(tmpPath) + _ = os.Remove(tmpPath) + return b, err + }, nil + } +} + +// CompileWorkflowToWasm compiles the workflow at workflowPath and returns the WASM binary. +// It runs the sequence of commands from getBuildCmds (make build + copy for WASM, or single build for Go/TS), then reads the temp WASM file. +func CompileWorkflowToWasm(workflowPath string) ([]byte, error) { + workflowRootFolder, workflowMainFile, err := WorkflowPathRootAndMain(workflowPath) + if err != nil { + return nil, fmt.Errorf("workflow path: %w", err) + } + workflowAbsFile := filepath.Join(workflowRootFolder, workflowMainFile) + language := GetWorkflowLanguage(workflowMainFile) + + if language != constants.WorkflowLanguageWasm { + if _, err := os.Stat(workflowAbsFile); os.IsNotExist(err) { + return nil, fmt.Errorf("workflow file not found: %s", workflowAbsFile) + } + } + + switch language { + case constants.WorkflowLanguageTypeScript: + if err := EnsureTool("bun"); err != nil { + return nil, errors.New("bun is required for TypeScript workflows but was not found in PATH; install from https://bun.com/docs/installation") + } + case constants.WorkflowLanguageGolang: + if err := EnsureTool("go"); err != nil { + return nil, errors.New("go toolchain is required for Go workflows but was not found in PATH; install from https://go.dev/dl") + } + case constants.WorkflowLanguageWasm: + if err := EnsureTool("make"); err != nil { + return nil, errors.New("make is required for WASM workflows but was not found in PATH") + } + default: + return nil, fmt.Errorf("unsupported workflow language for file %s", workflowMainFile) + } + + buildStep, err := getBuildCmd(workflowRootFolder, workflowMainFile, language) + if err != nil { + return nil, err + } + wasm, err := buildStep() + if err != nil { + return nil, fmt.Errorf("failed to compile workflow: %w", err) + } + return wasm, nil +} + +// findMakefileRoot walks up from dir and returns the first directory that contains a Makefile. +func findMakefileRoot(dir string) (string, error) { + abs, err := filepath.Abs(dir) + if err != nil { + return "", fmt.Errorf("resolve path: %w", err) + } + for { + if _, err := os.Stat(filepath.Join(abs, makefileName)); err == nil { + return abs, nil + } + parent := filepath.Dir(abs) + if parent == abs { + return "", errors.New("no Makefile found in directory or any parent (required for WASM workflow build)") + } + abs = parent + } +} diff --git a/cmd/common/compile_test.go b/cmd/common/compile_test.go new file mode 100644 index 00000000..aec71b63 --- /dev/null +++ b/cmd/common/compile_test.go @@ -0,0 +1,128 @@ +package common + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func deployTestdataPath(elem ...string) string { + _, filename, _, _ := runtime.Caller(0) + dir := filepath.Dir(filename) + return filepath.Join(append([]string{dir, "..", "workflow", "deploy", "testdata"}, elem...)...) +} + +func TestFindMakefileRoot(t *testing.T) { + dir := t.TempDir() + + _, err := findMakefileRoot(dir) + require.Error(t, err) + require.Contains(t, err.Error(), "no Makefile found") + + require.NoError(t, os.WriteFile(filepath.Join(dir, makefileName), []byte("build:\n\techo ok\n"), 0600)) + root, err := findMakefileRoot(dir) + require.NoError(t, err) + absDir, _ := filepath.Abs(dir) + require.Equal(t, absDir, root) + + sub := filepath.Join(dir, "wasm") + require.NoError(t, os.MkdirAll(sub, 0755)) + root, err = findMakefileRoot(sub) + require.NoError(t, err) + require.Equal(t, absDir, root) +} + +func TestCompileWorkflowToWasm_Go_Success(t *testing.T) { + t.Run("basic_workflow", func(t *testing.T) { + path := deployTestdataPath("basic_workflow", "main.go") + wasm, err := CompileWorkflowToWasm(path) + require.NoError(t, err) + assert.NotEmpty(t, wasm) + }) + + t.Run("configless_workflow", func(t *testing.T) { + path := deployTestdataPath("configless_workflow", "main.go") + wasm, err := CompileWorkflowToWasm(path) + require.NoError(t, err) + assert.NotEmpty(t, wasm) + }) + + t.Run("missing_go_mod", func(t *testing.T) { + path := deployTestdataPath("missing_go_mod", "main.go") + wasm, err := CompileWorkflowToWasm(path) + require.NoError(t, err) + assert.NotEmpty(t, wasm) + }) +} + +func TestCompileWorkflowToWasm_Go_Malformed_Fails(t *testing.T) { + path := deployTestdataPath("malformed_workflow", "main.go") + _, err := CompileWorkflowToWasm(path) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to compile workflow") + assert.Contains(t, err.Error(), "undefined: sdk.RemovedFunctionThatFailsCompilation") +} + +func TestCompileWorkflowToWasm_Wasm_Success(t *testing.T) { + wasmPath := deployTestdataPath("custom_wasm_workflow", "wasm", "workflow.wasm") + _ = os.Remove(wasmPath) + t.Cleanup(func() { _ = os.Remove(wasmPath) }) + + wasm, err := CompileWorkflowToWasm(wasmPath) + require.NoError(t, err) + assert.NotEmpty(t, wasm) + + _, err = os.Stat(wasmPath) + require.NoError(t, err, "make build should produce wasm/workflow.wasm") +} + +func TestCompileWorkflowToWasm_Wasm_Fails(t *testing.T) { + t.Run("no_makefile", func(t *testing.T) { + dir := t.TempDir() + wasmDir := filepath.Join(dir, "wasm") + require.NoError(t, os.MkdirAll(wasmDir, 0755)) + wasmPath := filepath.Join(wasmDir, "workflow.wasm") + require.NoError(t, os.WriteFile(wasmPath, []byte("not really wasm"), 0600)) + + _, err := CompileWorkflowToWasm(wasmPath) + require.Error(t, err) + assert.Contains(t, err.Error(), "no Makefile found") + }) + + t.Run("make_build_fails", func(t *testing.T) { + path := deployTestdataPath("wasm_make_fails", "wasm", "workflow.wasm") + _, err := CompileWorkflowToWasm(path) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to compile workflow") + assert.Contains(t, err.Error(), "build output:") + }) +} + +func TestCompileWorkflowToWasm_TS_Success(t *testing.T) { + if err := EnsureTool("bun"); err != nil { + t.Skip("bun not in PATH, skipping TS compile test") + } + dir := t.TempDir() + mainPath := filepath.Join(dir, "main.ts") + require.NoError(t, os.WriteFile(mainPath, []byte(`export async function main() { return "ok"; } +`), 0600)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"test","dependencies":{"@chainlink/cre-sdk":"latest"}} +`), 0600)) + install := exec.Command("bun", "install") + install.Dir = dir + install.Stdout = os.Stdout + install.Stderr = os.Stderr + if err := install.Run(); err != nil { + t.Skipf("bun install failed (network or cre-sdk): %v", err) + } + wasm, err := CompileWorkflowToWasm(mainPath) + if err != nil { + t.Skipf("TS compile failed (published cre-sdk may lack full layout): %v", err) + } + assert.NotEmpty(t, wasm) +} diff --git a/cmd/common/utils.go b/cmd/common/utils.go index 4a000c2d..470e8c0f 100644 --- a/cmd/common/utils.go +++ b/cmd/common/utils.go @@ -145,15 +145,52 @@ func ToStringSlice(args []any) []string { } // GetWorkflowLanguage determines the workflow language based on the file extension -// Note: inputFile can be a file path (e.g., "main.ts" or "main.go") or a directory (for Go workflows, e.g., ".") -// Returns constants.WorkflowLanguageTypeScript for .ts or .tsx files, constants.WorkflowLanguageGolang otherwise +// Note: inputFile can be a file path (e.g., "main.ts", "main.go", or "workflow.wasm") or a directory (for Go workflows, e.g., ".") +// Returns constants.WorkflowLanguageTypeScript for .ts or .tsx files, constants.WorkflowLanguageWasm for .wasm files, constants.WorkflowLanguageGolang otherwise func GetWorkflowLanguage(inputFile string) string { if strings.HasSuffix(inputFile, ".ts") || strings.HasSuffix(inputFile, ".tsx") { return constants.WorkflowLanguageTypeScript } + if strings.HasSuffix(inputFile, ".wasm") { + return constants.WorkflowLanguageWasm + } return constants.WorkflowLanguageGolang } +// ResolveWorkflowPath turns a workflow-path value from YAML (e.g. "." or "main.ts") into an +// absolute path to the main file. When pathFromYAML is "." or "", looks for main.go then main.ts +// under workflowDir. Callers can use GetWorkflowLanguage on the result to get the language. +func ResolveWorkflowPath(workflowDir, pathFromYAML string) (absPath string, err error) { + workflowDir, err = filepath.Abs(workflowDir) + if err != nil { + return "", fmt.Errorf("workflow directory: %w", err) + } + if pathFromYAML == "" || pathFromYAML == "." { + mainGo := filepath.Join(workflowDir, "main.go") + mainTS := filepath.Join(workflowDir, "main.ts") + if _, err := os.Stat(mainGo); err == nil { + return mainGo, nil + } + if _, err := os.Stat(mainTS); err == nil { + return mainTS, nil + } + return "", fmt.Errorf("no main.go or main.ts in %s", workflowDir) + } + joined := filepath.Join(workflowDir, pathFromYAML) + return filepath.Abs(joined) +} + +// WorkflowPathRootAndMain returns the absolute root directory and main file name for a workflow +// path (e.g. "workflowName/main.go" -> rootDir, "main.go"). Use with GetWorkflowLanguage(mainFile) +// for consistent language detection. +func WorkflowPathRootAndMain(workflowPath string) (rootDir, mainFile string, err error) { + abs, err := filepath.Abs(workflowPath) + if err != nil { + return "", "", fmt.Errorf("workflow path: %w", err) + } + return filepath.Dir(abs), filepath.Base(abs), nil +} + // EnsureTool checks that the binary exists on PATH func EnsureTool(bin string) error { if _, err := exec.LookPath(bin); err != nil { @@ -162,41 +199,6 @@ func EnsureTool(bin string) error { return nil } -// Gets a build command for either Golang or Typescript based on the filename -func GetBuildCmd(inputFile string, outputFile string, rootFolder string) *exec.Cmd { - isTypescriptWorkflow := strings.HasSuffix(inputFile, ".ts") || strings.HasSuffix(inputFile, ".tsx") - - var buildCmd *exec.Cmd - if isTypescriptWorkflow { - buildCmd = exec.Command( - "bun", - "cre-compile", - inputFile, - outputFile, - ) - } else { - // The build command for reproducible and trimmed binaries. - // -trimpath removes all file system paths from the compiled binary. - // -ldflags="-buildid= -w -s" further reduces the binary size: - // -buildid= removes the build ID, ensuring reproducibility. - // -w disables DWARF debugging information. - // -s removes the symbol table. - buildCmd = exec.Command( - "go", - "build", - "-o", outputFile, - "-trimpath", - "-ldflags=-buildid= -w -s", - inputFile, - ) - buildCmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm", "CGO_ENABLED=0") - } - - buildCmd.Dir = rootFolder - - return buildCmd -} - func WriteChangesetFile(fileName string, changesetFile *inttypes.ChangesetFile, settings *settings.Settings) error { // Set project context to ensure we're in the correct directory for writing the changeset file // This is needed because workflow commands set the workflow directory as the context, but path for changeset file is relative to the project root diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index ee6dd145..e632e16e 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -29,8 +29,9 @@ const SecretsFileName = "secrets.yaml" type TemplateLanguage string const ( - TemplateLangGo TemplateLanguage = "go" - TemplateLangTS TemplateLanguage = "typescript" + TemplateLangGo TemplateLanguage = "go" + TemplateLangTS TemplateLanguage = "typescript" + TemplateLangWasm TemplateLanguage = "wasm" ) const ( @@ -74,6 +75,14 @@ var languageTemplates = []LanguageTemplate{ {Folder: "typescriptConfHTTP", Title: "Confidential Http: Typescript example using the confidential http capability", ID: 5, Name: ConfHTTPTemplate, Hidden: true}, }, }, + { + Title: "Self-compiled WASM (advanced)", + Lang: TemplateLangWasm, + EntryPoint: "./wasm/workflow.wasm", + Workflows: []WorkflowTemplate{ + {Folder: "wasmBlankTemplate", Title: "Blank: Self-compiled WASM workflow template", ID: 6, Name: HelloWorldTemplate}, + }, + }, } type Inputs struct { @@ -333,10 +342,12 @@ func (h *handler) Execute(inputs Inputs) error { h.runtimeContext.Workflow.Language = constants.WorkflowLanguageGolang case TemplateLangTS: h.runtimeContext.Workflow.Language = constants.WorkflowLanguageTypeScript + case TemplateLangWasm: + h.runtimeContext.Workflow.Language = constants.WorkflowLanguageWasm } } - h.printSuccessMessage(projectRoot, workflowName, selectedLanguageTemplate.Lang) + h.printSuccessMessage(projectRoot, workflowName, workflowDirectory, selectedLanguageTemplate.Lang) return nil } @@ -358,25 +369,47 @@ func (h *handler) findExistingProject(dir string) (projectRoot string, language } } -func (h *handler) printSuccessMessage(projectRoot, workflowName string, lang TemplateLanguage) { +func (h *handler) printSuccessMessage(projectRoot, workflowName, workflowDirectory string, lang TemplateLanguage) { ui.Line() ui.Success("Project created successfully!") ui.Line() var steps string - if lang == TemplateLangGo { + workflowDirBase := filepath.Base(workflowDirectory) + projBase := filepath.Base(projectRoot) + readmeHint := filepath.Join(workflowDirBase, "README.md") + + switch lang { + case TemplateLangGo: steps = ui.RenderStep("1. Navigate to your project:") + "\n" + - " " + ui.RenderDim("cd "+filepath.Base(projectRoot)) + "\n\n" + + " " + ui.RenderDim("cd "+projBase) + "\n\n" + ui.RenderStep("2. Run the workflow:") + "\n" + - " " + ui.RenderDim("cre workflow simulate "+workflowName) - } else { + " " + ui.RenderDim("cre workflow simulate "+workflowName) + "\n\n" + + ui.RenderStep("3. (Optional) Consult " + readmeHint + " to learn more about this template.") + case TemplateLangTS: steps = ui.RenderStep("1. Navigate to your project:") + "\n" + - " " + ui.RenderDim("cd "+filepath.Base(projectRoot)) + "\n\n" + + " " + ui.RenderDim("cd "+projBase) + "\n\n" + ui.RenderStep("2. Install Bun (if needed):") + "\n" + " " + ui.RenderDim("npm install -g bun") + "\n\n" + - ui.RenderStep("3. Install dependencies:") + "\n" + + ui.RenderStep("3. Install workflow dependencies:") + "\n" + " " + ui.RenderDim("bun install --cwd ./"+workflowName) + "\n\n" + ui.RenderStep("4. Run the workflow:") + "\n" + + " " + ui.RenderDim("cre workflow simulate "+workflowName) + "\n\n" + + ui.RenderStep("5. (Optional) Consult " + readmeHint + " to learn more about this template.") + case TemplateLangWasm: + steps = ui.RenderStep("1. Navigate to your project:") + "\n" + + " " + ui.RenderDim("cd "+projBase) + "\n\n" + + ui.RenderStep("2. Add your build logic to the Makefile:") + "\n" + + " " + ui.RenderDim("Edit "+workflowDirBase+"/Makefile and implement the 'build' target") + "\n\n" + + ui.RenderStep("3. Build your workflow:") + "\n" + + " " + ui.RenderDim("cd "+workflowName+" && make build") + "\n\n" + + ui.RenderStep("4. Run the workflow:") + "\n" + + " " + ui.RenderDim("cre workflow simulate "+workflowName) + "\n\n" + + ui.RenderStep("5. (Optional) Consult " + readmeHint + " to learn more about this template.") + default: + steps = ui.RenderStep("1. Navigate to your project:") + "\n" + + " " + ui.RenderDim("cd "+projBase) + "\n\n" + + ui.RenderStep("2. Run the workflow:") + "\n" + " " + ui.RenderDim("cre workflow simulate "+workflowName) } diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl b/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl new file mode 100644 index 00000000..4d0f01be --- /dev/null +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl @@ -0,0 +1,9 @@ +.PHONY: build + +build: + # TODO: Add your build logic here + # This target should compile your workflow to wasm/workflow.wasm + # Example for Go: + # GOOS=wasip1 GOARCH=wasm go build -o wasm/workflow.wasm . + @echo "Please implement the build target in the Makefile" + @exit 1 diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/README.md b/cmd/creinit/template/workflow/wasmBlankTemplate/README.md new file mode 100644 index 00000000..94056ad3 --- /dev/null +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/README.md @@ -0,0 +1,32 @@ +# Self-compiled WASM Workflow Template + +This template provides a blank workflow template for self-compiled WASM workflows. It includes the necessary files for a workflow, excluding workflow code. + +## Structure + +- `Makefile`: Contains a TODO on the `build` target where you should add your build logic +- `workflow.yaml`: Workflow settings file with the wasm directory configured +- `config.staging.json` and `config.production.json`: Configuration files for different environments +- `secrets.yaml`: Secrets file (if needed) + +## Steps to use + +1. **Add your build logic**: Edit the `Makefile` and implement the `build` target. This should compile your workflow to `wasm/workflow.wasm`. + +2. **Build your workflow**: Run `make build` from the workflow directory. + +3. **Simulate the workflow**: From the project root, run: + ```bash + cre workflow simulate --target=staging-settings + ``` + +## Example Makefile build target + +```makefile +build: + # TODO: Add your build logic here + # Example for Go: + # GOOS=wasip1 GOARCH=wasm go build -o wasm/workflow.wasm . + @echo "Please implement the build target in the Makefile" + exit 1 +``` diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/config.production.json b/cmd/creinit/template/workflow/wasmBlankTemplate/config.production.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/config.production.json @@ -0,0 +1 @@ +{} diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/config.staging.json b/cmd/creinit/template/workflow/wasmBlankTemplate/config.staging.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/config.staging.json @@ -0,0 +1 @@ +{} diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/secrets.yaml b/cmd/creinit/template/workflow/wasmBlankTemplate/secrets.yaml new file mode 100644 index 00000000..7b85d864 --- /dev/null +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/secrets.yaml @@ -0,0 +1 @@ +secretsNames: diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/wasm/README.md b/cmd/creinit/template/workflow/wasmBlankTemplate/wasm/README.md new file mode 100644 index 00000000..5a3491cd --- /dev/null +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/wasm/README.md @@ -0,0 +1,3 @@ +# WASM Directory + +This directory should contain your compiled WASM file (`workflow.wasm`) after running `make build`. diff --git a/cmd/root.go b/cmd/root.go index 0ea014da..deabecf4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -364,23 +364,24 @@ func newRootCommand() *cobra.Command { func isLoadSettings(cmd *cobra.Command) bool { // It is not expected to have the settings file when running the following commands var excludedCommands = map[string]struct{}{ - "cre version": {}, - "cre login": {}, - "cre logout": {}, - "cre whoami": {}, - "cre account list-key": {}, - "cre init": {}, - "cre generate-bindings": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre help": {}, - "cre update": {}, - "cre workflow": {}, - "cre account": {}, - "cre secrets": {}, - "cre": {}, + "cre version": {}, + "cre login": {}, + "cre logout": {}, + "cre whoami": {}, + "cre account list-key": {}, + "cre init": {}, + "cre generate-bindings": {}, + "cre completion bash": {}, + "cre completion fish": {}, + "cre completion powershell": {}, + "cre completion zsh": {}, + "cre help": {}, + "cre update": {}, + "cre workflow": {}, + "cre workflow convert-to-custom-build": {}, + "cre account": {}, + "cre secrets": {}, + "cre": {}, } _, exists := excludedCommands[cmd.CommandPath()] diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go new file mode 100644 index 00000000..93b21f16 --- /dev/null +++ b/cmd/workflow/convert/convert.go @@ -0,0 +1,145 @@ +package convert + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/rs/zerolog" + "github.com/spf13/cobra" + + cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" + "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +const ( + wasmWorkflowPath = "./wasm/workflow.wasm" + convertWarning = "This will convert your workflow to a custom (self-compiled) build. This cannot be undone by the CLI. Continue?" +) + +type Inputs struct { + WorkflowFolder string + Force bool +} + +func New(runtimeContext *runtime.Context) *cobra.Command { + var force bool + convertCmd := &cobra.Command{ + Use: "convert-to-custom-build ", + Short: "Converts an existing workflow to a custom (self-compiled) build", + Long: `Converts a Go or TypeScript workflow to use a custom build via Makefile, producing wasm/workflow.wasm. The workflow-path in workflow.yaml is updated to ./wasm/workflow.wasm. This cannot be undone.`, + Args: cobra.ExactArgs(1), + Example: `cre workflow convert-to-custom-build ./my-workflow`, + RunE: func(cmd *cobra.Command, args []string) error { + handler := newHandler(runtimeContext) + inputs := Inputs{ + WorkflowFolder: args[0], + Force: force, + } + return handler.Execute(inputs) + }, + } + convertCmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt and convert immediately") + return convertCmd +} + +type handler struct { + log *zerolog.Logger + runtimeContext *runtime.Context +} + +func newHandler(runtimeContext *runtime.Context) *handler { + h := &handler{runtimeContext: runtimeContext} + if runtimeContext != nil { + h.log = runtimeContext.Logger + } + return h +} + +func (h *handler) Execute(inputs Inputs) error { + workflowDir, err := filepath.Abs(inputs.WorkflowFolder) + if err != nil { + return fmt.Errorf("workflow folder path: %w", err) + } + workflowYAML := filepath.Join(workflowDir, constants.DefaultWorkflowSettingsFileName) + currentPath, err := settings.GetWorkflowPathFromFile(workflowYAML) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("workflow folder does not contain %s: %w", constants.DefaultWorkflowSettingsFileName, err) + } + return err + } + workflowPath, err := cmdcommon.ResolveWorkflowPath(workflowDir, currentPath) + if err != nil { + return fmt.Errorf("cannot detect workflow language: %w", err) + } + lang := cmdcommon.GetWorkflowLanguage(workflowPath) + if lang == constants.WorkflowLanguageWasm { + return fmt.Errorf("workflow is already a custom build (workflow-path is %s)", currentPath) + } + + if !inputs.Force { + confirmed, err := ui.Confirm(convertWarning, ui.WithLabels("Yes", "No")) + if err != nil { + return err + } + if !confirmed { + ui.Dim("Convert cancelled.") + return nil + } + } + + if err := settings.SetWorkflowPathInFile(workflowYAML, wasmWorkflowPath); err != nil { + return err + } + + wasmDir := filepath.Join(workflowDir, "wasm") + if err := os.MkdirAll(wasmDir, 0755); err != nil { + return fmt.Errorf("create wasm directory: %w", err) + } + + makefilePath := filepath.Join(workflowDir, "Makefile") + mainFile := filepath.Base(workflowPath) + makefile, err := makefileContent(workflowDir, lang, mainFile) + if err != nil { + return err + } + if err := os.WriteFile(makefilePath, []byte(makefile), 0600); err != nil { + return fmt.Errorf("write Makefile: %w", err) + } + + ui.Success("Workflow converted to custom build. workflow-path is now " + wasmWorkflowPath) + ui.Dim("The Makefile is configured to output the WASM to this path. Run: make build") + return nil +} + +func goMakefile() string { + return `.PHONY: build + +build: + GOOS=wasip1 GOARCH=wasm CGO_ENABLED=0 go build -o wasm/workflow.wasm -trimpath -ldflags="-buildid= -w -s" . +` +} + +func makefileContent(workflowDir, lang string, mainFile string) (string, error) { + switch lang { + case constants.WorkflowLanguageGolang: + return goMakefile(), nil + case constants.WorkflowLanguageTypeScript: + return makefileContentTS(workflowDir, mainFile) + default: + return "", fmt.Errorf("unsupported workflow language") + } +} + +func makefileContentTS(_, mainFile string) (string, error) { + return fmt.Sprintf(`.PHONY: build + +build: + bun cre-compile %s wasm/workflow.wasm +`, mainFile), nil +} diff --git a/cmd/workflow/convert/convert_test.go b/cmd/workflow/convert/convert_test.go new file mode 100644 index 00000000..7d6acbfd --- /dev/null +++ b/cmd/workflow/convert/convert_test.go @@ -0,0 +1,120 @@ +package convert + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/constants" +) + +func TestConvert_AlreadyWasm_ReturnsError(t *testing.T) { + dir := t.TempDir() + workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) + yamlContent := `staging-settings: + user-workflow: + workflow-name: "foo-staging" + workflow-artifacts: + workflow-path: "./wasm/workflow.wasm" + config-path: "./config.staging.json" +production-settings: + user-workflow: + workflow-name: "foo-production" + workflow-artifacts: + workflow-path: "./wasm/workflow.wasm" + config-path: "./config.production.json" +` + require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) + + h := newHandler(nil) + err := h.Execute(Inputs{WorkflowFolder: dir, Force: true}) + require.Error(t, err) + require.Contains(t, err.Error(), "already a custom build") +} + +func TestConvert_Force_UpdatesYAMLAndCreatesMakefile(t *testing.T) { + dir := t.TempDir() + workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) + mainGo := filepath.Join(dir, "main.go") + yamlContent := `staging-settings: + user-workflow: + workflow-name: "wf-staging" + workflow-artifacts: + workflow-path: "." + config-path: "./config.staging.json" +production-settings: + user-workflow: + workflow-name: "wf-production" + workflow-artifacts: + workflow-path: "." + config-path: "./config.production.json" +` + require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) + require.NoError(t, os.WriteFile(mainGo, []byte("package main\n"), 0600)) + + h := newHandler(nil) + err := h.Execute(Inputs{WorkflowFolder: dir, Force: true}) + require.NoError(t, err) + + data, err := os.ReadFile(workflowYAML) + require.NoError(t, err) + require.Contains(t, string(data), wasmWorkflowPath) + + require.DirExists(t, filepath.Join(dir, "wasm")) + makefile := filepath.Join(dir, "Makefile") + require.FileExists(t, makefile) + content, _ := os.ReadFile(makefile) + require.Contains(t, string(content), "build") + require.Contains(t, string(content), "wasm/workflow.wasm") +} + +func TestConvert_PromptNo_Cancels(t *testing.T) { + t.Skip("Charm ui.Confirm requires TTY; use Force in production or run test with TTY") +} + +func TestConvert_PromptYes_Proceeds(t *testing.T) { + t.Skip("Charm ui.Confirm requires TTY; use Force in production or run test with TTY") +} + +func TestConvert_PromptEmpty_DefaultsYes_Proceeds(t *testing.T) { + t.Skip("Charm ui.Confirm requires TTY; use Force in production or run test with TTY") +} + +func TestConvert_TS_InstallsDepsIfNoNodeModules(t *testing.T) { + dir := t.TempDir() + workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) + mainTS := filepath.Join(dir, "main.ts") + packageJSON := filepath.Join(dir, "package.json") + yamlContent := `staging-settings: + user-workflow: + workflow-name: "wf-staging" + workflow-artifacts: + workflow-path: "main.ts" + config-path: "./config.staging.json" +production-settings: + user-workflow: + workflow-name: "wf-production" + workflow-artifacts: + workflow-path: "main.ts" + config-path: "./config.production.json" +` + require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) + require.NoError(t, os.WriteFile(mainTS, []byte("export default function run() { return Promise.resolve({ result: \"ok\" }); }\n"), 0600)) + require.NoError(t, os.WriteFile(packageJSON, []byte(`{"name":"test","private":true,"dependencies":{"@chainlink/cre-sdk":"^1.0.3"}}`), 0600)) + + h := newHandler(nil) + err := h.Execute(Inputs{WorkflowFolder: dir, Force: true}) + require.NoError(t, err) + + require.FileExists(t, filepath.Join(dir, "Makefile")) + makefile, _ := os.ReadFile(filepath.Join(dir, "Makefile")) + require.Contains(t, string(makefile), "bun cre-compile", "Makefile should match CLI build") + require.Contains(t, string(makefile), "main.ts", "Makefile should build main.ts") + require.Contains(t, string(makefile), "wasm/workflow.wasm", "Makefile should output to wasm/workflow.wasm") + + // CLI must not change the workflow; main.ts unchanged + mainTSContent, _ := os.ReadFile(mainTS) + require.Contains(t, string(mainTSContent), "export default function run()", "convert must not modify workflow source") +} diff --git a/cmd/workflow/deploy/compile.go b/cmd/workflow/deploy/compile.go index d18de7d4..4349319b 100644 --- a/cmd/workflow/deploy/compile.go +++ b/cmd/workflow/deploy/compile.go @@ -3,16 +3,13 @@ package deploy import ( "bytes" "encoding/base64" - "errors" "fmt" "os" - "path/filepath" "strings" "github.com/andybalholm/brotli" cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" - "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -35,61 +32,30 @@ func (h *handler) Compile() error { h.inputs.OutputPath += ".b64" // Append ".b64" if it doesn't already end with ".b64" } - workflowAbsFile, err := filepath.Abs(h.inputs.WorkflowPath) + workflowDir, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get absolute path for the workflow file: %w", err) + return fmt.Errorf("workflow directory: %w", err) } - - if _, err := os.Stat(workflowAbsFile); os.IsNotExist(err) { - return fmt.Errorf("workflow file not found: %s", workflowAbsFile) + resolvedWorkflowPath, err := cmdcommon.ResolveWorkflowPath(workflowDir, h.inputs.WorkflowPath) + if err != nil { + return fmt.Errorf("workflow path: %w", err) + } + _, workflowMainFile, err := cmdcommon.WorkflowPathRootAndMain(resolvedWorkflowPath) + if err != nil { + return fmt.Errorf("workflow path: %w", err) } - - workflowRootFolder := filepath.Dir(h.inputs.WorkflowPath) - - tmpWasmFileName := "tmp.wasm" - workflowMainFile := filepath.Base(h.inputs.WorkflowPath) - - // Set language in runtime context based on workflow file extension if h.runtimeContext != nil { h.runtimeContext.Workflow.Language = cmdcommon.GetWorkflowLanguage(workflowMainFile) - - switch h.runtimeContext.Workflow.Language { - case constants.WorkflowLanguageTypeScript: - if err := cmdcommon.EnsureTool("bun"); err != nil { - return errors.New("bun is required for TypeScript workflows but was not found in PATH; install from https://bun.com/docs/installation") - } - case constants.WorkflowLanguageGolang: - if err := cmdcommon.EnsureTool("go"); err != nil { - return errors.New("go toolchain is required for Go workflows but was not found in PATH; install from https://go.dev/dl") - } - default: - return fmt.Errorf("unsupported workflow language for file %s", workflowMainFile) - } } - buildCmd := cmdcommon.GetBuildCmd(workflowMainFile, tmpWasmFileName, workflowRootFolder) - h.log.Debug(). - Str("Workflow directory", buildCmd.Dir). - Str("Command", buildCmd.String()). - Msg("Executing go build command") - - buildOutput, err := buildCmd.CombinedOutput() + wasmFile, err := cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath) if err != nil { ui.Error("Build failed:") - ui.Print(string(buildOutput)) - - out := strings.TrimSpace(string(buildOutput)) - return fmt.Errorf("failed to compile workflow: %w\nbuild output:\n%s", err, out) + return fmt.Errorf("failed to compile workflow: %w", err) } - h.log.Debug().Msgf("Build output: %s", buildOutput) + h.log.Debug().Msg("Workflow compiled successfully") ui.Success("Workflow compiled successfully") - tmpWasmLocation := filepath.Join(workflowRootFolder, tmpWasmFileName) - wasmFile, err := os.ReadFile(tmpWasmLocation) - if err != nil { - return fmt.Errorf("failed to read workflow binary: %w", err) - } - compressedFile, err := applyBrotliCompressionV2(&wasmFile) if err != nil { return fmt.Errorf("failed to compress WASM binary: %w", err) @@ -101,10 +67,6 @@ func (h *handler) Compile() error { } h.log.Debug().Msg("WASM binary encoded") - if err = os.Remove(tmpWasmLocation); err != nil { - return fmt.Errorf("failed to remove the temporary file: %w", err) - } - return nil } diff --git a/cmd/workflow/deploy/compile_test.go b/cmd/workflow/deploy/compile_test.go index d0ebadd8..51f42911 100644 --- a/cmd/workflow/deploy/compile_test.go +++ b/cmd/workflow/deploy/compile_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/testutil/chainsim" @@ -165,102 +166,6 @@ func TestCompileCmd(t *testing.T) { httpmock.Activate() t.Cleanup(httpmock.DeactivateAndReset) - tests := []struct { - inputs Inputs - wantErr string - compilationErr string - WorkflowOwnerType string - }{ - { - inputs: Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "malformed_workflow", "main.go"), - OutputPath: outputPath, - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - }, - WorkflowOwnerType: constants.WorkflowOwnerTypeEOA, - wantErr: "failed to compile workflow: exit status 1", - compilationErr: "undefined: sdk.RemovedFunctionThatFailsCompilation", - }, - } - - for _, tt := range tests { - t.Run(tt.wantErr, func(t *testing.T) { - simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) - defer simulatedEnvironment.Close() - - // Capture stdout - oldStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - handler := newHandler(ctx, buf) - - ctx.Settings = createTestSettings( - chainsim.TestAddress, - tt.WorkflowOwnerType, - "test_workflow", - tt.inputs.WorkflowPath, - tt.inputs.ConfigPath, - ) - handler.settings = ctx.Settings - handler.inputs = tt.inputs - err := handler.ValidateInputs() - require.NoError(t, err) - - err = handler.Execute() - - w.Close() - os.Stdout = oldStdout - var output strings.Builder - _, _ = io.Copy(&output, r) - - require.Error(t, err) - assert.ErrorContains(t, err, tt.wantErr) - - if tt.compilationErr != "" { - assert.Contains(t, output.String(), tt.compilationErr) - } - }) - } - }) - - t.Run("no config", func(t *testing.T) { - simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) - defer simulatedEnvironment.Close() - - ctx, _ := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() - - ctx.Settings = createTestSettings( - chainsim.TestAddress, - constants.WorkflowOwnerTypeEOA, - "test_workflow", - "testdata/configless_workflow/main.go", - "", - ) - - httpmock.Activate() - t.Cleanup(httpmock.DeactivateAndReset) - - err := runCompile(simulatedEnvironment, Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "configless_workflow", "main.go"), - OutputPath: outputPath, - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - }, constants.WorkflowOwnerTypeEOA) - defer os.Remove(outputPath) - - require.NoError(t, err) - }) - - t.Run("with config", func(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) defer simulatedEnvironment.Close() @@ -268,142 +173,54 @@ func TestCompileCmd(t *testing.T) { WorkflowName: "test_workflow", WorkflowOwner: chainsim.TestAddress, DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + WorkflowPath: filepath.Join("testdata", "malformed_workflow", "main.go"), OutputPath: outputPath, - ConfigPath: filepath.Join("testdata", "basic_workflow", "config.yml"), WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", }, constants.WorkflowOwnerTypeEOA) - defer os.Remove(outputPath) - - require.NoError(t, err) + require.Error(t, err) + assert.ErrorContains(t, err, "failed to compile workflow") + assert.ErrorContains(t, err, "undefined: sdk.RemovedFunctionThatFailsCompilation") }) - - t.Run("compiles even without go.mod", func(t *testing.T) { - // it auto falls back to using the go.mod in the root directory (/cre-cli) - simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) - defer simulatedEnvironment.Close() - - httpmock.Activate() - t.Cleanup(httpmock.DeactivateAndReset) - - err := runCompile(simulatedEnvironment, Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "missing_go_mod", "main.go"), - OutputPath: outputPath, - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - }, constants.WorkflowOwnerTypeEOA) - defer os.Remove(outputPath) - - require.NoError(t, err) - }) - }) } -func TestCompileCreatesBase64EncodedFile(t *testing.T) { +func TestCompileOutputMatchesUnderlying(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + baseInputs := Inputs{ + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + ConfigPath: filepath.Join("testdata", "basic_workflow", "config.yml"), + WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", + WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + } - t.Run("default output file is binary.wasm.br", func(t *testing.T) { - expectedOutputPath := "./binary.wasm.br.b64" - - err := runCompile(simulatedEnvironment, Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - ConfigPath: filepath.Join("testdata", "basic_workflow", "config.yml"), - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - }, constants.WorkflowOwnerTypeEOA) - defer os.Remove(expectedOutputPath) - - require.NoError(t, err) - assert.FileExists(t, expectedOutputPath) + t.Run("default output path", func(t *testing.T) { + inputs := baseInputs + inputs.OutputPath = "./binary.wasm.br.b64" + assertCompileOutputMatchesUnderlying(t, simulatedEnvironment, inputs, constants.WorkflowOwnerTypeEOA) }) - t.Run("ensures output file has .wasm.br.b64 extension", func(t *testing.T) { + t.Run("output path extension variants", func(t *testing.T) { tests := []struct { - name string - outputPath string - expectedOutput string + name string + outputPath string }{ - { - name: "no extension", - outputPath: "./my-binary", - expectedOutput: "./my-binary.wasm.br.b64", - }, - { - name: "missing .br and .b64", - outputPath: "./my-binary.wasm", - expectedOutput: "./my-binary.wasm.br.b64", - }, - { - name: "missing .b64", - outputPath: "./my-binary.wasm.br", - expectedOutput: "./my-binary.wasm.br.b64", - }, - { - name: "all extensions", - outputPath: "./my-binary.wasm.br.b64", - expectedOutput: "./my-binary.wasm.br.b64", - }, - { - name: "all extensions - same as default", - outputPath: "./binary.wasm.br.b64", - expectedOutput: "./binary.wasm.br.b64", - }, + {"no extension", "./my-binary"}, + {"missing .br and .b64", "./my-binary.wasm"}, + {"missing .b64", "./my-binary.wasm.br"}, + {"all extensions", "./my-binary.wasm.br.b64"}, } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := runCompile(simulatedEnvironment, Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - ConfigPath: filepath.Join("testdata", "basic_workflow", "config.yml"), - OutputPath: tt.outputPath, - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - }, constants.WorkflowOwnerTypeEOA) - defer os.Remove(tt.expectedOutput) - - require.NoError(t, err) - assert.FileExists(t, tt.expectedOutput) + inputs := baseInputs + inputs.OutputPath = tt.outputPath + assertCompileOutputMatchesUnderlying(t, simulatedEnvironment, inputs, constants.WorkflowOwnerTypeEOA) }) } }) - - t.Run("output file is base64 encoded", func(t *testing.T) { - outputPath := "./binary.wasm.br.b64" - - err := runCompile(simulatedEnvironment, Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - ConfigPath: filepath.Join("testdata", "basic_workflow", "config.yml"), - OutputPath: outputPath, - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - }, constants.WorkflowOwnerTypeEOA) - defer os.Remove(outputPath) - - require.NoError(t, err) - assert.FileExists(t, outputPath) - - // Read the output file content - content, err := os.ReadFile(outputPath) - require.NoError(t, err) - - // Check if the content is valid base64 - _, err = base64.StdEncoding.DecodeString(string(content)) - assert.NoError(t, err, "Output file content should be valid base64 encoded data") - }) } // createTestSettings is a helper function to construct settings for tests @@ -458,3 +275,76 @@ func runCompile(simulatedEnvironment *chainsim.SimulatedEnvironment, inputs Inpu return handler.Compile() } + +// outputPathWithExtensions returns the path with .wasm.br.b64 appended as in Compile(). +func outputPathWithExtensions(path string) string { + if path == "" { + path = defaultOutputPath + } + if !strings.HasSuffix(path, ".b64") { + if !strings.HasSuffix(path, ".br") { + if !strings.HasSuffix(path, ".wasm") { + path += ".wasm" + } + path += ".br" + } + path += ".b64" + } + return path +} + +// assertCompileOutputMatchesUnderlying compiles via handler.Compile(), then verifies the output +// file content equals CompileWorkflowToWasm(workflowPath) + brotli + base64. +func assertCompileOutputMatchesUnderlying(t *testing.T, simulatedEnvironment *chainsim.SimulatedEnvironment, inputs Inputs, ownerType string) { + t.Helper() + wasm, err := cmdcommon.CompileWorkflowToWasm(inputs.WorkflowPath) + require.NoError(t, err) + compressed, err := applyBrotliCompressionV2(&wasm) + require.NoError(t, err) + expected := base64.StdEncoding.EncodeToString(compressed) + + err = runCompile(simulatedEnvironment, inputs, ownerType) + require.NoError(t, err) + + actualPath := outputPathWithExtensions(inputs.OutputPath) + t.Cleanup(func() { _ = os.Remove(actualPath) }) + actual, err := os.ReadFile(actualPath) + require.NoError(t, err) + assert.Equal(t, expected, string(actual), "handler.Compile() output should match CompileWorkflowToWasm + brotli + base64") +} + +// TestCustomWasmWorkflowRunsMakeBuild ensures that simulate/deploy run "make build" for a custom +// WASM workflow (workflow-path pointing to .wasm) so the user does not need to run make build manually. +func TestCustomWasmWorkflowRunsMakeBuild(t *testing.T) { + customWasmDir := filepath.Join("testdata", "custom_wasm_workflow") + wasmPath := filepath.Join(customWasmDir, "wasm", "workflow.wasm") + + // Remove wasm file if present so we assert the CLI builds it (CompileWorkflowToWasm runs make via ensureWasmBuilt). + _ = os.Remove(wasmPath) + t.Cleanup(func() { _ = os.Remove(wasmPath) }) + + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + + outputPath := filepath.Join(customWasmDir, "test_out.wasm.br.b64") + t.Cleanup(func() { _ = os.Remove(outputPath) }) + + inputs := Inputs{ + WorkflowName: "custom_wasm_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: wasmPath, + ConfigPath: filepath.Join(customWasmDir, "config.yml"), + WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", + WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + OutputPath: outputPath, + } + + // runCompile calls ValidateInputs then Compile; CompileWorkflowToWasm runs make build internally. No manual make build. + err := runCompile(simulatedEnvironment, inputs, constants.WorkflowOwnerTypeEOA) + require.NoError(t, err, "custom WASM workflow should build via CLI (CompileWorkflowToWasm) without manual make build") + + // Ensure the wasm was actually built by the CLI + _, err = os.Stat(wasmPath) + require.NoError(t, err, "wasm/workflow.wasm should exist after compile") +} diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index 6318acc7..6b4da9bd 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -31,7 +31,7 @@ type Inputs struct { ConfigURL *string `validate:"omitempty,http_url|eq="` KeepAlive bool - WorkflowPath string `validate:"required,path_read"` + WorkflowPath string `validate:"required,workflow_path_read"` ConfigPath string `validate:"omitempty,file,ascii,max=97" cli:"--config"` OutputPath string `validate:"omitempty,filepath,ascii,max=97" cli:"--output"` diff --git a/cmd/workflow/deploy/testdata/custom_wasm_workflow/Makefile b/cmd/workflow/deploy/testdata/custom_wasm_workflow/Makefile new file mode 100644 index 00000000..c910fcf4 --- /dev/null +++ b/cmd/workflow/deploy/testdata/custom_wasm_workflow/Makefile @@ -0,0 +1,4 @@ +.PHONY: build + +build: + GOOS=wasip1 GOARCH=wasm CGO_ENABLED=0 go build -o wasm/workflow.wasm -trimpath -ldflags="-buildid= -w -s" . diff --git a/cmd/workflow/deploy/testdata/custom_wasm_workflow/config.yml b/cmd/workflow/deploy/testdata/custom_wasm_workflow/config.yml new file mode 100644 index 00000000..87df9017 --- /dev/null +++ b/cmd/workflow/deploy/testdata/custom_wasm_workflow/config.yml @@ -0,0 +1,3 @@ +workflowName: "Basic Workflow" +workflowOwner: "0x775edE8C0718c655e5238239aC553E9657bcd8C2" +basicTriggerInterval: 1 # in seconds diff --git a/cmd/workflow/deploy/testdata/custom_wasm_workflow/go.mod b/cmd/workflow/deploy/testdata/custom_wasm_workflow/go.mod new file mode 100644 index 00000000..83f89f9e --- /dev/null +++ b/cmd/workflow/deploy/testdata/custom_wasm_workflow/go.mod @@ -0,0 +1,68 @@ +module custom_wasm_workflow + +go 1.23.3 + +toolchain go1.23.4 + +require ( + github.com/smartcontractkit/chainlink-common v0.4.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/go-viper/mapstructure/v2 v2.1.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/invopop/jsonschema v0.12.0 // indirect + github.com/leodido/go-urn v1.2.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.11.1 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240823153156-2a54df7bffb9 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.6.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.4.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 // indirect + go.opentelemetry.io/otel/log v0.6.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.6.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect +) diff --git a/cmd/workflow/deploy/testdata/custom_wasm_workflow/go.sum b/cmd/workflow/deploy/testdata/custom_wasm_workflow/go.sum new file mode 100644 index 00000000..07060312 --- /dev/null +++ b/cmd/workflow/deploy/testdata/custom_wasm_workflow/go.sum @@ -0,0 +1,158 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= +github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 h1:WCcC4vZDS1tYNxjWlwRJZQy28r8CMoggKnxNzxsVDMQ= +github.com/santhosh-tekuri/jsonschema/v5 v5.2.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chainlink-common v0.4.0 h1:GZ9MhHt5QHXSaK/sAZvKDxkEqF4fPiFHWHEPqs/2C2o= +github.com/smartcontractkit/chainlink-common v0.4.0/go.mod h1:yti7e1+G9hhkYhj+L5sVUULn9Bn3bBL5/AxaNqdJ5YQ= +github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12 h1:NzZGjaqez21I3DU7objl3xExTH4fxYvzTqar8DC6360= +github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12/go.mod h1:fb1ZDVXACvu4frX3APHZaEBp0xi1DIm34DcA0CwTsZM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240823153156-2a54df7bffb9 h1:UiRNKd1OgqsLbFwE+wkAWTdiAxXtCBqKIHeBIse4FUA= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240823153156-2a54df7bffb9/go.mod h1:eqZlW3pJWhjyexnDPrdQxix1pn0wwhI4AO4GKpP/bMI= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.6.0 h1:QSKmLBzbFULSyHzOdO9JsN9lpE4zkrz1byYGmJecdVE= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.6.0/go.mod h1:sTQ/NH8Yrirf0sJ5rWqVu+oT82i4zL9FaF6rWcqnptM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.30.0 h1:VrMAbeJz4gnVDg2zEzjHG4dEH86j4jO6VYB+NgtGD8s= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.30.0/go.mod h1:qqN/uFdpeitTvm+JDqqnjm517pmQRYxTORbETHq5tOc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.4.0 h1:0MH3f8lZrflbUWXVxyBg/zviDFdGE062uKh5+fu8Vv0= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.4.0/go.mod h1:Vh68vYiHY5mPdekTr0ox0sALsqjoVy0w3Os278yX5SQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0 h1:BJee2iLkfRfl9lc7aFmBwkWxY/RI1RDdXepSF6y8TPE= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.28.0/go.mod h1:DIzlHs3DRscCIBU3Y9YSzPfScwnYnzfnCd4g8zA7bZc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 h1:EVSnY9JbEEW92bEkIYOVMw4q1WJxIAGoFTrtYOzWuRQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0/go.mod h1:Ea1N1QQryNXpCD0I1fdLibBAIpQuBkznMmkdKrapk1Y= +go.opentelemetry.io/otel/log v0.6.0 h1:nH66tr+dmEgW5y+F9LanGJUBYPrRgP4g2EkmPE3LeK8= +go.opentelemetry.io/otel/log v0.6.0/go.mod h1:KdySypjQHhP069JX0z/t26VHwa8vSwzgaKmXtIB3fJM= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= +go.opentelemetry.io/otel/sdk/log v0.6.0 h1:4J8BwXY4EeDE9Mowg+CyhWVBhTSLXVXodiXxS/+PGqI= +go.opentelemetry.io/otel/sdk/log v0.6.0/go.mod h1:L1DN8RMAduKkrwRAFDEX3E3TLOq46+XMGSbUfHU/+vE= +go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM= +go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/cmd/workflow/deploy/testdata/custom_wasm_workflow/main.go b/cmd/workflow/deploy/testdata/custom_wasm_workflow/main.go new file mode 100644 index 00000000..d9e8e3ee --- /dev/null +++ b/cmd/workflow/deploy/testdata/custom_wasm_workflow/main.go @@ -0,0 +1,74 @@ +package main + +/* +This file contains the entry point for the WebAssembly (Wasm) executable. +To ensure the code compiles and runs correctly for Wasm (wasip1 target), we must follow these requirements: + +1) **File Name**: + The file must be named `main.go`. This is a Go convention for executables that defines where the program's entry point (`main()` function) is located. + +2) **Package Name**: + The package name must be `main`. This is essential for building an executable in Go. Go's compiler looks for a package named `main` that contains the `main()` function, which acts as the entry point of the program when the Wasm executable is run. +*/ + +import ( + "errors" + "log" + + "gopkg.in/yaml.v3" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/testdata/fixtures/capabilities/basictrigger" + "github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk" + "github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm" +) + +type Config struct { + WorkflowName string `yaml:"workflowName"` + WorkflowOwner string `yaml:"workflowOwner"` + BasicTriggerInterval uint64 `yaml:"basicTriggerInterval"` +} + +func BuildWorkflow(config []byte) *sdk.WorkflowSpecFactory { + // Unmarshal the config bytes into the Config struct + var parsedConfig Config + err := yaml.Unmarshal(config, &parsedConfig) + if err != nil { + log.Fatalf("Failed to parse config: %v", err) + } + log.Printf("WorkflowName from config: %v", parsedConfig.WorkflowName) + log.Printf("WorkflowOwner from config: %v", parsedConfig.WorkflowOwner) + log.Printf("BasicTriggerInterval from config: %v", parsedConfig.BasicTriggerInterval) + + // interval is a mandatory field, throw an error if empty + if parsedConfig.BasicTriggerInterval == 0 { + log.Fatalf("Error: BasicTriggerInterval is missing in the YAML file") + } + + workflow := sdk.NewWorkflowSpecFactory() + + // Trigger + triggerCfg := basictrigger.TriggerConfig{Name: "trigger", Number: parsedConfig.BasicTriggerInterval} + trigger := triggerCfg.New(workflow) + + // Action + sdk.Compute1[basictrigger.TriggerOutputs, bool]( + workflow, + "transform", + sdk.Compute1Inputs[basictrigger.TriggerOutputs]{Arg0: trigger}, + func(sdk sdk.Runtime, outputs basictrigger.TriggerOutputs) (bool, error) { + log.Printf("Output from the basic trigger: %v", outputs.CoolOutput) + if outputs.CoolOutput == "cool" { + return false, errors.New("it is cool, not good") + } + return true, nil + }) + + return workflow +} + +func main() { + runner := wasm.NewRunner() + + workflow := BuildWorkflow(runner.Config()) + runner.Run(workflow) +} diff --git a/cmd/workflow/deploy/testdata/wasm_make_fails/Makefile b/cmd/workflow/deploy/testdata/wasm_make_fails/Makefile new file mode 100644 index 00000000..4018c8c5 --- /dev/null +++ b/cmd/workflow/deploy/testdata/wasm_make_fails/Makefile @@ -0,0 +1,4 @@ +.PHONY: build + +build: + false diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 1876aaf4..6af98a6b 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -4,13 +4,11 @@ import ( "context" "crypto/ecdsa" "encoding/json" - "errors" "fmt" "math" "math/big" "os" "os/signal" - "path/filepath" "strconv" "strings" "syscall" @@ -39,7 +37,6 @@ import ( v2 "github.com/smartcontractkit/chainlink/v2/core/services/workflows/v2" cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" - "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" @@ -47,7 +44,7 @@ import ( ) type Inputs struct { - WorkflowPath string `validate:"required,path_read"` + WorkflowPath string `validate:"required,workflow_path_read"` ConfigPath string `validate:"omitempty,file,ascii,max=97"` SecretsPath string `validate:"omitempty,file,ascii,max=97"` EngineLogs bool `validate:"omitempty" cli:"--engine-logs"` @@ -258,58 +255,33 @@ func (h *handler) ValidateInputs(inputs Inputs) error { } func (h *handler) Execute(inputs Inputs) error { - // Compile the workflow - // terminal command: GOOS=wasip1 GOARCH=wasm go build -trimpath -ldflags="-buildid= -w -s" -o - workflowRootFolder := filepath.Dir(inputs.WorkflowPath) - tmpWasmFileName := "tmp.wasm" - workflowMainFile := filepath.Base(inputs.WorkflowPath) - - // Set language in runtime context based on workflow file extension + workflowDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("workflow directory: %w", err) + } + resolvedWorkflowPath, err := cmdcommon.ResolveWorkflowPath(workflowDir, inputs.WorkflowPath) + if err != nil { + return fmt.Errorf("workflow path: %w", err) + } + _, workflowMainFile, err := cmdcommon.WorkflowPathRootAndMain(resolvedWorkflowPath) + if err != nil { + return fmt.Errorf("workflow path: %w", err) + } if h.runtimeContext != nil { h.runtimeContext.Workflow.Language = cmdcommon.GetWorkflowLanguage(workflowMainFile) - - switch h.runtimeContext.Workflow.Language { - case constants.WorkflowLanguageTypeScript: - if err := cmdcommon.EnsureTool("bun"); err != nil { - return errors.New("bun is required for TypeScript workflows but was not found in PATH; install from https://bun.com/docs/installation") - } - case constants.WorkflowLanguageGolang: - if err := cmdcommon.EnsureTool("go"); err != nil { - return errors.New("go toolchain is required for Go workflows but was not found in PATH; install from https://go.dev/dl") - } - default: - return fmt.Errorf("unsupported workflow language for file %s", workflowMainFile) - } } - buildCmd := cmdcommon.GetBuildCmd(workflowMainFile, tmpWasmFileName, workflowRootFolder) - - h.log.Debug(). - Str("Workflow directory", buildCmd.Dir). - Str("Command", buildCmd.String()). - Msg("Executing go build command") - - // Execute the build command with spinner spinner := ui.NewSpinner() spinner.Start("Compiling workflow...") - buildOutput, err := buildCmd.CombinedOutput() + wasmFileBinary, err := cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath) spinner.Stop() - if err != nil { - out := strings.TrimSpace(string(buildOutput)) - h.log.Info().Msg(out) - return fmt.Errorf("failed to compile workflow: %w\nbuild output:\n%s", err, out) + ui.Error("Build failed:") + return fmt.Errorf("failed to compile workflow: %w", err) } - h.log.Debug().Msgf("Build output: %s", buildOutput) + h.log.Debug().Msg("Workflow compiled") ui.Success("Workflow compiled") - // Read the compiled workflow binary - tmpWasmLocation := filepath.Join(workflowRootFolder, tmpWasmFileName) - wasmFileBinary, err := os.ReadFile(tmpWasmLocation) - if err != nil { - return fmt.Errorf("failed to read workflow binary: %w", err) - } - // Read the config file var config []byte if inputs.ConfigPath != "" { diff --git a/cmd/workflow/simulate/simulate_test.go b/cmd/workflow/simulate/simulate_test.go index e9df7560..08d6d30a 100644 --- a/cmd/workflow/simulate/simulate_test.go +++ b/cmd/workflow/simulate/simulate_test.go @@ -29,6 +29,14 @@ func TestBlankWorkflowSimulation(t *testing.T) { absWorkflowPath, err := filepath.Abs(workflowPath) require.NoError(t, err) + // Run test from workflow dir so short relative paths (max 97 chars) work + prevWd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(absWorkflowPath)) + t.Cleanup(func() { + _ = os.Chdir(prevWd) + }) + // Clean up common artifacts produced by the compile/simulate flow outB64 := filepath.Join(absWorkflowPath, "binary.wasm.br.b64") t.Cleanup(func() { @@ -47,10 +55,11 @@ func TestBlankWorkflowSimulation(t *testing.T) { rpc.Url = "https://sepolia.infura.io/v3" v.Set(fmt.Sprintf("%s.%s", "staging-settings", settings.RpcsSettingName), []settings.RpcEndpoint{rpc}) + // Use relative paths so validation (max 97 chars) passes; cwd is workflow dir var workflowSettings settings.WorkflowSettings workflowSettings.UserWorkflowSettings.WorkflowName = "blank-workflow" - workflowSettings.WorkflowArtifactSettings.WorkflowPath = filepath.Join(absWorkflowPath, "main.go") - workflowSettings.WorkflowArtifactSettings.ConfigPath = filepath.Join(absWorkflowPath, "config.json") + workflowSettings.WorkflowArtifactSettings.WorkflowPath = "main.go" + workflowSettings.WorkflowArtifactSettings.ConfigPath = "config.json" // Mock `runtime.Context` with a test logger. runtimeCtx := &runtime.Context{ diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index 72e5b699..bc4298c2 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/cmd/workflow/activate" + "github.com/smartcontractkit/cre-cli/cmd/workflow/convert" "github.com/smartcontractkit/cre-cli/cmd/workflow/delete" "github.com/smartcontractkit/cre-cli/cmd/workflow/deploy" "github.com/smartcontractkit/cre-cli/cmd/workflow/pause" @@ -20,6 +21,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { } workflowCmd.AddCommand(activate.New(runtimeContext)) + workflowCmd.AddCommand(convert.New(runtimeContext)) workflowCmd.AddCommand(delete.New(runtimeContext)) workflowCmd.AddCommand(pause.New(runtimeContext)) workflowCmd.AddCommand(test.New(runtimeContext)) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 7c0c854c..c213add7 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -53,6 +53,7 @@ const ( WorkflowLanguageGolang = "golang" WorkflowLanguageTypeScript = "typescript" + WorkflowLanguageWasm = "wasm" TestAddress = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" TestAddress2 = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" diff --git a/internal/settings/workflow_settings.go b/internal/settings/workflow_settings.go index cf62e3d9..5d7bf476 100644 --- a/internal/settings/workflow_settings.go +++ b/internal/settings/workflow_settings.go @@ -3,14 +3,81 @@ package settings import ( "fmt" "net/url" + "os" "strings" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" + "sigs.k8s.io/yaml" ) +// GetWorkflowPathFromFile reads workflow-path from a workflow.yaml file (same value deploy/simulate get from Settings). +func GetWorkflowPathFromFile(workflowYAMLPath string) (string, error) { + data, err := os.ReadFile(workflowYAMLPath) + if err != nil { + return "", fmt.Errorf("read workflow settings: %w", err) + } + var raw map[string]interface{} + if err := yaml.Unmarshal(data, &raw); err != nil { + return "", fmt.Errorf("parse workflow settings: %w", err) + } + return workflowPathFromRaw(raw) +} + +// SetWorkflowPathInFile sets workflow-path in workflow.yaml (both staging-settings and production-settings) and writes the file. +func SetWorkflowPathInFile(workflowYAMLPath, newPath string) error { + data, err := os.ReadFile(workflowYAMLPath) + if err != nil { + return fmt.Errorf("read workflow settings: %w", err) + } + var raw map[string]interface{} + if err := yaml.Unmarshal(data, &raw); err != nil { + return fmt.Errorf("parse workflow settings: %w", err) + } + setWorkflowPathInRaw(raw, newPath) + out, err := yaml.Marshal(raw) + if err != nil { + return fmt.Errorf("marshal workflow settings: %w", err) + } + if err := os.WriteFile(workflowYAMLPath, out, 0600); err != nil { + return fmt.Errorf("write workflow settings: %w", err) + } + return nil +} + +func workflowPathFromRaw(raw map[string]interface{}) (string, error) { + for _, key := range []string{"staging-settings", "production-settings"} { + target, _ := raw[key].(map[string]interface{}) + if target == nil { + continue + } + artifacts, _ := target["workflow-artifacts"].(map[string]interface{}) + if artifacts == nil { + continue + } + if p, ok := artifacts["workflow-path"].(string); ok && p != "" { + return p, nil + } + } + return "", fmt.Errorf("workflow-path not found in workflow settings") +} + +func setWorkflowPathInRaw(raw map[string]interface{}, path string) { + for _, key := range []string{"staging-settings", "production-settings"} { + target, _ := raw[key].(map[string]interface{}) + if target == nil { + continue + } + artifacts, _ := target["workflow-artifacts"].(map[string]interface{}) + if artifacts == nil { + continue + } + artifacts["workflow-path"] = path + } +} + type WorkflowSettings struct { UserWorkflowSettings struct { WorkflowOwnerAddress string `mapstructure:"workflow-owner-address" yaml:"workflow-owner-address"` diff --git a/internal/testutil/graphql_mock.go b/internal/testutil/graphql_mock.go new file mode 100644 index 00000000..98bbd188 --- /dev/null +++ b/internal/testutil/graphql_mock.go @@ -0,0 +1,42 @@ +package testutil + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/smartcontractkit/cre-cli/internal/environments" +) + +// NewGraphQLMockServerGetOrganization starts an httptest.Server that responds to +// getOrganization with a fixed organizationId. It sets EnvVarGraphQLURL so CLI +// commands use this server. Caller must defer srv.Close(). +func NewGraphQLMockServerGetOrganization(t *testing.T) *httptest.Server { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost { + var req struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables"` + } + _ = json.NewDecoder(r.Body).Decode(&req) + w.Header().Set("Content-Type", "application/json") + if strings.Contains(req.Query, "getOrganization") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOrganization": map[string]any{"organizationId": "test-org-id"}, + }, + }) + return + } + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + } + })) + t.Setenv(environments.EnvVarGraphQLURL, srv.URL+"/graphql") + return srv +} diff --git a/internal/validation/files/path_read.go b/internal/validation/files/path_read.go index 84d6cce2..b6c879d7 100644 --- a/internal/validation/files/path_read.go +++ b/internal/validation/files/path_read.go @@ -3,32 +3,45 @@ package files import ( "fmt" "os" + "path/filepath" "reflect" + "strings" "github.com/go-playground/validator/v10" ) func HasReadAccessToPath(fl validator.FieldLevel) bool { - field := fl.Field() - - if field.Kind() != reflect.String { - panic(fmt.Sprintf("input field name is not a string: %s", fl.FieldName())) - } - - path := field.String() + path := mustBeString(fl) + return hasReadAccessToPath(path) +} - // Check if the file or directory exists +func hasReadAccessToPath(path string) bool { _, err := os.Stat(path) if err != nil { return false } - - // Attempt to open the file or directory to verify read access file, err := os.Open(path) if err != nil { return false } defer file.Close() - return true } + +// HasReadAccessToWorkflowPath validates workflow-path: for .wasm paths only the containing +// directory must exist (CompileWorkflowToWasm will run make build); otherwise same as path_read. +func HasReadAccessToWorkflowPath(fl validator.FieldLevel) bool { + path := mustBeString(fl) + if strings.HasSuffix(path, ".wasm") { + return hasReadAccessToPath(filepath.Dir(path)) + } + return hasReadAccessToPath(path) +} + +func mustBeString(fl validator.FieldLevel) string { + field := fl.Field() + if field.Kind() != reflect.String { + panic(fmt.Sprintf("input field name is not a string: %s", fl.FieldName())) + } + return field.String() +} diff --git a/internal/validation/validation.go b/internal/validation/validation.go index c36a07e8..5ca86ff0 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -14,15 +14,16 @@ import ( ) var customValidators = map[string]validator.Func{ - "ecdsa_private_key": isECDSAPrivateKey, - "uint8_string_array": isUint8Array, - "json": files.IsValidJSON, - "path_read": files.HasReadAccessToPath, - "project_name": isProjectName, - "wasm": files.IsValidWASM, - "workflow_name": isWorkflowName, - "workflow_owner": isWorkflowOwner, - "yaml": files.IsValidYAML, + "ecdsa_private_key": isECDSAPrivateKey, + "uint8_string_array": isUint8Array, + "json": files.IsValidJSON, + "path_read": files.HasReadAccessToPath, + "project_name": isProjectName, + "wasm": files.IsValidWASM, + "workflow_name": isWorkflowName, + "workflow_owner": isWorkflowOwner, + "workflow_path_read": files.HasReadAccessToWorkflowPath, + "yaml": files.IsValidYAML, } var customTranslations = map[string]string{ @@ -37,6 +38,7 @@ var customTranslations = map[string]string{ "http_url|eq=": "{0} must be empty or a valid HTTP URL: {1}", "json": "{0} must be a valid JSON file: {1}", "path_read": "{0} must have read access to path: {1}", + "workflow_path_read": "{0} must have read access to path: {1}", "project_name": "{0} must be non-empty, no longer than 64 characters, and contain only letters (a-z, A-Z), numbers (0-9), dashes (-), and underscores (_): {1}", "wasm": "{0} must be a valid WASM file: {1}", "workflow_name": "{0} must be non-empty, no longer than 64 characters, and contain only letters (a-z, A-Z), numbers (0-9), dashes (-), and underscores (_): {1}", diff --git a/test/convert_simulate_helper.go b/test/convert_simulate_helper.go new file mode 100644 index 00000000..5e86f124 --- /dev/null +++ b/test/convert_simulate_helper.go @@ -0,0 +1,77 @@ +package test + +import ( + "bytes" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/require" +) + +func convertSimulateCaptureOutput(t *testing.T, projectRoot, workflowName string) string { + t.Helper() + var stdout, stderr bytes.Buffer + cmd := exec.Command(CLIPath, "workflow", "simulate", workflowName, + "--project-root", projectRoot, + "--non-interactive", "--trigger-index=0", + ) + cmd.Dir = projectRoot + cmd.Stdout = &stdout + cmd.Stderr = &stderr + require.NoError(t, cmd.Run(), + "simulate (before convert) failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), stderr.String()) + return stdout.String() +} + +func convertSimulateRequireOutputContains(t *testing.T, projectRoot, workflowName, expectedSubstring string) { + t.Helper() + var stdout, stderr bytes.Buffer + cmd := exec.Command(CLIPath, "workflow", "simulate", workflowName, + "--project-root", projectRoot, + "--non-interactive", "--trigger-index=0", + ) + cmd.Dir = projectRoot + cmd.Stdout = &stdout + cmd.Stderr = &stderr + require.NoError(t, cmd.Run(), + "simulate (after convert) failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), stderr.String()) + require.Contains(t, stdout.String(), expectedSubstring, + "simulate output after convert should contain %q", expectedSubstring) +} + +// ConvertSimulateBeforeAfter runs simulate (capture output), convert, then simulate again +// and verifies output contains the same expectedSubstring. Simulate runs make build internally when needed. +func ConvertSimulateBeforeAfter(t *testing.T, projectRoot, workflowDir, workflowName, expectedSubstring string) { + t.Helper() + beforeOutput := convertSimulateCaptureOutput(t, projectRoot, workflowName) + require.Contains(t, beforeOutput, expectedSubstring, + "baseline simulate output should contain %q", expectedSubstring) + convertRunConvert(t, projectRoot, workflowDir) + convertSimulateRequireOutputContains(t, projectRoot, workflowName, expectedSubstring) +} + +func convertRunConvert(t *testing.T, projectRoot, workflowDir string) { + t.Helper() + var stdout, stderr bytes.Buffer + cmd := exec.Command(CLIPath, "workflow", "convert-to-custom-build", workflowDir, "-f") + cmd.Dir = projectRoot + cmd.Stdout = &stdout + cmd.Stderr = &stderr + require.NoError(t, cmd.Run(), + "convert failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) +} + +func convertRunMakeBuild(t *testing.T, workflowDir string, env ...string) { + t.Helper() + var stdout, stderr bytes.Buffer + cmd := exec.Command("make", "build") + cmd.Dir = workflowDir + cmd.Env = append(os.Environ(), env...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + require.NoError(t, cmd.Run(), + "make build failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) +} diff --git a/test/graphql_mock.go b/test/graphql_mock.go new file mode 100644 index 00000000..8298fe02 --- /dev/null +++ b/test/graphql_mock.go @@ -0,0 +1,14 @@ +package test + +import ( + "net/http/httptest" + "testing" + + "github.com/smartcontractkit/cre-cli/internal/testutil" +) + +// NewGraphQLMockServerGetOrganization starts a mock GraphQL server that responds to +// getOrganization and sets EnvVarGraphQLURL. Caller must defer srv.Close(). +func NewGraphQLMockServerGetOrganization(t *testing.T) *httptest.Server { + return testutil.NewGraphQLMockServerGetOrganization(t) +} diff --git a/test/init_and_binding_generation_and_simulate_go_test.go b/test/init_and_binding_generation_and_simulate_go_test.go index c12d9d1c..190d1922 100644 --- a/test/init_and_binding_generation_and_simulate_go_test.go +++ b/test/init_and_binding_generation_and_simulate_go_test.go @@ -2,20 +2,15 @@ package test import ( "bytes" - "encoding/json" - "net/http" - "net/http/httptest" "os" "os/exec" "path/filepath" - "strings" "testing" "github.com/stretchr/testify/require" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" - "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/settings" ) @@ -33,41 +28,9 @@ func TestE2EInit_DevPoRTemplate(t *testing.T) { // Set dummy API key t.Setenv(credentials.CreApiKeyVar, "test-api") - // Set up mock GraphQL server for authentication validation - // This is needed because validation now runs early in command execution - gqlSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost { - var req struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables"` - } - _ = json.NewDecoder(r.Body).Decode(&req) - - w.Header().Set("Content-Type", "application/json") - - // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", - }, - }, - }) - return - } - - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]any{ - "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, - }) - } - })) + gqlSrv := NewGraphQLMockServerGetOrganization(t) defer gqlSrv.Close() - // Point GraphQL client to mock server - t.Setenv(environments.EnvVarGraphQLURL, gqlSrv.URL+"/graphql") - initArgs := []string{ "init", "--project-root", tempDir, diff --git a/test/init_and_simulate_ts_test.go b/test/init_and_simulate_ts_test.go index b4265c54..563ba5a9 100644 --- a/test/init_and_simulate_ts_test.go +++ b/test/init_and_simulate_ts_test.go @@ -2,19 +2,14 @@ package test import ( "bytes" - "encoding/json" - "net/http" - "net/http/httptest" "os/exec" "path/filepath" - "strings" "testing" "github.com/stretchr/testify/require" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" - "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/settings" ) @@ -32,41 +27,9 @@ func TestE2EInit_DevPoRTemplateTS(t *testing.T) { // Set dummy API key t.Setenv(credentials.CreApiKeyVar, "test-api") - // Set up mock GraphQL server for authentication validation - // This is needed because validation now runs early in command execution - gqlSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost { - var req struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables"` - } - _ = json.NewDecoder(r.Body).Decode(&req) - - w.Header().Set("Content-Type", "application/json") - - // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", - }, - }, - }) - return - } - - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]any{ - "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, - }) - } - })) + gqlSrv := NewGraphQLMockServerGetOrganization(t) defer gqlSrv.Close() - // Point GraphQL client to mock server - t.Setenv(environments.EnvVarGraphQLURL, gqlSrv.URL+"/graphql") - initArgs := []string{ "init", "--project-root", tempDir, diff --git a/test/init_and_simulate_wasm_test.go b/test/init_and_simulate_wasm_test.go new file mode 100644 index 00000000..ae691bee --- /dev/null +++ b/test/init_and_simulate_wasm_test.go @@ -0,0 +1,215 @@ +package test + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +func TestE2EInit_WasmBlankTemplate(t *testing.T) { + tempDir := t.TempDir() + projectName := "e2e-init-wasm-test" + workflowName := "wasmWorkflow" + templateID := "6" + projectRoot := filepath.Join(tempDir, projectName) + workflowDirectory := filepath.Join(projectRoot, workflowName) + + ethKey := "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + t.Setenv(settings.EthPrivateKeyEnvVar, ethKey) + + // Set dummy API key + t.Setenv(credentials.CreApiKeyVar, "test-api") + + gqlSrv := NewGraphQLMockServerGetOrganization(t) + defer gqlSrv.Close() + + // --- cre init with WASM template --- + initArgs := []string{ + "init", + "--project-root", tempDir, + "--project-name", projectName, + "--template-id", templateID, + "--workflow-name", workflowName, + } + var stdout, stderr bytes.Buffer + initCmd := exec.Command(CLIPath, initArgs...) + initCmd.Dir = tempDir + initCmd.Stdout = &stdout + initCmd.Stderr = &stderr + + require.NoError( + t, + initCmd.Run(), + "cre init failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + + require.FileExists(t, filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)) + require.FileExists(t, filepath.Join(projectRoot, constants.DefaultEnvFileName)) + require.DirExists(t, workflowDirectory) + + expectedFiles := []string{"README.md", "Makefile", "workflow.yaml", "config.staging.json", "config.production.json", "secrets.yaml"} + for _, f := range expectedFiles { + require.FileExists(t, filepath.Join(workflowDirectory, f), "missing workflow file %q", f) + } + + // Create wasm directory + wasmDir := filepath.Join(workflowDirectory, "wasm") + require.NoError(t, os.MkdirAll(wasmDir, 0755)) + + // Create a simple Go workflow file similar to blankTemplate but with custom build tag + mainGoContent := `//go:build wasip1 && customwasm + +package main + +import ( + "fmt" + "log/slog" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" +) + +type ExecutionResult struct { + Result string +} + +type Config struct{} + +func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) { + cronTrigger := cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}) + return cre.Workflow[*Config]{ + cre.Handler(cronTrigger, onCronTrigger), + }, nil +} + +func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (*ExecutionResult, error) { + logger := runtime.Logger() + scheduledTime := trigger.ScheduledExecutionTime.AsTime() + logger.Info("Cron trigger fired", "scheduledTime", scheduledTime) + return &ExecutionResult{Result: fmt.Sprintf("Fired at %s", scheduledTime)}, nil +} + +func main() { + wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow) +} +` + mainGoPath := filepath.Join(workflowDirectory, "main.go") + require.NoError(t, os.WriteFile(mainGoPath, []byte(mainGoContent), 0600)) + + // Create go.mod file - will be updated by go mod tidy + goModContent := `module wasm-workflow + +go 1.25.3 + +require ( + github.com/smartcontractkit/cre-sdk-go v1.1.3 + github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.0.0-beta.0 +) +` + goModPath := filepath.Join(workflowDirectory, "go.mod") + require.NoError(t, os.WriteFile(goModPath, []byte(goModContent), 0600)) + + // Update Makefile to include build command with custom build tag + makefilePath := filepath.Join(workflowDirectory, "Makefile") + makefileContent := `.PHONY: build + +build: + GOOS=wasip1 GOARCH=wasm go build -tags customwasm -o wasm/workflow.wasm . +` + require.NoError(t, os.WriteFile(makefilePath, []byte(makefileContent), 0600)) + + // Run go mod tidy to resolve dependencies + stdout.Reset() + stderr.Reset() + tidyCmd := exec.Command("go", "mod", "tidy") + tidyCmd.Dir = workflowDirectory + tidyCmd.Stdout = &stdout + tidyCmd.Stderr = &stderr + + require.NoError( + t, + tidyCmd.Run(), + "go mod tidy failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + + // Build the workflow using make build + stdout.Reset() + stderr.Reset() + buildCmd := exec.Command("make", "build") + buildCmd.Dir = workflowDirectory + buildCmd.Stdout = &stdout + buildCmd.Stderr = &stderr + + require.NoError( + t, + buildCmd.Run(), + "make build failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + + // Verify WASM file was created + wasmFilePath := filepath.Join(wasmDir, "workflow.wasm") + require.FileExists(t, wasmFilePath, "WASM file should be created by make build") + + // --- cre workflow simulate wasmWorkflow --- + stdout.Reset() + stderr.Reset() + simulateArgs := []string{ + "workflow", "simulate", + workflowName, + "--project-root", projectRoot, + "--non-interactive", + "--trigger-index=0", + } + simulateCmd := exec.Command(CLIPath, simulateArgs...) + simulateCmd.Dir = projectRoot + simulateCmd.Stdout = &stdout + simulateCmd.Stderr = &stderr + + require.NoError( + t, + simulateCmd.Run(), + "cre workflow simulate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + + // --- cre workflow compile wasmWorkflow --- + stdout.Reset() + stderr.Reset() + compileArgs := []string{ + "workflow", "compile", + filepath.Join(workflowDirectory, "workflow.yaml"), + "--project-root", projectRoot, + } + compileCmd := exec.Command(CLIPath, compileArgs...) + compileCmd.Dir = projectRoot + compileCmd.Stdout = &stdout + compileCmd.Stderr = &stderr + + require.NoError( + t, + compileCmd.Run(), + "cre workflow compile failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + + // Verify compiled output exists + outputPath := filepath.Join(workflowDirectory, "binary.wasm.br.b64") + require.FileExists(t, outputPath, "compiled output should exist") +} diff --git a/test/init_convert_simulate_go_test.go b/test/init_convert_simulate_go_test.go new file mode 100644 index 00000000..09ad191d --- /dev/null +++ b/test/init_convert_simulate_go_test.go @@ -0,0 +1,116 @@ +package test + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// TestE2EInit_ConvertToCustomBuild_Go: init (blank Go), simulate (capture), convert, make build, simulate (require match), +// then add FlagProof/constA/constB/Makefile FLAG, make with FLAG=customFlag/differentFlag, simulate and assert. +func TestE2EInit_ConvertToCustomBuild_Go(t *testing.T) { + tempDir := t.TempDir() + projectName := "e2e-convert-go" + workflowName := "goWorkflow" + templateID := "2" // blank Go template + projectRoot := filepath.Join(tempDir, projectName) + workflowDirectory := filepath.Join(projectRoot, workflowName) + + t.Setenv(settings.EthPrivateKeyEnvVar, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + t.Setenv(credentials.CreApiKeyVar, "test-api") + + gqlSrv := NewGraphQLMockServerGetOrganization(t) + defer gqlSrv.Close() + + // --- cre init with blank Go template --- + var stdout, stderr bytes.Buffer + initCmd := exec.Command(CLIPath, "init", + "--project-root", tempDir, + "--project-name", projectName, + "--template-id", templateID, + "--workflow-name", workflowName, + ) + initCmd.Dir = tempDir + initCmd.Stdout = &stdout + initCmd.Stderr = &stderr + require.NoError(t, initCmd.Run(), "cre init failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + + require.FileExists(t, filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)) + require.DirExists(t, workflowDirectory) + require.FileExists(t, filepath.Join(workflowDirectory, "main.go")) + + // go mod tidy so simulate can build + tidyCmd := exec.Command("go", "mod", "tidy") + tidyCmd.Dir = workflowDirectory + tidyCmd.Stdout = &stdout + tidyCmd.Stderr = &stderr + require.NoError(t, tidyCmd.Run(), "go mod tidy failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + + // Before/after: simulate (capture), convert, make build, simulate (verify same key content) + ConvertSimulateBeforeAfter(t, projectRoot, workflowDirectory, workflowName, "Fired at") + require.FileExists(t, filepath.Join(workflowDirectory, "Makefile")) + require.DirExists(t, filepath.Join(workflowDirectory, "wasm")) + + // Now make test-specific changes: FlagProof, constA/constB, Makefile FLAG + mainPath := filepath.Join(workflowDirectory, "main.go") + mainBytes, err := os.ReadFile(mainPath) + require.NoError(t, err) + mainStr := string(mainBytes) + mainStr = strings.Replace(mainStr, "type ExecutionResult struct {\n\tResult string\n}", "type ExecutionResult struct {\n\tResult string\n\tFlagProof string\n}", 1) + mainStr = strings.Replace(mainStr, "\t// Your logic here...\n\n\treturn &ExecutionResult{Result: fmt.Sprintf(\"Fired at %s\", scheduledTime)}, nil", + "\treturn &ExecutionResult{Result: fmt.Sprintf(\"Fired at %s\", scheduledTime), FlagProof: FlagProof}, nil", 1) + require.NoError(t, os.WriteFile(mainPath, []byte(mainStr), 0600)) + + constA := `//go:build customFlag + +package main + +const FlagProof = "set" +` + require.NoError(t, os.WriteFile(filepath.Join(workflowDirectory, "constA.go"), []byte(constA), 0600)) + + constB := `//go:build !customFlag + +package main + +const FlagProof = "unset" +` + require.NoError(t, os.WriteFile(filepath.Join(workflowDirectory, "constB.go"), []byte(constB), 0600)) + + makefilePath := filepath.Join(workflowDirectory, "Makefile") + makefile, err := os.ReadFile(makefilePath) + require.NoError(t, err) + makefileStr := strings.Replace(string(makefile), "go build -o", "go build -tags $(FLAG) -o", 1) + require.NoError(t, os.WriteFile(makefilePath, []byte(makefileStr), 0600)) + + convertGoBuildWithFlagAndAssert(t, projectRoot, workflowDirectory, workflowName, "FLAG=customFlag", "set", "FlagProof") + convertGoBuildWithFlagAndAssert(t, projectRoot, workflowDirectory, workflowName, "FLAG=differentFlag", "unset", "FlagProof") +} + +func convertGoBuildWithFlagAndAssert(t *testing.T, projectRoot, workflowDir, workflowName, envVar, wantSubstr, wantSubstr2 string) { + t.Helper() + convertRunMakeBuild(t, workflowDir, envVar) + var stdout, stderr bytes.Buffer + cmd := exec.Command(CLIPath, "workflow", "simulate", workflowName, + "--project-root", projectRoot, + "--non-interactive", "--trigger-index=0", + ) + cmd.Dir = projectRoot + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if envVar != "" { + cmd.Env = append(os.Environ(), envVar) + } + require.NoError(t, cmd.Run(), "simulate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + require.Contains(t, stdout.String(), wantSubstr) + require.Contains(t, stdout.String(), wantSubstr2) +} diff --git a/test/init_convert_simulate_ts_test.go b/test/init_convert_simulate_ts_test.go new file mode 100644 index 00000000..362493e4 --- /dev/null +++ b/test/init_convert_simulate_ts_test.go @@ -0,0 +1,147 @@ +package test + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// TestE2EInit_ConvertToCustomBuild_TS: init (typescriptSimpleExample), bun install, simulate (capture), +// convert, simulate (verify same). Verify conversion did not change main.ts. Then test-only: copy +// workflow-wrapper, write custom compile-to-js with define section in Bun.build, patch main.ts, Makefile. +// make with FLAG=customFlag/differentFlag, simulate and assert. +func TestE2EInit_ConvertToCustomBuild_TS(t *testing.T) { + tempDir := t.TempDir() + projectName := "e2e-convert-ts" + workflowName := "tsWorkflow" + templateID := "3" // typescriptSimpleExample + projectRoot := filepath.Join(tempDir, projectName) + workflowDirectory := filepath.Join(projectRoot, workflowName) + + t.Setenv(settings.EthPrivateKeyEnvVar, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + t.Setenv(credentials.CreApiKeyVar, "test-api") + + gqlSrv := NewGraphQLMockServerGetOrganization(t) + defer gqlSrv.Close() + + // --- cre init with typescriptSimpleExample --- + var stdout, stderr bytes.Buffer + initCmd := exec.Command(CLIPath, "init", + "--project-root", tempDir, + "--project-name", projectName, + "--template-id", templateID, + "--workflow-name", workflowName, + ) + initCmd.Dir = tempDir + initCmd.Stdout = &stdout + initCmd.Stderr = &stderr + require.NoError(t, initCmd.Run(), "cre init failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + + require.FileExists(t, filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)) + require.DirExists(t, workflowDirectory) + require.FileExists(t, filepath.Join(workflowDirectory, "main.ts")) + + // bun install so simulate can build + installCmd := exec.Command("bun", "install") + installCmd.Dir = workflowDirectory + installCmd.Stdout = &stdout + installCmd.Stderr = &stderr + require.NoError(t, installCmd.Run(), "bun install failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + + ConvertSimulateBeforeAfter(t, projectRoot, workflowDirectory, workflowName, "Hello world!") + require.FileExists(t, filepath.Join(workflowDirectory, "Makefile")) + require.DirExists(t, filepath.Join(workflowDirectory, "wasm")) + + // Verify conversion did not change main.ts + mainPath := filepath.Join(workflowDirectory, "main.ts") + mainBefore, err := os.ReadFile(mainPath) + require.NoError(t, err) + require.Contains(t, string(mainBefore), `return "Hello world!";`, "convert must not modify workflow source") + + // Test-only: copy compile-to-js and workflow-wrapper from SDK, then patch to add define (so FLAG env drives the build). + scriptsDir := filepath.Join(workflowDirectory, "scripts") + require.NoError(t, os.MkdirAll(scriptsDir, 0755)) + srcDir := filepath.Join(workflowDirectory, "node_modules", "@chainlink", "cre-sdk", "scripts", "src") + for _, name := range []string{"compile-to-js.ts", "workflow-wrapper.ts"} { + b, err := os.ReadFile(filepath.Join(srcDir, name)) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(scriptsDir, name), b, 0600)) + } + compileToJSPath := filepath.Join(scriptsDir, "compile-to-js.ts") + compileToJS, err := os.ReadFile(compileToJSPath) + require.NoError(t, err) + src := string(compileToJS) + // Use local workflow-wrapper (script is under scripts/, not SDK entry) + if !strings.Contains(src, "workflow-wrapper") { + src = strings.Replace(src, `import { $ } from "bun";`, `import { $ } from "bun"; +import { wrapWorkflowCode } from "./workflow-wrapper";`, 1) + } + // Argv slice(2) so "bun scripts/compile-to-js.ts main.ts wasm/workflow.js" passes both args + // it's called differently than the SDK so we need to patch it + src = strings.Replace(src, "process.argv.slice(3)", "process.argv.slice(2)", 1) + + defineBlock := "define: {\n\t\t\tBUILD_FLAG: JSON.stringify(process.env.FLAG ?? \"\"),\n\t\t},\n\t\t" + anchor := "naming: path.basename(resolvedOutput)," + if idx := strings.Index(src, anchor); idx >= 0 { + src = src[:idx] + defineBlock + src[idx:] + } + require.Contains(t, src, "define:", "patch must add define section to Bun.build") + if !strings.Contains(src, "main().catch") && !strings.Contains(src, "await main()") { + src = src + "\nmain().catch((err: unknown) => { console.error(err); process.exit(1); });\n" + } + require.NoError(t, os.WriteFile(compileToJSPath, []byte(src), 0600)) + + mainStr := string(mainBefore) + mainStr = "declare const BUILD_FLAG: string;\n" + mainStr + newReturn := `return BUILD_FLAG === "customFlag" ? "Hello World (custom)" : "Hello World (default)";` + for _, oldReturn := range []string{` return "Hello world!";`, `return "Hello world!";`} { + if strings.Contains(mainStr, oldReturn) { + mainStr = strings.Replace(mainStr, oldReturn, newReturn, 1) + break + } + } + require.Contains(t, mainStr, "Hello World (custom)", "main.ts return patch must apply") + require.NoError(t, os.WriteFile(mainPath, []byte(mainStr), 0600)) + + makefilePath := filepath.Join(workflowDirectory, "Makefile") + makefileContent := `.PHONY: build + +build: + FLAG=$(FLAG) bun scripts/compile-to-js.ts main.ts wasm/workflow.js + bunx cre-compile-workflow wasm/workflow.js wasm/workflow.wasm +` + require.NoError(t, os.WriteFile(makefilePath, []byte(makefileContent), 0600)) + + convertTSBuildWithFlagAndAssert(t, projectRoot, workflowDirectory, workflowName, "FLAG=customFlag", "Hello World (custom)") + convertTSBuildWithFlagAndAssert(t, projectRoot, workflowDirectory, workflowName, "FLAG=differentFlag", "Hello World (default)") +} + +func convertTSBuildWithFlagAndAssert(t *testing.T, projectRoot, workflowDir, workflowName, envVar, wantSubstr string) { + t.Helper() + convertRunMakeBuild(t, workflowDir, envVar) + var stdout, stderr bytes.Buffer + workflowDirAbs, err := filepath.Abs(workflowDir) + require.NoError(t, err) + cmd := exec.Command(CLIPath, "workflow", "simulate", workflowDirAbs, + "--project-root", projectRoot, + "--non-interactive", "--trigger-index=0", + ) + cmd.Dir = projectRoot + cmd.Stdout = &stdout + cmd.Stderr = &stderr + // Simulate runs CompileWorkflowToWasm which runs make build again; pass env so the rebuild uses the same FLAG + if envVar != "" { + cmd.Env = append(os.Environ(), envVar) + } + require.NoError(t, cmd.Run(), "simulate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) + require.Contains(t, stdout.String(), wantSubstr) +} diff --git a/test/multi_command_flows/workflow_simulator_path.go b/test/multi_command_flows/workflow_simulator_path.go index cdafd1bd..9f8bd17f 100644 --- a/test/multi_command_flows/workflow_simulator_path.go +++ b/test/multi_command_flows/workflow_simulator_path.go @@ -8,13 +8,12 @@ import ( "os" "os/exec" "path/filepath" - "strings" "testing" "time" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/testutil" ) type testEVMConfig struct { @@ -84,37 +83,9 @@ func RunSimulationHappyPath(t *testing.T, tc TestConfig, projectDir string) { t.Helper() t.Run("Simulate", func(t *testing.T) { - // Set up GraphQL mock server for authentication validation - gqlSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost { - var req graphQLRequest - _ = json.NewDecoder(r.Body).Decode(&req) - - w.Header().Set("Content-Type", "application/json") - - // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { - _ = json.NewEncoder(w).Encode(map[string]any{ - "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", - }, - }, - }) - return - } - - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]any{ - "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, - }) - } - })) + gqlSrv := testutil.NewGraphQLMockServerGetOrganization(t) defer gqlSrv.Close() - // Point GraphQL client to mock server - t.Setenv(environments.EnvVarGraphQLURL, gqlSrv.URL+"/graphql") - srv := startMockPORServer(t) patchWorkflowConfigURL(t, projectDir, "por_workflow", srv.URL) From be2a448e9f358b2fb6bf5c2d02cf19f0ffec81ec Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 12 Feb 2026 09:48:11 -0500 Subject: [PATCH 02/11] Fix lint and regenerate docs --- cmd/root.go | 34 +++++++++++++------------- cmd/workflow/convert/convert.go | 4 +-- docs/cre_workflow.md | 1 + internal/validation/files/path_read.go | 8 +++--- internal/validation/validation.go | 20 +++++++-------- 5 files changed, 34 insertions(+), 33 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index deabecf4..6d0a3c75 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -364,24 +364,24 @@ func newRootCommand() *cobra.Command { func isLoadSettings(cmd *cobra.Command) bool { // It is not expected to have the settings file when running the following commands var excludedCommands = map[string]struct{}{ - "cre version": {}, - "cre login": {}, - "cre logout": {}, - "cre whoami": {}, - "cre account list-key": {}, - "cre init": {}, - "cre generate-bindings": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre help": {}, - "cre update": {}, - "cre workflow": {}, + "cre version": {}, + "cre login": {}, + "cre logout": {}, + "cre whoami": {}, + "cre account list-key": {}, + "cre init": {}, + "cre generate-bindings": {}, + "cre completion bash": {}, + "cre completion fish": {}, + "cre completion powershell": {}, + "cre completion zsh": {}, + "cre help": {}, + "cre update": {}, + "cre workflow": {}, "cre workflow convert-to-custom-build": {}, - "cre account": {}, - "cre secrets": {}, - "cre": {}, + "cre account": {}, + "cre secrets": {}, + "cre": {}, } _, exists := excludedCommands[cmd.CommandPath()] diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go index 93b21f16..81940b99 100644 --- a/cmd/workflow/convert/convert.go +++ b/cmd/workflow/convert/convert.go @@ -23,7 +23,7 @@ const ( type Inputs struct { WorkflowFolder string - Force bool + Force bool } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -38,7 +38,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { handler := newHandler(runtimeContext) inputs := Inputs{ WorkflowFolder: args[0], - Force: force, + Force: force, } return handler.Execute(inputs) }, diff --git a/docs/cre_workflow.md b/docs/cre_workflow.md index a5b83833..9f1bd57c 100644 --- a/docs/cre_workflow.md +++ b/docs/cre_workflow.md @@ -29,6 +29,7 @@ cre workflow [optional flags] * [cre](cre.md) - CRE CLI tool * [cre workflow activate](cre_workflow_activate.md) - Activates workflow on the Workflow Registry contract +* [cre workflow convert-to-custom-build](cre_workflow_convert-to-custom-build.md) - Converts an existing workflow to a custom (self-compiled) build * [cre workflow delete](cre_workflow_delete.md) - Deletes all versions of a workflow from the Workflow Registry * [cre workflow deploy](cre_workflow_deploy.md) - Deploys a workflow to the Workflow Registry contract * [cre workflow pause](cre_workflow_pause.md) - Pauses workflow on the Workflow Registry contract diff --git a/internal/validation/files/path_read.go b/internal/validation/files/path_read.go index b6c879d7..07034651 100644 --- a/internal/validation/files/path_read.go +++ b/internal/validation/files/path_read.go @@ -12,10 +12,10 @@ import ( func HasReadAccessToPath(fl validator.FieldLevel) bool { path := mustBeString(fl) - return hasReadAccessToPath(path) + return checkReadAccess(path) } -func hasReadAccessToPath(path string) bool { +func checkReadAccess(path string) bool { _, err := os.Stat(path) if err != nil { return false @@ -33,9 +33,9 @@ func hasReadAccessToPath(path string) bool { func HasReadAccessToWorkflowPath(fl validator.FieldLevel) bool { path := mustBeString(fl) if strings.HasSuffix(path, ".wasm") { - return hasReadAccessToPath(filepath.Dir(path)) + return checkReadAccess(filepath.Dir(path)) } - return hasReadAccessToPath(path) + return checkReadAccess(path) } func mustBeString(fl validator.FieldLevel) string { diff --git a/internal/validation/validation.go b/internal/validation/validation.go index 5ca86ff0..799dbf46 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -14,16 +14,16 @@ import ( ) var customValidators = map[string]validator.Func{ - "ecdsa_private_key": isECDSAPrivateKey, - "uint8_string_array": isUint8Array, - "json": files.IsValidJSON, - "path_read": files.HasReadAccessToPath, - "project_name": isProjectName, - "wasm": files.IsValidWASM, - "workflow_name": isWorkflowName, - "workflow_owner": isWorkflowOwner, - "workflow_path_read": files.HasReadAccessToWorkflowPath, - "yaml": files.IsValidYAML, + "ecdsa_private_key": isECDSAPrivateKey, + "uint8_string_array": isUint8Array, + "json": files.IsValidJSON, + "path_read": files.HasReadAccessToPath, + "project_name": isProjectName, + "wasm": files.IsValidWASM, + "workflow_name": isWorkflowName, + "workflow_owner": isWorkflowOwner, + "workflow_path_read": files.HasReadAccessToWorkflowPath, + "yaml": files.IsValidYAML, } var customTranslations = map[string]string{ From de0377827d0cd1acc9081062acdd1a21e5d0a957 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 12 Feb 2026 11:11:05 -0500 Subject: [PATCH 03/11] Fix a bug adding extra env variables in tests that caused windows to fail the build --- test/convert_simulate_helper.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/convert_simulate_helper.go b/test/convert_simulate_helper.go index 5e86f124..f7b3d02d 100644 --- a/test/convert_simulate_helper.go +++ b/test/convert_simulate_helper.go @@ -2,7 +2,6 @@ package test import ( "bytes" - "os" "os/exec" "testing" @@ -64,12 +63,13 @@ func convertRunConvert(t *testing.T, projectRoot, workflowDir string) { "convert failed:\nSTDOUT:\n%s\nSTDERR:\n%s", stdout.String(), stderr.String()) } -func convertRunMakeBuild(t *testing.T, workflowDir string, env ...string) { +func convertRunMakeBuild(t *testing.T, workflowDir string, makeArgs ...string) { t.Helper() var stdout, stderr bytes.Buffer - cmd := exec.Command("make", "build") + args := []string{"build"} + args = append(args, makeArgs...) + cmd := exec.Command("make", args...) cmd.Dir = workflowDir - cmd.Env = append(os.Environ(), env...) cmd.Stdout = &stdout cmd.Stderr = &stderr require.NoError(t, cmd.Run(), From e624a37e43a07240d1b32e7234e8cd4a0c454acf Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 12 Feb 2026 11:45:41 -0500 Subject: [PATCH 04/11] Really fix windows by updating how env vars work in the makefile? --- .../template/workflow/wasmBlankTemplate/Makefile.tpl | 7 ++++++- .../template/workflow/wasmBlankTemplate/README.md | 11 ++++++----- cmd/workflow/convert/convert.go | 6 +++++- .../deploy/testdata/custom_wasm_workflow/Makefile | 6 +++++- test/init_and_simulate_wasm_test.go | 5 ++++- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl b/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl index 4d0f01be..2ebdb2d5 100644 --- a/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl @@ -1,9 +1,14 @@ .PHONY: build +# Example for Go - uncomment and modify the export lines and build command: +# export GOOS := wasip1 +# export GOARCH := wasm +# export CGO_ENABLED := 0 + build: # TODO: Add your build logic here # This target should compile your workflow to wasm/workflow.wasm # Example for Go: - # GOOS=wasip1 GOARCH=wasm go build -o wasm/workflow.wasm . + # go build -o wasm/workflow.wasm . @echo "Please implement the build target in the Makefile" @exit 1 diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/README.md b/cmd/creinit/template/workflow/wasmBlankTemplate/README.md index 94056ad3..fe2ba259 100644 --- a/cmd/creinit/template/workflow/wasmBlankTemplate/README.md +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/README.md @@ -23,10 +23,11 @@ This template provides a blank workflow template for self-compiled WASM workflow ## Example Makefile build target ```makefile +# For Go workflows: +export GOOS := wasip1 +export GOARCH := wasm +export CGO_ENABLED := 0 + build: - # TODO: Add your build logic here - # Example for Go: - # GOOS=wasip1 GOARCH=wasm go build -o wasm/workflow.wasm . - @echo "Please implement the build target in the Makefile" - exit 1 + go build -o wasm/workflow.wasm . ``` diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go index 81940b99..44f937a9 100644 --- a/cmd/workflow/convert/convert.go +++ b/cmd/workflow/convert/convert.go @@ -120,8 +120,12 @@ func (h *handler) Execute(inputs Inputs) error { func goMakefile() string { return `.PHONY: build +export GOOS := wasip1 +export GOARCH := wasm +export CGO_ENABLED := 0 + build: - GOOS=wasip1 GOARCH=wasm CGO_ENABLED=0 go build -o wasm/workflow.wasm -trimpath -ldflags="-buildid= -w -s" . + go build -o wasm/workflow.wasm -trimpath -ldflags="-buildid= -w -s" . ` } diff --git a/cmd/workflow/deploy/testdata/custom_wasm_workflow/Makefile b/cmd/workflow/deploy/testdata/custom_wasm_workflow/Makefile index c910fcf4..8f5b436e 100644 --- a/cmd/workflow/deploy/testdata/custom_wasm_workflow/Makefile +++ b/cmd/workflow/deploy/testdata/custom_wasm_workflow/Makefile @@ -1,4 +1,8 @@ .PHONY: build +export GOOS := wasip1 +export GOARCH := wasm +export CGO_ENABLED := 0 + build: - GOOS=wasip1 GOARCH=wasm CGO_ENABLED=0 go build -o wasm/workflow.wasm -trimpath -ldflags="-buildid= -w -s" . + go build -o wasm/workflow.wasm -trimpath -ldflags="-buildid= -w -s" . diff --git a/test/init_and_simulate_wasm_test.go b/test/init_and_simulate_wasm_test.go index ae691bee..4810f1a9 100644 --- a/test/init_and_simulate_wasm_test.go +++ b/test/init_and_simulate_wasm_test.go @@ -124,8 +124,11 @@ require ( makefilePath := filepath.Join(workflowDirectory, "Makefile") makefileContent := `.PHONY: build +export GOOS := wasip1 +export GOARCH := wasm + build: - GOOS=wasip1 GOARCH=wasm go build -tags customwasm -o wasm/workflow.wasm . + go build -tags customwasm -o wasm/workflow.wasm . ` require.NoError(t, os.WriteFile(makefilePath, []byte(makefileContent), 0600)) From 1773c41b2dc6d041f533cd0ec00ed171084e6e31 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 12 Feb 2026 13:16:05 -0500 Subject: [PATCH 05/11] Use filepath.join instead of hard-coded slash --- cmd/common/compile.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/common/compile.go b/cmd/common/compile.go index 2f1798cd..4e844708 100644 --- a/cmd/common/compile.go +++ b/cmd/common/compile.go @@ -11,10 +11,9 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" ) -const ( - makefileName = "Makefile" - defaultWasmOutput = "wasm/workflow.wasm" // hardcoded in Makefile by convert; must match workflow-path default -) +const makefileName = "Makefile" + +var defaultWasmOutput = filepath.Join("wasm", "workflow.wasm") // getBuildCmd returns a single step that builds the workflow and returns the WASM bytes. func getBuildCmd(workflowRootFolder, mainFile, language string) (func() ([]byte, error), error) { From 089b31d32a725d4d16a9cb34f3e699be4ec491cc Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Thu, 12 Feb 2026 14:11:36 -0500 Subject: [PATCH 06/11] New line replacement from template to fix windows test? --- test/init_convert_simulate_go_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/init_convert_simulate_go_test.go b/test/init_convert_simulate_go_test.go index 09ad191d..f1a1b33f 100644 --- a/test/init_convert_simulate_go_test.go +++ b/test/init_convert_simulate_go_test.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "testing" @@ -65,9 +66,15 @@ func TestE2EInit_ConvertToCustomBuild_Go(t *testing.T) { mainBytes, err := os.ReadFile(mainPath) require.NoError(t, err) mainStr := string(mainBytes) - mainStr = strings.Replace(mainStr, "type ExecutionResult struct {\n\tResult string\n}", "type ExecutionResult struct {\n\tResult string\n\tFlagProof string\n}", 1) - mainStr = strings.Replace(mainStr, "\t// Your logic here...\n\n\treturn &ExecutionResult{Result: fmt.Sprintf(\"Fired at %s\", scheduledTime)}, nil", + var nl = "\n" + if runtime.GOOS == "windows" { + nl = "\r\n" + } + + mainStr = strings.Replace(mainStr, "type ExecutionResult struct {"+nl+"\tResult string"+nl+"}", "type ExecutionResult struct {"+nl+"\tResult string"+nl+"\tFlagProof string"+nl+"}", 1) + mainStr = strings.Replace(mainStr, "\t// Your logic here..."+nl+nl+"\treturn &ExecutionResult{Result: fmt.Sprintf(\"Fired at %s\", scheduledTime)}, nil", "\treturn &ExecutionResult{Result: fmt.Sprintf(\"Fired at %s\", scheduledTime), FlagProof: FlagProof}, nil", 1) + require.NoError(t, os.WriteFile(mainPath, []byte(mainStr), 0600)) constA := `//go:build customFlag From bb4ed429836e84479f5e307f8f31da7bcb37b085 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 18 Feb 2026 11:22:29 -0500 Subject: [PATCH 07/11] Changes requested by product, remove the template for init, rename the command --- cmd/creinit/creinit.go | 25 +- .../workflow/wasmBlankTemplate/Makefile.tpl | 14 -- .../workflow/wasmBlankTemplate/README.md | 33 --- .../wasmBlankTemplate/config.production.json | 1 - .../wasmBlankTemplate/config.staging.json | 1 - .../workflow/wasmBlankTemplate/secrets.yaml | 1 - .../workflow/wasmBlankTemplate/wasm/README.md | 3 - cmd/root.go | 2 +- cmd/workflow/convert/convert.go | 4 +- docs/cre_workflow.md | 15 +- test/convert_simulate_helper.go | 2 +- test/init_and_simulate_wasm_test.go | 218 ------------------ 12 files changed, 13 insertions(+), 306 deletions(-) delete mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl delete mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/README.md delete mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/config.production.json delete mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/config.staging.json delete mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/secrets.yaml delete mode 100644 cmd/creinit/template/workflow/wasmBlankTemplate/wasm/README.md delete mode 100644 test/init_and_simulate_wasm_test.go diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index e632e16e..d4d8d4c2 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -29,9 +29,8 @@ const SecretsFileName = "secrets.yaml" type TemplateLanguage string const ( - TemplateLangGo TemplateLanguage = "go" - TemplateLangTS TemplateLanguage = "typescript" - TemplateLangWasm TemplateLanguage = "wasm" + TemplateLangGo TemplateLanguage = "go" + TemplateLangTS TemplateLanguage = "typescript" ) const ( @@ -75,14 +74,6 @@ var languageTemplates = []LanguageTemplate{ {Folder: "typescriptConfHTTP", Title: "Confidential Http: Typescript example using the confidential http capability", ID: 5, Name: ConfHTTPTemplate, Hidden: true}, }, }, - { - Title: "Self-compiled WASM (advanced)", - Lang: TemplateLangWasm, - EntryPoint: "./wasm/workflow.wasm", - Workflows: []WorkflowTemplate{ - {Folder: "wasmBlankTemplate", Title: "Blank: Self-compiled WASM workflow template", ID: 6, Name: HelloWorldTemplate}, - }, - }, } type Inputs struct { @@ -342,8 +333,6 @@ func (h *handler) Execute(inputs Inputs) error { h.runtimeContext.Workflow.Language = constants.WorkflowLanguageGolang case TemplateLangTS: h.runtimeContext.Workflow.Language = constants.WorkflowLanguageTypeScript - case TemplateLangWasm: - h.runtimeContext.Workflow.Language = constants.WorkflowLanguageWasm } } @@ -396,16 +385,6 @@ func (h *handler) printSuccessMessage(projectRoot, workflowName, workflowDirecto ui.RenderStep("4. Run the workflow:") + "\n" + " " + ui.RenderDim("cre workflow simulate "+workflowName) + "\n\n" + ui.RenderStep("5. (Optional) Consult " + readmeHint + " to learn more about this template.") - case TemplateLangWasm: - steps = ui.RenderStep("1. Navigate to your project:") + "\n" + - " " + ui.RenderDim("cd "+projBase) + "\n\n" + - ui.RenderStep("2. Add your build logic to the Makefile:") + "\n" + - " " + ui.RenderDim("Edit "+workflowDirBase+"/Makefile and implement the 'build' target") + "\n\n" + - ui.RenderStep("3. Build your workflow:") + "\n" + - " " + ui.RenderDim("cd "+workflowName+" && make build") + "\n\n" + - ui.RenderStep("4. Run the workflow:") + "\n" + - " " + ui.RenderDim("cre workflow simulate "+workflowName) + "\n\n" + - ui.RenderStep("5. (Optional) Consult " + readmeHint + " to learn more about this template.") default: steps = ui.RenderStep("1. Navigate to your project:") + "\n" + " " + ui.RenderDim("cd "+projBase) + "\n\n" + diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl b/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl deleted file mode 100644 index 2ebdb2d5..00000000 --- a/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl +++ /dev/null @@ -1,14 +0,0 @@ -.PHONY: build - -# Example for Go - uncomment and modify the export lines and build command: -# export GOOS := wasip1 -# export GOARCH := wasm -# export CGO_ENABLED := 0 - -build: - # TODO: Add your build logic here - # This target should compile your workflow to wasm/workflow.wasm - # Example for Go: - # go build -o wasm/workflow.wasm . - @echo "Please implement the build target in the Makefile" - @exit 1 diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/README.md b/cmd/creinit/template/workflow/wasmBlankTemplate/README.md deleted file mode 100644 index fe2ba259..00000000 --- a/cmd/creinit/template/workflow/wasmBlankTemplate/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Self-compiled WASM Workflow Template - -This template provides a blank workflow template for self-compiled WASM workflows. It includes the necessary files for a workflow, excluding workflow code. - -## Structure - -- `Makefile`: Contains a TODO on the `build` target where you should add your build logic -- `workflow.yaml`: Workflow settings file with the wasm directory configured -- `config.staging.json` and `config.production.json`: Configuration files for different environments -- `secrets.yaml`: Secrets file (if needed) - -## Steps to use - -1. **Add your build logic**: Edit the `Makefile` and implement the `build` target. This should compile your workflow to `wasm/workflow.wasm`. - -2. **Build your workflow**: Run `make build` from the workflow directory. - -3. **Simulate the workflow**: From the project root, run: - ```bash - cre workflow simulate --target=staging-settings - ``` - -## Example Makefile build target - -```makefile -# For Go workflows: -export GOOS := wasip1 -export GOARCH := wasm -export CGO_ENABLED := 0 - -build: - go build -o wasm/workflow.wasm . -``` diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/config.production.json b/cmd/creinit/template/workflow/wasmBlankTemplate/config.production.json deleted file mode 100644 index 0967ef42..00000000 --- a/cmd/creinit/template/workflow/wasmBlankTemplate/config.production.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/config.staging.json b/cmd/creinit/template/workflow/wasmBlankTemplate/config.staging.json deleted file mode 100644 index 0967ef42..00000000 --- a/cmd/creinit/template/workflow/wasmBlankTemplate/config.staging.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/secrets.yaml b/cmd/creinit/template/workflow/wasmBlankTemplate/secrets.yaml deleted file mode 100644 index 7b85d864..00000000 --- a/cmd/creinit/template/workflow/wasmBlankTemplate/secrets.yaml +++ /dev/null @@ -1 +0,0 @@ -secretsNames: diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/wasm/README.md b/cmd/creinit/template/workflow/wasmBlankTemplate/wasm/README.md deleted file mode 100644 index 5a3491cd..00000000 --- a/cmd/creinit/template/workflow/wasmBlankTemplate/wasm/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# WASM Directory - -This directory should contain your compiled WASM file (`workflow.wasm`) after running `make build`. diff --git a/cmd/root.go b/cmd/root.go index 6d0a3c75..aac07ba4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -378,7 +378,7 @@ func isLoadSettings(cmd *cobra.Command) bool { "cre help": {}, "cre update": {}, "cre workflow": {}, - "cre workflow convert-to-custom-build": {}, + "cre workflow custom-build": {}, "cre account": {}, "cre secrets": {}, "cre": {}, diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go index 44f937a9..92a370d0 100644 --- a/cmd/workflow/convert/convert.go +++ b/cmd/workflow/convert/convert.go @@ -29,11 +29,11 @@ type Inputs struct { func New(runtimeContext *runtime.Context) *cobra.Command { var force bool convertCmd := &cobra.Command{ - Use: "convert-to-custom-build ", + Use: "custom-build ", Short: "Converts an existing workflow to a custom (self-compiled) build", Long: `Converts a Go or TypeScript workflow to use a custom build via Makefile, producing wasm/workflow.wasm. The workflow-path in workflow.yaml is updated to ./wasm/workflow.wasm. This cannot be undone.`, Args: cobra.ExactArgs(1), - Example: `cre workflow convert-to-custom-build ./my-workflow`, + Example: `cre workflow custom-build ./my-workflow`, RunE: func(cmd *cobra.Command, args []string) error { handler := newHandler(runtimeContext) inputs := Inputs{ diff --git a/docs/cre_workflow.md b/docs/cre_workflow.md index 9f1bd57c..30f4e96e 100644 --- a/docs/cre_workflow.md +++ b/docs/cre_workflow.md @@ -27,11 +27,10 @@ cre workflow [optional flags] ### SEE ALSO -* [cre](cre.md) - CRE CLI tool -* [cre workflow activate](cre_workflow_activate.md) - Activates workflow on the Workflow Registry contract -* [cre workflow convert-to-custom-build](cre_workflow_convert-to-custom-build.md) - Converts an existing workflow to a custom (self-compiled) build -* [cre workflow delete](cre_workflow_delete.md) - Deletes all versions of a workflow from the Workflow Registry -* [cre workflow deploy](cre_workflow_deploy.md) - Deploys a workflow to the Workflow Registry contract -* [cre workflow pause](cre_workflow_pause.md) - Pauses workflow on the Workflow Registry contract -* [cre workflow simulate](cre_workflow_simulate.md) - Simulates a workflow - +- [cre](cre.md) - CRE CLI tool +- [cre workflow activate](cre_workflow_activate.md) - Activates workflow on the Workflow Registry contract +- [cre workflow custom-build](cre_workflow_custom-build.md) - Converts an existing workflow to a custom (self-compiled) build +- [cre workflow delete](cre_workflow_delete.md) - Deletes all versions of a workflow from the Workflow Registry +- [cre workflow deploy](cre_workflow_deploy.md) - Deploys a workflow to the Workflow Registry contract +- [cre workflow pause](cre_workflow_pause.md) - Pauses workflow on the Workflow Registry contract +- [cre workflow simulate](cre_workflow_simulate.md) - Simulates a workflow diff --git a/test/convert_simulate_helper.go b/test/convert_simulate_helper.go index f7b3d02d..cfda81f2 100644 --- a/test/convert_simulate_helper.go +++ b/test/convert_simulate_helper.go @@ -55,7 +55,7 @@ func ConvertSimulateBeforeAfter(t *testing.T, projectRoot, workflowDir, workflow func convertRunConvert(t *testing.T, projectRoot, workflowDir string) { t.Helper() var stdout, stderr bytes.Buffer - cmd := exec.Command(CLIPath, "workflow", "convert-to-custom-build", workflowDir, "-f") + cmd := exec.Command(CLIPath, "workflow", "custom-build", workflowDir, "-f") cmd.Dir = projectRoot cmd.Stdout = &stdout cmd.Stderr = &stderr diff --git a/test/init_and_simulate_wasm_test.go b/test/init_and_simulate_wasm_test.go deleted file mode 100644 index 4810f1a9..00000000 --- a/test/init_and_simulate_wasm_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package test - -import ( - "bytes" - "os" - "os/exec" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/cre-cli/internal/constants" - "github.com/smartcontractkit/cre-cli/internal/credentials" - "github.com/smartcontractkit/cre-cli/internal/settings" -) - -func TestE2EInit_WasmBlankTemplate(t *testing.T) { - tempDir := t.TempDir() - projectName := "e2e-init-wasm-test" - workflowName := "wasmWorkflow" - templateID := "6" - projectRoot := filepath.Join(tempDir, projectName) - workflowDirectory := filepath.Join(projectRoot, workflowName) - - ethKey := "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - t.Setenv(settings.EthPrivateKeyEnvVar, ethKey) - - // Set dummy API key - t.Setenv(credentials.CreApiKeyVar, "test-api") - - gqlSrv := NewGraphQLMockServerGetOrganization(t) - defer gqlSrv.Close() - - // --- cre init with WASM template --- - initArgs := []string{ - "init", - "--project-root", tempDir, - "--project-name", projectName, - "--template-id", templateID, - "--workflow-name", workflowName, - } - var stdout, stderr bytes.Buffer - initCmd := exec.Command(CLIPath, initArgs...) - initCmd.Dir = tempDir - initCmd.Stdout = &stdout - initCmd.Stderr = &stderr - - require.NoError( - t, - initCmd.Run(), - "cre init failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - require.FileExists(t, filepath.Join(projectRoot, constants.DefaultProjectSettingsFileName)) - require.FileExists(t, filepath.Join(projectRoot, constants.DefaultEnvFileName)) - require.DirExists(t, workflowDirectory) - - expectedFiles := []string{"README.md", "Makefile", "workflow.yaml", "config.staging.json", "config.production.json", "secrets.yaml"} - for _, f := range expectedFiles { - require.FileExists(t, filepath.Join(workflowDirectory, f), "missing workflow file %q", f) - } - - // Create wasm directory - wasmDir := filepath.Join(workflowDirectory, "wasm") - require.NoError(t, os.MkdirAll(wasmDir, 0755)) - - // Create a simple Go workflow file similar to blankTemplate but with custom build tag - mainGoContent := `//go:build wasip1 && customwasm - -package main - -import ( - "fmt" - "log/slog" - - "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" - "github.com/smartcontractkit/cre-sdk-go/cre" - "github.com/smartcontractkit/cre-sdk-go/cre/wasm" -) - -type ExecutionResult struct { - Result string -} - -type Config struct{} - -func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) { - cronTrigger := cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}) - return cre.Workflow[*Config]{ - cre.Handler(cronTrigger, onCronTrigger), - }, nil -} - -func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (*ExecutionResult, error) { - logger := runtime.Logger() - scheduledTime := trigger.ScheduledExecutionTime.AsTime() - logger.Info("Cron trigger fired", "scheduledTime", scheduledTime) - return &ExecutionResult{Result: fmt.Sprintf("Fired at %s", scheduledTime)}, nil -} - -func main() { - wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow) -} -` - mainGoPath := filepath.Join(workflowDirectory, "main.go") - require.NoError(t, os.WriteFile(mainGoPath, []byte(mainGoContent), 0600)) - - // Create go.mod file - will be updated by go mod tidy - goModContent := `module wasm-workflow - -go 1.25.3 - -require ( - github.com/smartcontractkit/cre-sdk-go v1.1.3 - github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.0.0-beta.0 -) -` - goModPath := filepath.Join(workflowDirectory, "go.mod") - require.NoError(t, os.WriteFile(goModPath, []byte(goModContent), 0600)) - - // Update Makefile to include build command with custom build tag - makefilePath := filepath.Join(workflowDirectory, "Makefile") - makefileContent := `.PHONY: build - -export GOOS := wasip1 -export GOARCH := wasm - -build: - go build -tags customwasm -o wasm/workflow.wasm . -` - require.NoError(t, os.WriteFile(makefilePath, []byte(makefileContent), 0600)) - - // Run go mod tidy to resolve dependencies - stdout.Reset() - stderr.Reset() - tidyCmd := exec.Command("go", "mod", "tidy") - tidyCmd.Dir = workflowDirectory - tidyCmd.Stdout = &stdout - tidyCmd.Stderr = &stderr - - require.NoError( - t, - tidyCmd.Run(), - "go mod tidy failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - // Build the workflow using make build - stdout.Reset() - stderr.Reset() - buildCmd := exec.Command("make", "build") - buildCmd.Dir = workflowDirectory - buildCmd.Stdout = &stdout - buildCmd.Stderr = &stderr - - require.NoError( - t, - buildCmd.Run(), - "make build failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - // Verify WASM file was created - wasmFilePath := filepath.Join(wasmDir, "workflow.wasm") - require.FileExists(t, wasmFilePath, "WASM file should be created by make build") - - // --- cre workflow simulate wasmWorkflow --- - stdout.Reset() - stderr.Reset() - simulateArgs := []string{ - "workflow", "simulate", - workflowName, - "--project-root", projectRoot, - "--non-interactive", - "--trigger-index=0", - } - simulateCmd := exec.Command(CLIPath, simulateArgs...) - simulateCmd.Dir = projectRoot - simulateCmd.Stdout = &stdout - simulateCmd.Stderr = &stderr - - require.NoError( - t, - simulateCmd.Run(), - "cre workflow simulate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - // --- cre workflow compile wasmWorkflow --- - stdout.Reset() - stderr.Reset() - compileArgs := []string{ - "workflow", "compile", - filepath.Join(workflowDirectory, "workflow.yaml"), - "--project-root", projectRoot, - } - compileCmd := exec.Command(CLIPath, compileArgs...) - compileCmd.Dir = projectRoot - compileCmd.Stdout = &stdout - compileCmd.Stderr = &stderr - - require.NoError( - t, - compileCmd.Run(), - "cre workflow compile failed:\nSTDOUT:\n%s\nSTDERR:\n%s", - stdout.String(), - stderr.String(), - ) - - // Verify compiled output exists - outputPath := filepath.Join(workflowDirectory, "binary.wasm.br.b64") - require.FileExists(t, outputPath, "compiled output should exist") -} From d96bf81f8a04f1baaae9b2602446c81569652230 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 18 Feb 2026 11:58:38 -0500 Subject: [PATCH 08/11] Revert test skip --- cmd/workflow/convert/convert.go | 8 ++- cmd/workflow/convert/convert_test.go | 90 +++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go index 92a370d0..e06b5efa 100644 --- a/cmd/workflow/convert/convert.go +++ b/cmd/workflow/convert/convert.go @@ -47,13 +47,17 @@ func New(runtimeContext *runtime.Context) *cobra.Command { return convertCmd } +// confirmFn is the type for the confirmation prompt; production uses ui.Confirm (Charm). +type confirmFn func(title string, opts ...ui.ConfirmOption) (bool, error) + type handler struct { log *zerolog.Logger runtimeContext *runtime.Context + confirmFn confirmFn // always set: ui.Confirm in production, test double in tests } func newHandler(runtimeContext *runtime.Context) *handler { - h := &handler{runtimeContext: runtimeContext} + h := &handler{runtimeContext: runtimeContext, confirmFn: ui.Confirm} if runtimeContext != nil { h.log = runtimeContext.Logger } @@ -83,7 +87,7 @@ func (h *handler) Execute(inputs Inputs) error { } if !inputs.Force { - confirmed, err := ui.Confirm(convertWarning, ui.WithLabels("Yes", "No")) + confirmed, err := h.confirmFn(convertWarning, ui.WithLabels("Yes", "No")) if err != nil { return err } diff --git a/cmd/workflow/convert/convert_test.go b/cmd/workflow/convert/convert_test.go index 7d6acbfd..a5546b16 100644 --- a/cmd/workflow/convert/convert_test.go +++ b/cmd/workflow/convert/convert_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/ui" ) func TestConvert_AlreadyWasm_ReturnsError(t *testing.T) { @@ -71,15 +72,98 @@ production-settings: } func TestConvert_PromptNo_Cancels(t *testing.T) { - t.Skip("Charm ui.Confirm requires TTY; use Force in production or run test with TTY") + dir := t.TempDir() + workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) + mainGo := filepath.Join(dir, "main.go") + yamlContent := `staging-settings: + user-workflow: + workflow-name: "wf-staging" + workflow-artifacts: + workflow-path: "." + config-path: "./config.staging.json" +production-settings: + user-workflow: + workflow-name: "wf-production" + workflow-artifacts: + workflow-path: "." + config-path: "./config.production.json" +` + require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) + require.NoError(t, os.WriteFile(mainGo, []byte("package main\n"), 0600)) + + h := newHandler(nil) + h.confirmFn = func(_ string, _ ...ui.ConfirmOption) (bool, error) { return false, nil } + err := h.Execute(Inputs{WorkflowFolder: dir, Force: false}) + require.NoError(t, err) + + data, err := os.ReadFile(workflowYAML) + require.NoError(t, err) + require.Contains(t, string(data), "workflow-path: \".\"") + require.NotContains(t, string(data), wasmWorkflowPath) + require.NoFileExists(t, filepath.Join(dir, "Makefile")) } func TestConvert_PromptYes_Proceeds(t *testing.T) { - t.Skip("Charm ui.Confirm requires TTY; use Force in production or run test with TTY") + dir := t.TempDir() + workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) + mainGo := filepath.Join(dir, "main.go") + yamlContent := `staging-settings: + user-workflow: + workflow-name: "wf-staging" + workflow-artifacts: + workflow-path: "." + config-path: "./config.staging.json" +production-settings: + user-workflow: + workflow-name: "wf-production" + workflow-artifacts: + workflow-path: "." + config-path: "./config.production.json" +` + require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) + require.NoError(t, os.WriteFile(mainGo, []byte("package main\n"), 0600)) + + h := newHandler(nil) + h.confirmFn = func(_ string, _ ...ui.ConfirmOption) (bool, error) { return true, nil } + err := h.Execute(Inputs{WorkflowFolder: dir, Force: false}) + require.NoError(t, err) + + data, err := os.ReadFile(workflowYAML) + require.NoError(t, err) + require.Contains(t, string(data), wasmWorkflowPath) + require.FileExists(t, filepath.Join(dir, "Makefile")) + require.DirExists(t, filepath.Join(dir, "wasm")) } func TestConvert_PromptEmpty_DefaultsYes_Proceeds(t *testing.T) { - t.Skip("Charm ui.Confirm requires TTY; use Force in production or run test with TTY") + dir := t.TempDir() + workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) + mainGo := filepath.Join(dir, "main.go") + yamlContent := `staging-settings: + user-workflow: + workflow-name: "wf-staging" + workflow-artifacts: + workflow-path: "." + config-path: "./config.staging.json" +production-settings: + user-workflow: + workflow-name: "wf-production" + workflow-artifacts: + workflow-path: "." + config-path: "./config.production.json" +` + require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) + require.NoError(t, os.WriteFile(mainGo, []byte("package main\n"), 0600)) + + h := newHandler(nil) + h.confirmFn = func(_ string, _ ...ui.ConfirmOption) (bool, error) { return true, nil } + err := h.Execute(Inputs{WorkflowFolder: dir, Force: false}) + require.NoError(t, err) + + data, err := os.ReadFile(workflowYAML) + require.NoError(t, err) + require.Contains(t, string(data), wasmWorkflowPath) + require.FileExists(t, filepath.Join(dir, "Makefile")) } func TestConvert_TS_InstallsDepsIfNoNodeModules(t *testing.T) { From 5bc9d5931c4274b9fb1fbae2eaf7e1606baba4cc Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 18 Feb 2026 12:26:22 -0500 Subject: [PATCH 09/11] Add project root to convert --- cmd/common/utils.go | 6 ++-- cmd/common/utils_test.go | 27 ++++++++++++++ cmd/workflow/convert/convert.go | 29 +++++++++++++-- cmd/workflow/convert/convert_test.go | 54 ++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 cmd/common/utils_test.go diff --git a/cmd/common/utils.go b/cmd/common/utils.go index 470e8c0f..4649d549 100644 --- a/cmd/common/utils.go +++ b/cmd/common/utils.go @@ -200,10 +200,8 @@ func EnsureTool(bin string) error { } func WriteChangesetFile(fileName string, changesetFile *inttypes.ChangesetFile, settings *settings.Settings) error { - // Set project context to ensure we're in the correct directory for writing the changeset file - // This is needed because workflow commands set the workflow directory as the context, but path for changeset file is relative to the project root - err := context.SetProjectContext("") - if err != nil { + // Set project context so the changeset path is resolved from project root + if err := context.SetProjectContext(""); err != nil { return err } diff --git a/cmd/common/utils_test.go b/cmd/common/utils_test.go new file mode 100644 index 00000000..d739abef --- /dev/null +++ b/cmd/common/utils_test.go @@ -0,0 +1,27 @@ +package common + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +// ResolveWorkflowDir was removed; convert uses transformation.ResolveWorkflowPath (existing function). +// Project-root behavior for convert is tested in cmd/workflow/convert/convert_test.go TestConvert_ProjectRootFlag_ResolvesWorkflowDir. + +func TestResolveWorkflowPath_WorkflowDir(t *testing.T) { + // Sanity check: ResolveWorkflowPath(workflowDir, ".") returns main.go or main.ts when present + dir := t.TempDir() + mainGo := filepath.Join(dir, "main.go") + require.NoError(t, os.WriteFile(mainGo, []byte("package main\n"), 0600)) + prev, _ := os.Getwd() + require.NoError(t, os.Chdir(dir)) + t.Cleanup(func() { _ = os.Chdir(prev) }) + + absDir, _ := filepath.Abs(dir) + got, err := ResolveWorkflowPath(absDir, ".") + require.NoError(t, err) + require.Equal(t, mainGo, got) +} diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go index e06b5efa..9734630d 100644 --- a/cmd/workflow/convert/convert.go +++ b/cmd/workflow/convert/convert.go @@ -13,6 +13,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "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/ui" ) @@ -65,9 +66,31 @@ func newHandler(runtimeContext *runtime.Context) *handler { } func (h *handler) Execute(inputs Inputs) error { - workflowDir, err := filepath.Abs(inputs.WorkflowFolder) - if err != nil { - return fmt.Errorf("workflow folder path: %w", err) + projectRoot := "" + if h.runtimeContext != nil && h.runtimeContext.Viper != nil { + projectRoot = h.runtimeContext.Viper.GetString(settings.Flags.ProjectRoot.Name) + } + var workflowDir string + if projectRoot != "" { + // Use the same resolution as other workflow commands: ResolveWorkflowPath resolves relative to CWD + prevWd, err := os.Getwd() + if err != nil { + return fmt.Errorf("workflow folder path: %w", err) + } + if err := os.Chdir(projectRoot); err != nil { + return fmt.Errorf("project root path: %w", err) + } + defer func() { _ = os.Chdir(prevWd) }() + workflowDir, err = transformation.ResolveWorkflowPath(inputs.WorkflowFolder) + if err != nil { + return err + } + } else { + var err error + workflowDir, err = transformation.ResolveWorkflowPath(inputs.WorkflowFolder) + if err != nil { + return err + } } workflowYAML := filepath.Join(workflowDir, constants.DefaultWorkflowSettingsFileName) currentPath, err := settings.GetWorkflowPathFromFile(workflowYAML) diff --git a/cmd/workflow/convert/convert_test.go b/cmd/workflow/convert/convert_test.go index a5546b16..12cd9333 100644 --- a/cmd/workflow/convert/convert_test.go +++ b/cmd/workflow/convert/convert_test.go @@ -5,9 +5,13 @@ import ( "path/filepath" "testing" + "github.com/spf13/viper" "github.com/stretchr/testify/require" "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -35,6 +39,56 @@ production-settings: require.Contains(t, err.Error(), "already a custom build") } +func TestConvert_ProjectRootFlag_ResolvesWorkflowDir(t *testing.T) { + // Project layout: projectRoot/workflowName/ with workflow.yaml and main.go. + // Each subtest gets its own dir so they don't share state (second run would see "already custom build"). + makeWorkflowUnderProjectRoot := func(t *testing.T) (projectRoot, workflowDir, workflowName string) { + t.Helper() + projectRoot = t.TempDir() + workflowName = "my-wf" + workflowDir = filepath.Join(projectRoot, workflowName) + require.NoError(t, os.MkdirAll(workflowDir, 0755)) + workflowYAML := filepath.Join(workflowDir, constants.DefaultWorkflowSettingsFileName) + mainGo := filepath.Join(workflowDir, "main.go") + yamlContent := `staging-settings: + user-workflow: + workflow-name: "wf-staging" + workflow-artifacts: + workflow-path: "." + config-path: "./config.staging.json" +production-settings: + user-workflow: + workflow-name: "wf-production" + workflow-artifacts: + workflow-path: "." + config-path: "./config.production.json" +` + require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) + require.NoError(t, os.WriteFile(mainGo, []byte("package main\n"), 0600)) + return projectRoot, workflowDir, workflowName + } + + for _, flagName := range []string{"-R", "--project-root"} { + t.Helper() + projectRoot, workflowDir, workflowName := makeWorkflowUnderProjectRoot(t) + v := viper.New() + v.Set(settings.Flags.ProjectRoot.Name, projectRoot) + ctx := &runtime.Context{Viper: v, Logger: testutil.NewTestLogger()} + h := newHandler(ctx) + h.confirmFn = func(_ string, _ ...ui.ConfirmOption) (bool, error) { return true, nil } + err := h.Execute(Inputs{WorkflowFolder: workflowName, Force: false}) + require.NoError(t, err) + + workflowYAML := filepath.Join(workflowDir, constants.DefaultWorkflowSettingsFileName) + data, err := os.ReadFile(workflowYAML) + require.NoError(t, err) + require.Contains(t, string(data), wasmWorkflowPath, "flag %s: workflow.yaml should be updated", flagName) + require.FileExists(t, filepath.Join(workflowDir, "Makefile"), "flag %s: Makefile should be created in workflow dir", flagName) + require.DirExists(t, filepath.Join(workflowDir, "wasm"), "flag %s: wasm dir should exist", flagName) + + } +} + func TestConvert_Force_UpdatesYAMLAndCreatesMakefile(t *testing.T) { dir := t.TempDir() workflowYAML := filepath.Join(dir, constants.DefaultWorkflowSettingsFileName) From a80a3265df160e6cbf1c3bf6038ef551c33b96a2 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 18 Feb 2026 13:00:31 -0500 Subject: [PATCH 10/11] make gendoc --- docs/cre_workflow.md | 15 ++++++------ docs/cre_workflow_custom-build.md | 38 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 docs/cre_workflow_custom-build.md diff --git a/docs/cre_workflow.md b/docs/cre_workflow.md index 30f4e96e..0bcc74cb 100644 --- a/docs/cre_workflow.md +++ b/docs/cre_workflow.md @@ -27,10 +27,11 @@ cre workflow [optional flags] ### SEE ALSO -- [cre](cre.md) - CRE CLI tool -- [cre workflow activate](cre_workflow_activate.md) - Activates workflow on the Workflow Registry contract -- [cre workflow custom-build](cre_workflow_custom-build.md) - Converts an existing workflow to a custom (self-compiled) build -- [cre workflow delete](cre_workflow_delete.md) - Deletes all versions of a workflow from the Workflow Registry -- [cre workflow deploy](cre_workflow_deploy.md) - Deploys a workflow to the Workflow Registry contract -- [cre workflow pause](cre_workflow_pause.md) - Pauses workflow on the Workflow Registry contract -- [cre workflow simulate](cre_workflow_simulate.md) - Simulates a workflow +* [cre](cre.md) - CRE CLI tool +* [cre workflow activate](cre_workflow_activate.md) - Activates workflow on the Workflow Registry contract +* [cre workflow custom-build](cre_workflow_custom-build.md) - Converts an existing workflow to a custom (self-compiled) build +* [cre workflow delete](cre_workflow_delete.md) - Deletes all versions of a workflow from the Workflow Registry +* [cre workflow deploy](cre_workflow_deploy.md) - Deploys a workflow to the Workflow Registry contract +* [cre workflow pause](cre_workflow_pause.md) - Pauses workflow on the Workflow Registry contract +* [cre workflow simulate](cre_workflow_simulate.md) - Simulates a workflow + diff --git a/docs/cre_workflow_custom-build.md b/docs/cre_workflow_custom-build.md new file mode 100644 index 00000000..f5a1fc01 --- /dev/null +++ b/docs/cre_workflow_custom-build.md @@ -0,0 +1,38 @@ +## cre workflow custom-build + +Converts an existing workflow to a custom (self-compiled) build + +### Synopsis + +Converts a Go or TypeScript workflow to use a custom build via Makefile, producing wasm/workflow.wasm. The workflow-path in workflow.yaml is updated to ./wasm/workflow.wasm. This cannot be undone. + +``` +cre workflow custom-build [optional flags] +``` + +### Examples + +``` +cre workflow custom-build ./my-workflow +``` + +### Options + +``` + -f, --force Skip confirmation prompt and convert immediately + -h, --help help for custom-build +``` + +### Options inherited from parent commands + +``` + -e, --env string Path to .env file which contains sensitive info (default ".env") + -R, --project-root string Path to the project root + -T, --target string Use target settings from YAML config + -v, --verbose Run command in VERBOSE mode +``` + +### SEE ALSO + +* [cre workflow](cre_workflow.md) - Manages workflows + From 778def33041de90b84c137400c4f66a16643bac8 Mon Sep 17 00:00:00 2001 From: Ryan Tinianov Date: Wed, 18 Feb 2026 13:55:37 -0500 Subject: [PATCH 11/11] fix lint --- cmd/creinit/creinit.go | 4 ++-- cmd/root.go | 34 ++++++++++++++-------------- cmd/workflow/convert/convert_test.go | 3 +-- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index d4d8d4c2..df078e72 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -374,7 +374,7 @@ func (h *handler) printSuccessMessage(projectRoot, workflowName, workflowDirecto " " + ui.RenderDim("cd "+projBase) + "\n\n" + ui.RenderStep("2. Run the workflow:") + "\n" + " " + ui.RenderDim("cre workflow simulate "+workflowName) + "\n\n" + - ui.RenderStep("3. (Optional) Consult " + readmeHint + " to learn more about this template.") + ui.RenderStep("3. (Optional) Consult "+readmeHint+" to learn more about this template.") case TemplateLangTS: steps = ui.RenderStep("1. Navigate to your project:") + "\n" + " " + ui.RenderDim("cd "+projBase) + "\n\n" + @@ -384,7 +384,7 @@ func (h *handler) printSuccessMessage(projectRoot, workflowName, workflowDirecto " " + ui.RenderDim("bun install --cwd ./"+workflowName) + "\n\n" + ui.RenderStep("4. Run the workflow:") + "\n" + " " + ui.RenderDim("cre workflow simulate "+workflowName) + "\n\n" + - ui.RenderStep("5. (Optional) Consult " + readmeHint + " to learn more about this template.") + ui.RenderStep("5. (Optional) Consult "+readmeHint+" to learn more about this template.") default: steps = ui.RenderStep("1. Navigate to your project:") + "\n" + " " + ui.RenderDim("cd "+projBase) + "\n\n" + diff --git a/cmd/root.go b/cmd/root.go index aac07ba4..18a2a8d8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -364,24 +364,24 @@ func newRootCommand() *cobra.Command { func isLoadSettings(cmd *cobra.Command) bool { // It is not expected to have the settings file when running the following commands var excludedCommands = map[string]struct{}{ - "cre version": {}, - "cre login": {}, - "cre logout": {}, - "cre whoami": {}, - "cre account list-key": {}, - "cre init": {}, - "cre generate-bindings": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre help": {}, - "cre update": {}, - "cre workflow": {}, + "cre version": {}, + "cre login": {}, + "cre logout": {}, + "cre whoami": {}, + "cre account list-key": {}, + "cre init": {}, + "cre generate-bindings": {}, + "cre completion bash": {}, + "cre completion fish": {}, + "cre completion powershell": {}, + "cre completion zsh": {}, + "cre help": {}, + "cre update": {}, + "cre workflow": {}, "cre workflow custom-build": {}, - "cre account": {}, - "cre secrets": {}, - "cre": {}, + "cre account": {}, + "cre secrets": {}, + "cre": {}, } _, exists := excludedCommands[cmd.CommandPath()] diff --git a/cmd/workflow/convert/convert_test.go b/cmd/workflow/convert/convert_test.go index 12cd9333..a400749b 100644 --- a/cmd/workflow/convert/convert_test.go +++ b/cmd/workflow/convert/convert_test.go @@ -69,7 +69,6 @@ production-settings: } for _, flagName := range []string{"-R", "--project-root"} { - t.Helper() projectRoot, workflowDir, workflowName := makeWorkflowUnderProjectRoot(t) v := viper.New() v.Set(settings.Flags.ProjectRoot.Name, projectRoot) @@ -85,7 +84,7 @@ production-settings: require.Contains(t, string(data), wasmWorkflowPath, "flag %s: workflow.yaml should be updated", flagName) require.FileExists(t, filepath.Join(workflowDir, "Makefile"), "flag %s: Makefile should be created in workflow dir", flagName) require.DirExists(t, filepath.Join(workflowDir, "wasm"), "flag %s: wasm dir should exist", flagName) - + } }