diff --git a/cmd/common/utils.go b/cmd/common/utils.go index 98805798..27b5cddd 100644 --- a/cmd/common/utils.go +++ b/cmd/common/utils.go @@ -166,12 +166,15 @@ 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 } @@ -183,19 +186,23 @@ func EnsureTool(bin string) error { return nil } -// Gets a build command for either Golang or Typescript based on the filename +// Gets a build command for Golang, TypeScript, or WASM based on the workflow language func GetBuildCmd(inputFile string, outputFile string, rootFolder string) *exec.Cmd { - isTypescriptWorkflow := strings.HasSuffix(inputFile, ".ts") || strings.HasSuffix(inputFile, ".tsx") + language := GetWorkflowLanguage(inputFile) var buildCmd *exec.Cmd - if isTypescriptWorkflow { + switch language { + case constants.WorkflowLanguageTypeScript: buildCmd = exec.Command( "bun", "cre-compile", inputFile, outputFile, ) - } else { + case constants.WorkflowLanguageWasm: + // For WASM workflows, use make build + buildCmd = exec.Command("make", "build") + case constants.WorkflowLanguageGolang: // 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: @@ -211,6 +218,17 @@ func GetBuildCmd(inputFile string, outputFile string, rootFolder string) *exec.C inputFile, ) buildCmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm", "CGO_ENABLED=0") + default: + // Fallback to Go for unknown languages + 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 diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index ea8d3480..d335e609 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -30,8 +30,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 ( @@ -75,6 +76,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 { @@ -365,6 +374,8 @@ 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 } } @@ -372,7 +383,8 @@ func (h *handler) Execute(inputs Inputs) error { fmt.Println("") fmt.Println("Next steps:") - if selectedLanguageTemplate.Lang == TemplateLangGo { + switch selectedLanguageTemplate.Lang { + case TemplateLangGo: fmt.Println(" 1. Navigate to your project directory:") fmt.Printf(" cd %s\n", filepath.Base(projectRoot)) fmt.Println("") @@ -382,7 +394,7 @@ func (h *handler) Execute(inputs Inputs) error { fmt.Printf(" 3. (Optional) Consult %s to learn more about this template:\n\n", filepath.Join(filepath.Base(workflowDirectory), "README.md")) fmt.Println("") - } else { + case TemplateLangTS: fmt.Println(" 1. Navigate to your project directory:") fmt.Printf(" cd %s\n", filepath.Base(projectRoot)) fmt.Println("") @@ -398,6 +410,22 @@ func (h *handler) Execute(inputs Inputs) error { fmt.Printf(" 5. (Optional) Consult %s to learn more about this template:\n\n", filepath.Join(filepath.Base(workflowDirectory), "README.md")) fmt.Println("") + case TemplateLangWasm: + fmt.Println(" 1. Navigate to your project directory:") + fmt.Printf(" cd %s\n", filepath.Base(projectRoot)) + fmt.Println("") + fmt.Println(" 2. Add your build logic to the Makefile:") + fmt.Printf(" Edit %s/Makefile and implement the 'build' target\n", filepath.Base(workflowDirectory)) + fmt.Println("") + fmt.Println(" 3. Build your workflow:") + fmt.Printf(" cd %s && make build\n", filepath.Base(workflowDirectory)) + fmt.Println("") + fmt.Println(" 4. Run the workflow on your machine:") + fmt.Printf(" cre workflow simulate %s\n", workflowName) + fmt.Println("") + fmt.Printf(" 5. (Optional) Consult %s to learn more about this template:\n\n", + filepath.Join(filepath.Base(workflowDirectory), "README.md")) + fmt.Println("") } return nil } diff --git a/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl b/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl new file mode 100644 index 00000000..f7973159 --- /dev/null +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/Makefile.tpl @@ -0,0 +1,12 @@ +.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 . + # Example for Rust: + # cargo build --target wasm32-wasi --release + # cp target/wasm32-wasi/release/workflow.wasm 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..a124c20e --- /dev/null +++ b/cmd/creinit/template/workflow/wasmBlankTemplate/README.md @@ -0,0 +1,35 @@ +# 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 . + # Example for Rust: + # cargo build --target wasm32-wasi --release + # cp target/wasm32-wasi/release/workflow.wasm 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/workflow/deploy/compile.go b/cmd/workflow/deploy/compile.go index 51387569..1f597bee 100644 --- a/cmd/workflow/deploy/compile.go +++ b/cmd/workflow/deploy/compile.go @@ -44,8 +44,6 @@ func (h *handler) Compile() error { } 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 @@ -61,31 +59,73 @@ func (h *handler) Compile() error { 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") } + case constants.WorkflowLanguageWasm: + if err := cmdcommon.EnsureTool("make"); err != nil { + return errors.New("make is required for WASM workflows but was not found in PATH") + } 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") + var wasmFile []byte - buildOutput, err := buildCmd.CombinedOutput() - if err != nil { - fmt.Println(string(buildOutput)) + // For WASM workflows, if the path already points to a .wasm file, use it directly + if h.runtimeContext != nil && h.runtimeContext.Workflow.Language == constants.WorkflowLanguageWasm { + if strings.HasSuffix(h.inputs.WorkflowPath, ".wasm") { + // Use the WASM file directly + wasmFile, err = os.ReadFile(h.inputs.WorkflowPath) + if err != nil { + return fmt.Errorf("failed to read WASM file: %w", err) + } + } else { + // Build the workflow using make build + tmpWasmFileName := "tmp.wasm" + buildCmd := cmdcommon.GetBuildCmd(workflowMainFile, tmpWasmFileName, workflowRootFolder) + h.log.Debug(). + Str("Workflow directory", buildCmd.Dir). + Str("Command", buildCmd.String()). + Msg("Executing make build command") + + buildOutput, err := buildCmd.CombinedOutput() + if err != nil { + fmt.Println(string(buildOutput)) + out := strings.TrimSpace(string(buildOutput)) + return fmt.Errorf("failed to build workflow: %w\nbuild output:\n%s", err, out) + } + h.log.Debug().Msgf("Build output: %s", buildOutput) + fmt.Println("Workflow compiled successfully") - out := strings.TrimSpace(string(buildOutput)) - return fmt.Errorf("failed to compile workflow: %w\nbuild output:\n%s", err, out) - } - h.log.Debug().Msgf("Build output: %s", buildOutput) - fmt.Println("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) + } + } + } else { + // For Go and TypeScript workflows, compile as before + tmpWasmFileName := "tmp.wasm" + buildCmd := cmdcommon.GetBuildCmd(workflowMainFile, tmpWasmFileName, workflowRootFolder) + h.log.Debug(). + Str("Workflow directory", buildCmd.Dir). + Str("Command", buildCmd.String()). + Msg("Executing build command") + + buildOutput, err := buildCmd.CombinedOutput() + if err != nil { + fmt.Println(string(buildOutput)) + + out := strings.TrimSpace(string(buildOutput)) + return fmt.Errorf("failed to compile workflow: %w\nbuild output:\n%s", err, out) + } + h.log.Debug().Msgf("Build output: %s", buildOutput) + fmt.Println("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) + 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) @@ -99,8 +139,13 @@ 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) + // Only remove tmp file if we created one (not for direct WASM file usage) + // Check if we used a tmp file (i.e., not a direct .wasm file and not WASM language that used direct file) + if !strings.HasSuffix(h.inputs.WorkflowPath, ".wasm") { + tmpWasmLocation := filepath.Join(workflowRootFolder, "tmp.wasm") + if err = os.Remove(tmpWasmLocation); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove the temporary file: %w", err) + } } return nil diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 1623235d..7a706c88 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -194,10 +194,7 @@ 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 @@ -213,33 +210,78 @@ func (h *handler) Execute(inputs Inputs) error { 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") } + case constants.WorkflowLanguageWasm: + if err := cmdcommon.EnsureTool("make"); err != nil { + return errors.New("make is required for WASM workflows but was not found in PATH") + } default: return fmt.Errorf("unsupported workflow language for file %s", workflowMainFile) } } - buildCmd := cmdcommon.GetBuildCmd(workflowMainFile, tmpWasmFileName, workflowRootFolder) + var wasmFileBinary []byte + var err error - h.log.Debug(). - Str("Workflow directory", buildCmd.Dir). - Str("Command", buildCmd.String()). - Msg("Executing go build command") + // For WASM workflows, if the path already points to a .wasm file, use it directly + if h.runtimeContext != nil && h.runtimeContext.Workflow.Language == constants.WorkflowLanguageWasm { + if strings.HasSuffix(inputs.WorkflowPath, ".wasm") { + // Use the WASM file directly + wasmFileBinary, err = os.ReadFile(inputs.WorkflowPath) + if err != nil { + return fmt.Errorf("failed to read WASM file: %w", err) + } + } else { + // Build the workflow using make build + tmpWasmFileName := "tmp.wasm" + buildCmd := cmdcommon.GetBuildCmd(workflowMainFile, tmpWasmFileName, workflowRootFolder) - // Execute the build command - buildOutput, err := buildCmd.CombinedOutput() - 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) - } - h.log.Debug().Msgf("Build output: %s", buildOutput) - fmt.Println("Workflow compiled") + h.log.Debug(). + Str("Workflow directory", buildCmd.Dir). + Str("Command", buildCmd.String()). + Msg("Executing make build command") - // 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) + buildOutput, err := buildCmd.CombinedOutput() + if err != nil { + out := strings.TrimSpace(string(buildOutput)) + h.log.Info().Msg(out) + return fmt.Errorf("failed to build workflow: %w\nbuild output:\n%s", err, out) + } + h.log.Debug().Msgf("Build output: %s", buildOutput) + fmt.Println("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) + } + } + } else { + // For Go and TypeScript workflows, compile as before + tmpWasmFileName := "tmp.wasm" + buildCmd := cmdcommon.GetBuildCmd(workflowMainFile, tmpWasmFileName, workflowRootFolder) + + h.log.Debug(). + Str("Workflow directory", buildCmd.Dir). + Str("Command", buildCmd.String()). + Msg("Executing build command") + + // Execute the build command + buildOutput, err := buildCmd.CombinedOutput() + 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) + } + h.log.Debug().Msgf("Build output: %s", buildOutput) + fmt.Println("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 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/test/init_and_simulate_wasm_test.go b/test/init_and_simulate_wasm_test.go new file mode 100644 index 00000000..11b4683a --- /dev/null +++ b/test/init_and_simulate_wasm_test.go @@ -0,0 +1,252 @@ +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" +) + +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") + + // 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"}}, + }) + } + })) + defer gqlSrv.Close() + + // Point GraphQL client to mock server + t.Setenv(environments.EnvVarGraphQLURL, gqlSrv.URL+"/graphql") + + // --- 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") +}