From 7af58a382fb1e854a6de2cc70cf13a4a95bb319d Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 12 May 2026 10:16:35 -0600 Subject: [PATCH 1/2] feat: add support for multiple api key to deploy workflow --- cre/changesets/workflow_deploy_test.go | 22 ++++++++ cre/operations/workflow_deploy.go | 23 +++++++-- cre/operations/workflow_deploy_test.go | 71 ++++++++++++++++++++++++++ go.mod | 4 +- go.sum | 4 +- 5 files changed, 117 insertions(+), 7 deletions(-) diff --git a/cre/changesets/workflow_deploy_test.go b/cre/changesets/workflow_deploy_test.go index 504b721..3c1e982 100644 --- a/cre/changesets/workflow_deploy_test.go +++ b/cre/changesets/workflow_deploy_test.go @@ -200,6 +200,28 @@ func TestCREWorkflowDeployChangeset_Apply(t *testing.T) { require.NoError(t, err) require.Len(t, out.Reports, 1) }) + + t.Run("APIKeyName propagates through changeset to selected CLI", func(t *testing.T) { //nolint:paralleltest + inner := cremocks.NewMockCLIRunner(t) + inner.EXPECT().ContextRegistries().Return([]fcre.ContextRegistryEntry{ + {ID: "private", Type: "off-chain"}, + }).Once() + inner.EXPECT(). + Run(mock.Anything, (map[string]string)(nil), matchCLIArgs("workflow", "deploy")). + Return(&fcre.CallResult{ExitCode: 0, Stdout: []byte("ok")}, nil). + Once() + + outer := cremocks.NewMockCLIRunner(t) + outer.EXPECT().WithNamedAPIKey("prod-1").Return(inner, nil).Once() + + env := newTestEnv(t, testenv.WithCRERunner(fcre.NewRunner(fcre.WithCLI(outer)))) + + namedInput := input + namedInput.APIKeyName = "prod-1" + out, err := cs.Apply(*env, namedInput) + require.NoError(t, err) + require.Len(t, out.Reports, 1) + }) } func matchCLIArgs(wantArgs ...string) any { diff --git a/cre/operations/workflow_deploy.go b/cre/operations/workflow_deploy.go index b445dd6..409c626 100644 --- a/cre/operations/workflow_deploy.go +++ b/cre/operations/workflow_deploy.go @@ -59,6 +59,10 @@ type CREWorkflowDeployInput struct { // Optional - TargetName is the CRE CLI target key that must match a top-level key // in project.yaml. Defaults to CREDeployTargetName ("cld-deploy") when empty. TargetName string `json:"targetName,omitempty" yaml:"targetName,omitempty"` + // Optional - APIKeyName selects which CRE API key to use when the runner is + // configured with multiple named keys (e.g. CRE_API_KEY={"prod":"...","stg":"..."}). + // Leave empty when the runner is configured with a single key. + APIKeyName string `json:"apiKeyName,omitempty" yaml:"apiKeyName,omitempty"` } // resolveTargetName returns the user-specified target name, falling back to [CREDeployTargetName]. @@ -72,9 +76,13 @@ func (in CREWorkflowDeployInput) resolveTargetName() string { } // CREWorkflowDeployOp deploys a workflow via the CRE CLI (single side effect: CLI invocation). +// +// Version history: +// - 1.1.0: input gained APIKeyName for selecting among named CRE API keys. +// - 1.0.0: initial release. var CREWorkflowDeployOp = fwops.NewOperation( "cre-workflow-deploy", - semver.MustParse("1.0.0"), + semver.MustParse("1.1.0"), "Deploys a CRE workflow via the CRE CLI subprocess", func(b fwops.Bundle, deps CREDeployDeps, input CREWorkflowDeployInput) (CREWorkflowDeployOutput, error) { ctx := b.GetContext() @@ -82,6 +90,15 @@ var CREWorkflowDeployOp = fwops.NewOperation( return CREWorkflowDeployOutput{}, errors.New("cre CLIRunner is nil") } + cli := deps.CLI + if input.APIKeyName != "" { + selected, err := deps.CLI.WithNamedAPIKey(input.APIKeyName) + if err != nil { + return CREWorkflowDeployOutput{}, fmt.Errorf("select cre api key %q: %w", input.APIKeyName, err) + } + cli = selected + } + workDir, err := os.MkdirTemp("", "cre-workflow-artifacts-*") if err != nil { return CREWorkflowDeployOutput{}, fmt.Errorf("mkdir temp workflow artifacts: %w", err) @@ -136,7 +153,7 @@ var CREWorkflowDeployOp = fwops.NewOperation( return CREWorkflowDeployOutput{}, fmt.Errorf("write workflow.yaml: %w", err) } - ctxCfg, err := crecli.BuildContextConfig(input.DonFamily, input.Context, deps.CRECfg, deps.CLI.ContextRegistries()) + ctxCfg, err := crecli.BuildContextConfig(input.DonFamily, input.Context, deps.CRECfg, cli.ContextRegistries()) if err != nil { return CREWorkflowDeployOutput{}, err } @@ -163,7 +180,7 @@ var CREWorkflowDeployOp = fwops.NewOperation( "CRE_ETH_PRIVATE_KEY": deps.EVMDeployerKey, } } - res, runErr := deps.CLI.Run(ctx, runEnv, args...) + res, runErr := cli.Run(ctx, runEnv, args...) if runErr != nil { var exitErr *fcre.ExitError if errors.As(runErr, &exitErr) { diff --git a/cre/operations/workflow_deploy_test.go b/cre/operations/workflow_deploy_test.go index 51b03bd..dc47b55 100644 --- a/cre/operations/workflow_deploy_test.go +++ b/cre/operations/workflow_deploy_test.go @@ -2,6 +2,7 @@ package operations import ( "context" + "errors" "os" "path/filepath" "testing" @@ -165,6 +166,76 @@ func TestCREWorkflowDeployOp(t *testing.T) { require.Equal(t, "err", out.Output.Stderr) }, }, + { + name: "APIKeyName selects cli before run", + input: func(t *testing.T) CREWorkflowDeployInput { + t.Helper() + + return CREWorkflowDeployInput{ + WorkflowBundle: creartifacts.WorkflowBundle{ + WorkflowName: "wf", + Binary: creartifacts.NewBinarySourceLocal(writeFile(t, "x.wasm", []byte("wasm"))), + Config: creartifacts.NewConfigSourceLocal(writeFile(t, "cfg.json", []byte(`{}`))), + DonFamily: "feeds-zone-a", + DeploymentRegistry: "private", + }, + Project: creartifacts.NewConfigSourceLocal(writeFile(t, "project.yaml", []byte("cld-deploy: {}\n"))), + APIKeyName: "prod-1", + } + }, + setupCLI: func(t *testing.T) *cremocks.MockCLIRunner { + t.Helper() + // The outer mock receives WithNamedAPIKey; the inner mock receives + // the deploy traffic. Strict mockery assertions in NewMockCLIRunner + // guarantee the outer mock's Run/ContextRegistries are never called. + inner := cremocks.NewMockCLIRunner(t) + inner.EXPECT().ContextRegistries().Return(testRegistries()).Once() + inner.EXPECT().Run(mock.Anything, mock.Anything, matchCLIArgs("workflow", "deploy")).Return( + &fcre.CallResult{ExitCode: 0, Stdout: []byte("ok")}, nil, + ).Once() + + outer := cremocks.NewMockCLIRunner(t) + outer.EXPECT().WithNamedAPIKey("prod-1").Return(inner, nil).Once() + + return outer + }, + assert: func(t *testing.T, _ fwops.Report[CREWorkflowDeployInput, CREWorkflowDeployOutput], err error) { + t.Helper() + require.NoError(t, err) + }, + }, + { + name: "unknown APIKeyName short-circuits before CLI work", + input: func(t *testing.T) CREWorkflowDeployInput { + t.Helper() + + return CREWorkflowDeployInput{ + WorkflowBundle: creartifacts.WorkflowBundle{ + WorkflowName: "wf", + Binary: creartifacts.NewBinarySourceLocal(writeFile(t, "x.wasm", []byte("wasm"))), + Config: creartifacts.NewConfigSourceLocal(writeFile(t, "cfg.json", []byte(`{}`))), + DonFamily: "feeds-zone-a", + DeploymentRegistry: "private", + }, + Project: creartifacts.NewConfigSourceLocal(writeFile(t, "project.yaml", []byte("cld-deploy: {}\n"))), + APIKeyName: "missing", + } + }, + setupCLI: func(t *testing.T) *cremocks.MockCLIRunner { + t.Helper() + // Inner deploy work must never run; strict mock expectations enforce this + // because no EXPECT().Run/ContextRegistries is registered on this mock. + outer := cremocks.NewMockCLIRunner(t) + outer.EXPECT().WithNamedAPIKey("missing").Return(nil, errors.New(`API key "missing" not configured`)).Once() + + return outer + }, + assert: func(t *testing.T, _ fwops.Report[CREWorkflowDeployInput, CREWorkflowDeployOutput], err error) { + t.Helper() + require.ErrorContains(t, err, `select cre api key "missing"`) + require.ErrorContains(t, err, "not configured") + }, + }, } for _, tc := range tests { diff --git a/go.mod b/go.mod index 1d4ec05..40d62d6 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,7 @@ require ( github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260429160308-91a892a60171 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260421142741-9c7fbaf7c828 github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 - github.com/smartcontractkit/chainlink-ton/deployment v0.0.0-20260430134932-681b7a7fe426 - github.com/smartcontractkit/mcms v0.41.1 + github.com/smartcontractkit/mcms v0.42.0 github.com/smartcontractkit/quarantine v0.0.0-20251203215908-fd0551c6adf9 github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 github.com/spf13/cast v1.10.0 @@ -247,6 +246,7 @@ require ( github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.19 // indirect github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 // indirect github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260430134932-681b7a7fe426 // indirect + github.com/smartcontractkit/chainlink-ton/deployment v0.0.0-20260430134932-681b7a7fe426 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260408092456-3c6369888d4a // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect diff --git a/go.sum b/go.sum index c449231..cdc4fc1 100644 --- a/go.sum +++ b/go.sum @@ -898,8 +898,8 @@ github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12i github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20260403184524-b6409238958d h1:PvXor5Fjer7FIONSqYXbpd1LkA14hWrlAyxXzOrC9t8= github.com/smartcontractkit/libocr v0.0.0-20260403184524-b6409238958d/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= -github.com/smartcontractkit/mcms v0.41.1 h1:rK5X7if29gRhL6yqpUwxwaLYV0CqgwSJivdDqEJGFv4= -github.com/smartcontractkit/mcms v0.41.1/go.mod h1:9AJhwHSVwV2mETizHBNfEF9CemL/Fmf0yPxNGdTtL/0= +github.com/smartcontractkit/mcms v0.42.0 h1:zzs+auX6BL6sRIVpgVbLUviwrvWi8Fxo5dOP+9Wx/gk= +github.com/smartcontractkit/mcms v0.42.0/go.mod h1:39OxzRApGN7HG+JGbjxdCxyo5lvV0H0REUPyh3CzDGU= github.com/smartcontractkit/quarantine v0.0.0-20251203215908-fd0551c6adf9 h1:MOEuXYogv+RStASb8dWsyescu/xkigSi/Sv45NEjV7A= github.com/smartcontractkit/quarantine v0.0.0-20251203215908-fd0551c6adf9/go.mod h1:iwy4yWFuK+1JeoIRTaSOA9pl+8Kf//26zezxEXrAQEQ= github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 h1:zxcODLrFytOKmAd8ty8S/XK6WcIEJEgRBaL7sY/7l4Y= From 1a14f460ebb9b48d5141f1893b5e0c75a1c7848d Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 13 May 2026 19:39:45 -0600 Subject: [PATCH 2/2] chore: cleanup --- cre/operations/workflow_deploy.go | 4 ---- cre/operations/workflow_deploy_test.go | 5 ----- 2 files changed, 9 deletions(-) diff --git a/cre/operations/workflow_deploy.go b/cre/operations/workflow_deploy.go index 409c626..1b031e4 100644 --- a/cre/operations/workflow_deploy.go +++ b/cre/operations/workflow_deploy.go @@ -76,10 +76,6 @@ func (in CREWorkflowDeployInput) resolveTargetName() string { } // CREWorkflowDeployOp deploys a workflow via the CRE CLI (single side effect: CLI invocation). -// -// Version history: -// - 1.1.0: input gained APIKeyName for selecting among named CRE API keys. -// - 1.0.0: initial release. var CREWorkflowDeployOp = fwops.NewOperation( "cre-workflow-deploy", semver.MustParse("1.1.0"), diff --git a/cre/operations/workflow_deploy_test.go b/cre/operations/workflow_deploy_test.go index dc47b55..d717b2a 100644 --- a/cre/operations/workflow_deploy_test.go +++ b/cre/operations/workflow_deploy_test.go @@ -185,9 +185,6 @@ func TestCREWorkflowDeployOp(t *testing.T) { }, setupCLI: func(t *testing.T) *cremocks.MockCLIRunner { t.Helper() - // The outer mock receives WithNamedAPIKey; the inner mock receives - // the deploy traffic. Strict mockery assertions in NewMockCLIRunner - // guarantee the outer mock's Run/ContextRegistries are never called. inner := cremocks.NewMockCLIRunner(t) inner.EXPECT().ContextRegistries().Return(testRegistries()).Once() inner.EXPECT().Run(mock.Anything, mock.Anything, matchCLIArgs("workflow", "deploy")).Return( @@ -223,8 +220,6 @@ func TestCREWorkflowDeployOp(t *testing.T) { }, setupCLI: func(t *testing.T) *cremocks.MockCLIRunner { t.Helper() - // Inner deploy work must never run; strict mock expectations enforce this - // because no EXPECT().Run/ContextRegistries is registered on this mock. outer := cremocks.NewMockCLIRunner(t) outer.EXPECT().WithNamedAPIKey("missing").Return(nil, errors.New(`API key "missing" not configured`)).Once()