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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions cmd/workflow/deploy/artifacts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cre-cli/internal/artifacts"
"github.com/smartcontractkit/cre-cli/internal/credentials"
"github.com/smartcontractkit/cre-cli/internal/testutil/chainsim"
)
Expand Down Expand Up @@ -91,7 +92,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) {
h.environmentSet.GraphQLURL = "http://graphql.endpoint"

// Success case : uploading binary and config data
h.workflowArtifact = &workflowArtifact{
h.workflowArtifact = &artifacts.Artifact{
BinaryData: []byte("binarydata"),
ConfigData: []byte("configdata"),
WorkflowID: "workflow-id",
Expand All @@ -102,7 +103,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) {
require.Equal(t, "http://origin/get", *h.inputs.ConfigURL)

// Success: empty ConfigData
h.workflowArtifact = &workflowArtifact{
h.workflowArtifact = &artifacts.Artifact{
BinaryData: []byte("binarydata"),
ConfigData: nil,
WorkflowID: "workflow-id",
Expand All @@ -116,7 +117,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) {
require.ErrorContains(t, err, "workflowArtifact is nil")

// Error: empty BinaryData
h.workflowArtifact = &workflowArtifact{
h.workflowArtifact = &artifacts.Artifact{
BinaryData: nil,
ConfigData: []byte("configdata"),
WorkflowID: "workflow-id",
Expand All @@ -125,7 +126,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) {
require.ErrorContains(t, err, "uploading binary artifact: content is empty for artifactType BINARY")

// Error: workflowID is empty
h.workflowArtifact = &workflowArtifact{
h.workflowArtifact = &artifacts.Artifact{
BinaryData: []byte("binarydata"),
ConfigData: []byte("configdata"),
WorkflowID: "",
Expand Down Expand Up @@ -166,7 +167,7 @@ func TestUploadArtifactToStorageService_OriginError(t *testing.T) {
// Patch settings to use mock GraphQL endpoint
h.environmentSet.GraphQLURL = "http://graphql.endpoint"

h.workflowArtifact = &workflowArtifact{
h.workflowArtifact = &artifacts.Artifact{
BinaryData: []byte("binarydata"),
ConfigData: []byte("configdata"),
WorkflowID: "workflow-id",
Expand Down Expand Up @@ -232,7 +233,7 @@ func TestUploadArtifactToStorageService_AlreadyExistsError(t *testing.T) {
// Patch settings to use mock GraphQL endpoint
h.environmentSet.GraphQLURL = "http://graphql.endpoint"

h.workflowArtifact = &workflowArtifact{
h.workflowArtifact = &artifacts.Artifact{
BinaryData: []byte("binarydata"),
ConfigData: []byte("configdata"),
WorkflowID: "workflow-id",
Expand Down
130 changes: 9 additions & 121 deletions cmd/workflow/deploy/compile.go
Original file line number Diff line number Diff line change
@@ -1,137 +1,25 @@
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/build"
)

func (h *handler) Compile() error {
if !h.validated {
return fmt.Errorf("handler h.inputs not validated")
}
fmt.Println("Compiling workflow...")

if h.inputs.OutputPath == "" {
h.inputs.OutputPath = defaultOutputPath
}
if !strings.HasSuffix(h.inputs.OutputPath, ".b64") {
if !strings.HasSuffix(h.inputs.OutputPath, ".br") {
if !strings.HasSuffix(h.inputs.OutputPath, ".wasm") {
h.inputs.OutputPath += ".wasm" // Append ".wasm" if it doesn't already end with ".wasm"
}
h.inputs.OutputPath += ".br" // Append ".br" if it doesn't already end with ".br"
}
h.inputs.OutputPath += ".b64" // Append ".b64" if it doesn't already end with ".b64"
outputPath := h.inputs.OutputPath
if outputPath == "" {
outputPath = defaultOutputPath
}

workflowAbsFile, err := filepath.Abs(h.inputs.WorkflowPath)
buildParams, err := build.ResolveBuildParamsForWorkflow(h.inputs.WorkflowPath, outputPath)
if err != nil {
return fmt.Errorf("failed to get absolute path for the workflow file: %w", err)
}

if _, err := os.Stat(workflowAbsFile); os.IsNotExist(err) {
return fmt.Errorf("workflow file not found: %s", workflowAbsFile)
}

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)
}
return fmt.Errorf("failed to resolve build inputs: %w", err)
}
h.runtimeContext.Workflow.Language = buildParams.WorkflowLanguage

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()
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)
}

compressedFile, err := applyBrotliCompressionV2(&wasmFile)
if err != nil {
return fmt.Errorf("failed to compress WASM binary: %w", err)
}
h.log.Debug().Msg("WASM binary compressed")

if err = encodeToBase64AndSaveToFile(&compressedFile, h.inputs.OutputPath); err != nil {
return fmt.Errorf("failed to base64 encode the WASM binary: %w", err)
}
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
}

func applyBrotliCompressionV2(wasmContent *[]byte) ([]byte, error) {
var buffer bytes.Buffer

// Compress using Brotli with default options
writer := brotli.NewWriter(&buffer)

_, err := writer.Write(*wasmContent)
if err != nil {
return nil, err
}

// must close it to flush the writer and ensure all data is stored to the buffer
err = writer.Close()
if err != nil {
return nil, err
}

return buffer.Bytes(), nil
}

func encodeToBase64AndSaveToFile(input *[]byte, outputFile string) error {
encoded := base64.StdEncoding.EncodeToString(*input)

err := os.WriteFile(outputFile, []byte(encoded), 0666) //nolint:gosec
if err != nil {
return err
if err := h.builder.CompileAndSave(buildParams); err != nil {
return fmt.Errorf("failed to compile workflow: %w", err)
}

return nil
Expand Down
20 changes: 15 additions & 5 deletions cmd/workflow/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/spf13/viper"

"github.com/smartcontractkit/cre-cli/cmd/client"
"github.com/smartcontractkit/cre-cli/internal/artifacts"
"github.com/smartcontractkit/cre-cli/internal/build"
"github.com/smartcontractkit/cre-cli/internal/constants"
"github.com/smartcontractkit/cre-cli/internal/credentials"
"github.com/smartcontractkit/cre-cli/internal/environments"
Expand Down Expand Up @@ -59,17 +61,19 @@ type handler struct {
stdin io.Reader
credentials *credentials.Credentials
environmentSet *environments.EnvironmentSet
workflowArtifact *workflowArtifact
workflowArtifact *artifacts.Artifact
wrc *client.WorkflowRegistryV2Client
runtimeContext *runtime.Context
builder *build.Builder
artifactBuilder *artifacts.Builder

validated bool

wg sync.WaitGroup
wrcErr error
}

var defaultOutputPath = "./binary.wasm.br.b64"
const defaultOutputPath = "./binary.wasm.br.b64"

func New(runtimeContext *runtime.Context) *cobra.Command {
var deployCmd = &cobra.Command{
Expand Down Expand Up @@ -111,7 +115,9 @@ func newHandler(ctx *runtime.Context, stdin io.Reader) *handler {
stdin: stdin,
credentials: ctx.Credentials,
environmentSet: ctx.EnvironmentSet,
workflowArtifact: &workflowArtifact{},
workflowArtifact: &artifacts.Artifact{},
builder: build.NewBuilder(ctx.Logger),
artifactBuilder: artifacts.NewBuilder(ctx.Logger),
wrc: nil,
runtimeContext: ctx,
validated: false,
Expand Down Expand Up @@ -176,13 +182,17 @@ func (h *handler) ValidateInputs() error {
func (h *handler) Execute() error {
h.displayWorkflowDetails()

if !h.validated {
return errors.New("inputs have not been validated")
}

if err := h.Compile(); err != nil {
return fmt.Errorf("failed to compile workflow: %w", err)
return err
}

if err := h.PrepareWorkflowArtifact(); err != nil {
return fmt.Errorf("failed to prepare workflow artifact: %w", err)
}

h.runtimeContext.Workflow.ID = h.workflowArtifact.WorkflowID

h.wg.Wait()
Expand Down
69 changes: 9 additions & 60 deletions cmd/workflow/deploy/prepare.go
Original file line number Diff line number Diff line change
@@ -1,72 +1,21 @@
package deploy

import (
"encoding/base64"
"fmt"
"os"

workflowUtils "github.com/smartcontractkit/chainlink-common/pkg/workflows"
"github.com/smartcontractkit/cre-cli/internal/artifacts"
)

type workflowArtifact struct {
BinaryData []byte
ConfigData []byte
WorkflowID string
}

func (h *handler) prepareWorkflowBinary() ([]byte, error) {
h.log.Debug().Str("Binary Path", h.inputs.OutputPath).Msg("Fetching workflow binary")
binaryData, err := os.ReadFile(h.inputs.OutputPath)
if err != nil {
h.log.Error().Err(err).Str("path", h.inputs.OutputPath).Msg("Failed to read output file")
return nil, err
}
h.log.Debug().Msg("Workflow binary WASM is ready")
return binaryData, nil
}

func (h *handler) prepareWorkflowConfig() ([]byte, error) {
h.log.Debug().Str("Config Path", h.inputs.ConfigPath).Msg("Fetching workflow config")
var configData []byte
var err error
if h.inputs.ConfigPath != "" {
configData, err = os.ReadFile(h.inputs.ConfigPath)
if err != nil {
h.log.Error().Err(err).Str("path", h.inputs.ConfigPath).Msg("Failed to read config file")
return nil, err
}
}
h.log.Debug().Msg("Workflow config is ready")
return configData, nil
}

func (h *handler) PrepareWorkflowArtifact() error {
var err error
binaryData, err := h.prepareWorkflowBinary()
func (h *handler) PrepareWorkflowArtifact() (err error) {
h.workflowArtifact, err = h.artifactBuilder.Build(artifacts.Inputs{
WorkflowOwner: h.inputs.WorkflowOwner,
WorkflowName: h.inputs.WorkflowName,
OutputPath: h.inputs.OutputPath,
ConfigPath: h.inputs.ConfigPath,
})
if err != nil {
return err
}

configData, err := h.prepareWorkflowConfig()
if err != nil {
return err
}

// Note: the binary data read from file is base64 encoded, so we need to decode it before generating the workflow ID.
// This matches the behavior in the Chainlink node. Ref https://github.com/smartcontractkit/chainlink/blob/a4adc900d98d4e6eec0a6f80fcf86d883a8f1e3c/core/services/workflows/artifacts/v2/store.go#L211-L213
binaryDataDecoded, err := base64.StdEncoding.DecodeString(string(binaryData))
if err != nil {
return fmt.Errorf("failed to decode base64 binary data: %w", err)
}

workflowID, err := workflowUtils.GenerateWorkflowIDFromStrings(h.inputs.WorkflowOwner, h.inputs.WorkflowName, binaryDataDecoded, configData, "")
if err != nil {
return fmt.Errorf("failed to generate workflow ID: %w", err)
}

h.workflowArtifact.BinaryData = binaryData
h.workflowArtifact.ConfigData = configData
h.workflowArtifact.WorkflowID = workflowID
h.log.Info().Str("workflowID", h.workflowArtifact.WorkflowID).Msg("Prepared workflow artifact")

return nil
}
3 changes: 2 additions & 1 deletion cmd/workflow/deploy/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cre-cli/internal/artifacts"
"github.com/smartcontractkit/cre-cli/internal/testutil/chainsim"
)

Expand Down Expand Up @@ -55,7 +56,7 @@ func TestWorkflowUpsert(t *testing.T) {
err = handler.ValidateInputs()
require.NoError(t, err)

wfArt := workflowArtifact{
wfArt := artifacts.Artifact{
BinaryData: []byte("0x1234"),
ConfigData: []byte("config"),
WorkflowID: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
Expand Down
Loading
Loading