From 4d77d67b84cf9a883f9d1e8692e758621df141f1 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Tue, 5 May 2026 13:36:12 -0400 Subject: [PATCH 1/6] Added registry prompt in cre init --- cmd/creinit/creinit.go | 43 +++++--- cmd/creinit/creinit_test.go | 9 +- cmd/creinit/wizard.go | 102 +++++++++++++++++-- docs/cre_init.md | 13 +-- internal/settings/settings_generate.go | 7 +- internal/settings/template/workflow.yaml.tpl | 4 +- 6 files changed, 146 insertions(+), 32 deletions(-) diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index ab2760de..715314c8 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -18,17 +18,19 @@ import ( "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/templateconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) type Inputs struct { - ProjectName string `validate:"omitempty,project_name" cli:"project-name"` - TemplateName string `validate:"omitempty" cli:"template"` - WorkflowName string `validate:"omitempty,workflow_name" cli:"workflow-name"` - RpcURLs map[string]string // chain-name -> url, from --rpc-url flags - NonInteractive bool - ProjectRoot string // from -R / --project-root flag + ProjectName string `validate:"omitempty,project_name" cli:"project-name"` + TemplateName string `validate:"omitempty" cli:"template"` + WorkflowName string `validate:"omitempty,workflow_name" cli:"workflow-name"` + RpcURLs map[string]string // chain-name -> url, from --rpc-url flags + DeploymentRegistry string // from --deployment-registry flag + NonInteractive bool + ProjectRoot string // from -R / --project-root flag } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -75,6 +77,7 @@ Templates are fetched dynamically from GitHub repositories.`, initCmd.Flags().StringP("template", "t", "", "Name of the template to use (e.g., kv-store-go)") initCmd.Flags().Bool("refresh", false, "Bypass template cache and fetch fresh data") initCmd.Flags().StringArray("rpc-url", nil, "RPC URL for a network (format: chain-name=url, repeatable)") + initCmd.Flags().String("deployment-registry", "", "Registry ID to deploy workflows to (e.g. my-private-registry or onchain:ethereum-testnet-sepolia)") // Deprecated: --template-id is kept for backwards compatibility, maps to hello-world-go initCmd.Flags().Uint32("template-id", 0, "") @@ -141,11 +144,12 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { } return Inputs{ - ProjectName: v.GetString("project-name"), - TemplateName: templateName, - WorkflowName: v.GetString("workflow-name"), - RpcURLs: rpcURLs, - NonInteractive: v.GetBool("non-interactive"), + ProjectName: v.GetString("project-name"), + TemplateName: templateName, + WorkflowName: v.GetString("workflow-name"), + RpcURLs: rpcURLs, + DeploymentRegistry: v.GetString("deployment-registry"), + NonInteractive: v.GetBool("non-interactive"), }, nil } @@ -251,6 +255,9 @@ func (h *handler) Execute(inputs Inputs) error { if inputs.TemplateName == "" { missingFlags = append(missingFlags, "--template") } + if inputs.DeploymentRegistry == "" { + missingFlags = append(missingFlags, "--deployment-registry") + } if selectedTemplate != nil { missing := MissingNetworks(selectedTemplate, inputs.RpcURLs) for _, network := range missing { @@ -270,7 +277,14 @@ func (h *handler) Execute(inputs Inputs) error { } // Run the interactive wizard - result, err := RunWizard(inputs, isNewProject, startDir, workflowTemplates, selectedTemplate) + var registries []*tenantctx.Registry + if h.runtimeContext.TenantContext != nil { + registries = h.runtimeContext.TenantContext.Registries + } + if len(registries) == 0 && !inputs.NonInteractive { + ui.Warning("No registries found for your organization — skipping registry selection. Run `cre registry list` to check.") + } + result, err := RunWizard(inputs, isNewProject, startDir, workflowTemplates, selectedTemplate, registries) if err != nil { // If stdin is not a terminal, the wizard will fail trying to open a TTY. // Detect this via term.IsTerminal rather than matching third-party error strings. @@ -301,6 +315,7 @@ func (h *handler) Execute(inputs Inputs) error { // Extract values from wizard result projName := result.ProjectName workflowName := result.WorkflowName + deploymentRegistry := result.RegistryID // Apply defaults if projName == "" { @@ -409,7 +424,7 @@ func (h *handler) Execute(inputs Inputs) error { h.log.Debug().Msgf("Skipping workflow.yaml generation for %s (already exists from template)", wf.Dir) continue } - if _, err := settings.GenerateWorkflowSettingsFile(wfDir, wf.Dir, entryPoint); err != nil { + if _, err := settings.GenerateWorkflowSettingsFile(wfDir, wf.Dir, entryPoint, deploymentRegistry); err != nil { return fmt.Errorf("failed to generate workflow settings for %s: %w", wf.Dir, err) } } @@ -418,7 +433,7 @@ func (h *handler) Execute(inputs Inputs) error { wfSettingsPath := filepath.Join(workflowDirectory, constants.DefaultWorkflowSettingsFileName) if _, err := os.Stat(wfSettingsPath); err == nil { h.log.Debug().Msgf("Skipping workflow.yaml generation (already exists from template)") - } else if _, err := settings.GenerateWorkflowSettingsFile(workflowDirectory, workflowName, entryPoint); err != nil { + } else if _, err := settings.GenerateWorkflowSettingsFile(workflowDirectory, workflowName, entryPoint, deploymentRegistry); err != nil { return fmt.Errorf("failed to generate %s file: %w", constants.DefaultWorkflowSettingsFileName, err) } } diff --git a/cmd/creinit/creinit_test.go b/cmd/creinit/creinit_test.go index e4755cd4..c3729508 100644 --- a/cmd/creinit/creinit_test.go +++ b/cmd/creinit/creinit_test.go @@ -919,10 +919,11 @@ func TestNonInteractiveAllFlagsProvided(t *testing.T) { defer restoreCwd() inputs := Inputs{ - ProjectName: "niProj", - TemplateName: "hello-world-go", - WorkflowName: "my-wf", - NonInteractive: true, + ProjectName: "niProj", + TemplateName: "hello-world-go", + WorkflowName: "my-wf", + DeploymentRegistry: "onchain:ethereum-testnet-sepolia", + NonInteractive: true, } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) diff --git a/cmd/creinit/wizard.go b/cmd/creinit/wizard.go index 7b42de8c..b3e441ce 100644 --- a/cmd/creinit/wizard.go +++ b/cmd/creinit/wizard.go @@ -16,6 +16,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/templaterepo" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -85,6 +86,21 @@ func (f languageFilter) next() languageFilter { } } +// sortRegistries sorts registries: private (off-chain) first, everything else after. +func sortRegistries(registries []*tenantctx.Registry) []*tenantctx.Registry { + priority := func(r *tenantctx.Registry) int { + if strings.EqualFold(r.Type, "off-chain") { + return 0 + } + return 1 + } + sorted := slices.Clone(registries) + slices.SortStableFunc(sorted, func(a, b *tenantctx.Registry) int { + return priority(a) - priority(b) + }) + return sorted +} + // sortTemplates sorts templates: built-in first, then by kind, then alphabetical by title. func sortTemplates(templates []templaterepo.TemplateSummary) []templaterepo.TemplateSummary { sorted := slices.Clone(templates) @@ -257,6 +273,7 @@ const ( stepTemplateConfirm stepNetworkRPCs stepWorkflowName + stepRegistry stepDone ) @@ -290,6 +307,12 @@ type wizardModel struct { // Pre-provided RPC URLs from flags flagRpcURLs map[string]string + // Registry selection + registries []*tenantctx.Registry + registryCursor int + selectedRegistryID string + skipRegistry bool + // Flags to skip steps skipProjectName bool skipTemplate bool @@ -327,12 +350,13 @@ type WizardResult struct { WorkflowName string SelectedTemplate *templaterepo.TemplateSummary NetworkRPCs map[string]string // chain-name -> rpc-url + RegistryID string // selected deployment-registry ID OverwriteDir bool // user confirmed directory overwrite in wizard Completed bool Cancelled bool } -func newWizardModel(inputs Inputs, isNewProject bool, startDir string, templates []templaterepo.TemplateSummary, preselected *templaterepo.TemplateSummary) wizardModel { +func newWizardModel(inputs Inputs, isNewProject bool, startDir string, templates []templaterepo.TemplateSummary, preselected *templaterepo.TemplateSummary, registries []*tenantctx.Registry) wizardModel { // Project name input pi := textinput.New() pi.Placeholder = constants.DefaultProjectName @@ -373,6 +397,7 @@ func newWizardModel(inputs Inputs, isNewProject bool, startDir string, templates flagRpcURLs: flagRPCs, startDir: startDir, isNewProject: isNewProject, + registries: registries, // Styles logoStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(ui.ColorBlue500)).Bold(true), @@ -407,6 +432,16 @@ func newWizardModel(inputs Inputs, isNewProject bool, startDir string, templates m.skipWorkflowName = true } + if inputs.DeploymentRegistry != "" { + m.selectedRegistryID = inputs.DeploymentRegistry + m.skipRegistry = true + } else if len(registries) == 0 { + m.skipRegistry = true + } else { + // Sort: off-chain (private) registries first so the default cursor lands on them. + m.registries = sortRegistries(registries) + } + // Start at the right step m.advanceToNextStep() @@ -508,6 +543,12 @@ func (m *wizardModel) advanceToNextStep() { } m.workflowInput.Focus() return + case stepRegistry: + if m.skipRegistry { + m.step++ + continue + } + return case stepDone: m.completed = true return @@ -618,6 +659,27 @@ func (m wizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + if m.step == stepRegistry { + switch msg.String() { + case "ctrl+c", "esc": + m.cancelled = true + return m, tea.Quit + case "up", "k": + if m.registryCursor > 0 { + m.registryCursor-- + } + return m, nil + case "down", "j": + if m.registryCursor < len(m.registries)-1 { + m.registryCursor++ + } + return m, nil + case "enter": + return m.handleEnter() + } + return m, nil + } + switch msg.String() { case "ctrl+c", "esc": m.cancelled = true @@ -641,9 +703,7 @@ func (m wizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case stepTemplate: // Forward non-key messages (e.g. FilterMatchesMsg) to the list m.templateList, cmd = m.templateList.Update(msg) - case stepTemplateConfirm: - // Nothing to update - case stepDone: + case stepTemplateConfirm, stepRegistry, stepDone: // Nothing to update } @@ -741,6 +801,13 @@ func (m wizardModel) handleEnter(msgs ...tea.Msg) (tea.Model, tea.Cmd) { m.step++ m.advanceToNextStep() + case stepRegistry: + if m.registryCursor < len(m.registries) { + m.selectedRegistryID = m.registries[m.registryCursor].ID + } + m.step++ + m.advanceToNextStep() + case stepDone: // Already done } @@ -931,6 +998,28 @@ func (m wizardModel) View() string { } } + case stepRegistry: + b.WriteString(m.promptStyle.Render(" Deployment registry")) + b.WriteString("\n") + b.WriteString(m.dimStyle.Render(" Private registries are hosted by Chainlink — no gas needed, ideal for quick testing.")) + b.WriteString("\n") + b.WriteString(m.dimStyle.Render(" On-chain registries require gas and a Web3 key — ideal for production and multisig ownership.")) + b.WriteString("\n\n") + for i, reg := range m.registries { + regType := reg.Type + label := reg.Label + if label == "" { + label = reg.ID + } + typeTag := m.tagStyle.Render("[" + regType + "]") + if i == m.registryCursor { + cursor := m.cursorStyle.Render(">") + fmt.Fprintf(&b, " %s %s %s\n", cursor, typeTag, m.selectedStyle.Render(label)) + } else { + fmt.Fprintf(&b, " %s %s\n", typeTag, m.dimStyle.Render(label)) + } + } + case stepDone: // Nothing to render } @@ -967,6 +1056,7 @@ func (m wizardModel) Result() WizardResult { WorkflowName: m.workflowName, SelectedTemplate: m.selectedTemplate, NetworkRPCs: m.networkRPCs, + RegistryID: m.selectedRegistryID, OverwriteDir: m.overwriteDir, Completed: m.completed, Cancelled: m.cancelled, @@ -974,8 +1064,8 @@ func (m wizardModel) Result() WizardResult { } // RunWizard runs the interactive wizard and returns the result. -func RunWizard(inputs Inputs, isNewProject bool, startDir string, templates []templaterepo.TemplateSummary, preselected *templaterepo.TemplateSummary) (WizardResult, error) { - m := newWizardModel(inputs, isNewProject, startDir, templates, preselected) +func RunWizard(inputs Inputs, isNewProject bool, startDir string, templates []templaterepo.TemplateSummary, preselected *templaterepo.TemplateSummary, registries []*tenantctx.Registry) (WizardResult, error) { + m := newWizardModel(inputs, isNewProject, startDir, templates, preselected, registries) // Check if all steps are skipped if m.completed { diff --git a/docs/cre_init.md b/docs/cre_init.md index 962da29a..3ee24a3e 100644 --- a/docs/cre_init.md +++ b/docs/cre_init.md @@ -18,12 +18,13 @@ cre init [optional flags] ### Options ``` - -h, --help help for init - -p, --project-name string Name for the new project - --refresh Bypass template cache and fetch fresh data - --rpc-url stringArray RPC URL for a network (format: chain-name=url, repeatable) - -t, --template string Name of the template to use (e.g., kv-store-go) - -w, --workflow-name string Name for the new workflow + --deployment-registry string Registry ID to deploy workflows to (e.g. my-private-registry or onchain:ethereum-testnet-sepolia) + -h, --help help for init + -p, --project-name string Name for the new project + --refresh Bypass template cache and fetch fresh data + --rpc-url stringArray RPC URL for a network (format: chain-name=url, repeatable) + -t, --template string Name of the template to use (e.g., kv-store-go) + -w, --workflow-name string Name for the new workflow ``` ### Options inherited from parent commands diff --git a/internal/settings/settings_generate.go b/internal/settings/settings_generate.go index 1651cbdc..1be54d2e 100644 --- a/internal/settings/settings_generate.go +++ b/internal/settings/settings_generate.go @@ -178,11 +178,16 @@ func FindOrCreateProjectSettings(startDir string, replacements map[string]string return nil } -func GenerateWorkflowSettingsFile(workingDirectory string, workflowName string, workflowPath string) (string, error) { +func GenerateWorkflowSettingsFile(workingDirectory string, workflowName string, workflowPath string, deploymentRegistry string) (string, error) { // Use default replacements. replacements := GetDefaultReplacements() replacements["WorkflowName"] = workflowName replacements["WorkflowPath"] = workflowPath + if deploymentRegistry != "" { + replacements["DeploymentRegistryLine"] = fmt.Sprintf(" deployment-registry: %q", deploymentRegistry) + } else { + replacements["DeploymentRegistryLine"] = "" + } // Resolve the absolute output path for the workflow settings file. outputPath, err := filepath.Abs(path.Join(workingDirectory, constants.DefaultWorkflowSettingsFileName)) diff --git a/internal/settings/template/workflow.yaml.tpl b/internal/settings/template/workflow.yaml.tpl index ae0124b7..6741729a 100644 --- a/internal/settings/template/workflow.yaml.tpl +++ b/internal/settings/template/workflow.yaml.tpl @@ -18,16 +18,18 @@ staging-settings: user-workflow: workflow-name: "{{WorkflowName}}-staging" +{{DeploymentRegistryLine}} workflow-artifacts: workflow-path: "{{WorkflowPath}}" config-path: "{{ConfigPathStaging}}" secrets-path: "{{SecretsPath}}" - + # ========================================================================== production-settings: user-workflow: workflow-name: "{{WorkflowName}}-production" +{{DeploymentRegistryLine}} workflow-artifacts: workflow-path: "{{WorkflowPath}}" config-path: "{{ConfigPathProduction}}" From 67471bfa1a4d0628b44abe94f9f781cfdccd17b4 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Tue, 5 May 2026 21:27:19 -0400 Subject: [PATCH 2/6] =?UTF-8?q?Non-interactive,=20no=20--deployment-regist?= =?UTF-8?q?ry=20=E2=86=92=20warning=20printed,=20defaults=20to=20on-chain.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/creinit/creinit.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index 715314c8..19fc5f8f 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -255,9 +255,6 @@ func (h *handler) Execute(inputs Inputs) error { if inputs.TemplateName == "" { missingFlags = append(missingFlags, "--template") } - if inputs.DeploymentRegistry == "" { - missingFlags = append(missingFlags, "--deployment-registry") - } if selectedTemplate != nil { missing := MissingNetworks(selectedTemplate, inputs.RpcURLs) for _, network := range missing { @@ -277,6 +274,10 @@ func (h *handler) Execute(inputs Inputs) error { } // Run the interactive wizard + if inputs.NonInteractive && inputs.DeploymentRegistry == "" { + ui.Warning("No --deployment-registry specified, defaulting to on-chain registry. Add --deployment-registry to make this explicit.") + } + var registries []*tenantctx.Registry if h.runtimeContext.TenantContext != nil { registries = h.runtimeContext.TenantContext.Registries From d356b35e27b0de7c5bcf9003381b7e9954500a05 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Thu, 23 Jul 2026 12:18:59 -0400 Subject: [PATCH 3/6] tests --- cmd/creinit/creinit_test.go | 132 ++++++++++-------- ...binding_generation_and_simulate_go_test.go | 1 + test/init_and_simulate_ts_test.go | 1 + test/init_convert_simulate_go_test.go | 1 + test/init_convert_simulate_ts_test.go | 1 + .../workflow_happy_path_3.go | 10 +- 6 files changed, 86 insertions(+), 60 deletions(-) diff --git a/cmd/creinit/creinit_test.go b/cmd/creinit/creinit_test.go index 79a266fd..f007094c 100644 --- a/cmd/creinit/creinit_test.go +++ b/cmd/creinit/creinit_test.go @@ -416,10 +416,11 @@ func TestInitExecuteFlows(t *testing.T) { defer restoreCwd() inputs := Inputs{ - ProjectName: tc.projectNameFlag, - TemplateName: tc.templateNameFlag, - WorkflowName: tc.workflowNameFlag, - RpcURLs: tc.rpcURLs, + ProjectName: tc.projectNameFlag, + TemplateName: tc.templateNameFlag, + WorkflowName: tc.workflowNameFlag, + RpcURLs: tc.rpcURLs, + DeploymentRegistry: "private", } ctx := sim.NewRuntimeContext() @@ -451,10 +452,11 @@ func TestInsideExistingProjectAddsWorkflow(t *testing.T) { _ = os.Remove(constants.DefaultEnvFileName) inputs := Inputs{ - ProjectName: "", - TemplateName: "test-go", - WorkflowName: "wf-inside-existing-project", - RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + ProjectName: "", + TemplateName: "test-go", + WorkflowName: "wf-inside-existing-project", + RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -483,9 +485,10 @@ func TestInitWithTypescriptTemplateSkipsGoScaffold(t *testing.T) { defer restoreCwd() inputs := Inputs{ - ProjectName: "tsProj", - TemplateName: "test-ts", - WorkflowName: "ts-workflow-01", + ProjectName: "tsProj", + TemplateName: "test-ts", + WorkflowName: "ts-workflow-01", + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -515,13 +518,14 @@ func TestInitWithRpcUrlFlags(t *testing.T) { defer restoreCwd() inputs := Inputs{ - ProjectName: "rpcProj", - TemplateName: "test-multichain", - WorkflowName: "rpc-workflow", + ProjectName: "rpcProj", + TemplateName: "test-multichain", + WorkflowName: "rpc-workflow", RpcURLs: map[string]string{ "ethereum-testnet-sepolia": "https://sepolia.example.com", "ethereum-mainnet": "https://mainnet.example.com", }, + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -554,9 +558,10 @@ func TestInitNoNetworksFallsBackToDefault(t *testing.T) { // Built-in template has no project.yaml from scaffold, // so the CLI generates one with default networks. inputs := Inputs{ - ProjectName: "defaultProj", - TemplateName: "hello-world-go", - WorkflowName: "default-wf", + ProjectName: "defaultProj", + TemplateName: "hello-world-go", + WorkflowName: "default-wf", + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -583,9 +588,10 @@ func TestInitRemoteTemplateKeepsProjectYAML(t *testing.T) { // Remote template (test-ts) has no Networks — mock creates project.yaml with default chain. // CLI should preserve the template's project.yaml (no patching needed since no user RPCs). inputs := Inputs{ - ProjectName: "remoteProj", - TemplateName: "test-ts", - WorkflowName: "ts-wf", + ProjectName: "remoteProj", + TemplateName: "test-ts", + WorkflowName: "ts-wf", + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -618,13 +624,14 @@ func TestInitProjectDirTemplateRpcPatching(t *testing.T) { // Template with ProjectDir set AND Networks — the bug was that RPC URLs // were silently dropped because the patching was inside the ProjectDir=="" block. inputs := Inputs{ - ProjectName: "projectDirProj", - TemplateName: "starter-with-projectdir", - WorkflowName: "my-workflow", + ProjectName: "projectDirProj", + TemplateName: "starter-with-projectdir", + WorkflowName: "my-workflow", RpcURLs: map[string]string{ "ethereum-testnet-sepolia": "https://sepolia.custom.com", "ethereum-mainnet": "https://mainnet.custom.com", }, + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -655,9 +662,10 @@ func TestTemplateNotFound(t *testing.T) { defer restoreCwd() inputs := Inputs{ - ProjectName: "proj", - TemplateName: "nonexistent-template", - WorkflowName: "wf", + ProjectName: "proj", + TemplateName: "nonexistent-template", + WorkflowName: "wf", + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -679,10 +687,11 @@ func TestMultiWorkflowNoRename(t *testing.T) { // Multi-workflow template: no --workflow-name needed, dirs stay as declared inputs := Inputs{ - ProjectName: "multiProj", - TemplateName: "bring-your-own-data-go", - WorkflowName: "", - RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + ProjectName: "multiProj", + TemplateName: "bring-your-own-data-go", + WorkflowName: "", + RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -713,10 +722,11 @@ func TestMultiWorkflowIgnoresWorkflowNameFlag(t *testing.T) { // Multi-workflow with --workflow-name flag: flag should be ignored inputs := Inputs{ - ProjectName: "multiProj2", - TemplateName: "bring-your-own-data-go", - WorkflowName: "test-rename", - RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + ProjectName: "multiProj2", + TemplateName: "bring-your-own-data-go", + WorkflowName: "test-rename", + RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -748,9 +758,10 @@ func TestSingleWorkflowDefaultFromTemplate(t *testing.T) { // Note: We must provide a workflow name to avoid the TTY prompt in tests. // Instead, we verify the default logic by providing it explicitly. inputs := Inputs{ - ProjectName: "singleProj", - TemplateName: "kv-store-go", - WorkflowName: "my-workflow", // same as template's workflows[0].dir + ProjectName: "singleProj", + TemplateName: "kv-store-go", + WorkflowName: "my-workflow", // same as template's workflows[0].dir + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -797,9 +808,10 @@ func TestSingleWorkflowRenameWithFlag(t *testing.T) { // Single workflow with --workflow-name: should rename to user's choice inputs := Inputs{ - ProjectName: "renameProj", - TemplateName: "kv-store-go", - WorkflowName: "counter", + ProjectName: "renameProj", + TemplateName: "kv-store-go", + WorkflowName: "counter", + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -827,9 +839,10 @@ func TestBuiltInTemplateBackwardsCompat(t *testing.T) { // Built-in template has no Workflows field — should use existing heuristic inputs := Inputs{ - ProjectName: "builtinProj", - TemplateName: "hello-world-go", - WorkflowName: "hello-wf", + ProjectName: "builtinProj", + TemplateName: "hello-world-go", + WorkflowName: "hello-wf", + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -906,11 +919,12 @@ func TestNonInteractiveMissingFlags(t *testing.T) { defer restoreCwd() inputs := Inputs{ - ProjectName: "proj", - TemplateName: "test-multichain", - WorkflowName: "", - NonInteractive: true, - RpcURLs: map[string]string{}, + ProjectName: "proj", + TemplateName: "test-multichain", + WorkflowName: "", + NonInteractive: true, + RpcURLs: map[string]string{}, + DeploymentRegistry: "private", } h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) @@ -959,11 +973,12 @@ func TestInitRespectsProjectRootFlag(t *testing.T) { targetDir := t.TempDir() inputs := Inputs{ - ProjectName: "myproj", - TemplateName: "test-go", - WorkflowName: "mywf", - RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, - ProjectRoot: targetDir, + ProjectName: "myproj", + TemplateName: "test-go", + WorkflowName: "mywf", + RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + ProjectRoot: targetDir, + DeploymentRegistry: "private", } ctx := sim.NewRuntimeContext() @@ -1004,11 +1019,12 @@ func TestInitProjectRootFlagFindsExistingProject(t *testing.T) { )) inputs := Inputs{ - ProjectName: "", - TemplateName: "test-go", - WorkflowName: "new-workflow", - RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, - ProjectRoot: existingProject, + ProjectName: "", + TemplateName: "test-go", + WorkflowName: "new-workflow", + RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + ProjectRoot: existingProject, + DeploymentRegistry: "private", } ctx := sim.NewRuntimeContext() 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 53cf7a60..73fb3a13 100644 --- a/test/init_and_binding_generation_and_simulate_go_test.go +++ b/test/init_and_binding_generation_and_simulate_go_test.go @@ -37,6 +37,7 @@ func TestE2EInit_DevPoRTemplate(t *testing.T) { "--project-name", projectName, "--template", templateName, "--workflow-name", workflowName, + "--deployment-registry", "private", } var stdout, stderr bytes.Buffer initCmd := exec.Command(CLIPath, initArgs...) diff --git a/test/init_and_simulate_ts_test.go b/test/init_and_simulate_ts_test.go index d0e4a0e4..5f51c16f 100644 --- a/test/init_and_simulate_ts_test.go +++ b/test/init_and_simulate_ts_test.go @@ -36,6 +36,7 @@ func TestE2EInit_DevPoRTemplateTS(t *testing.T) { "--project-name", projectName, "--template", templateName, "--workflow-name", workflowName, + "--deployment-registry", "private", } var stdout, stderr bytes.Buffer initCmd := exec.Command(CLIPath, initArgs...) diff --git a/test/init_convert_simulate_go_test.go b/test/init_convert_simulate_go_test.go index ab7c2f96..b644053d 100644 --- a/test/init_convert_simulate_go_test.go +++ b/test/init_convert_simulate_go_test.go @@ -39,6 +39,7 @@ func TestE2EInit_ConvertToCustomBuild_Go(t *testing.T) { "--project-name", projectName, "--template-id", templateID, "--workflow-name", workflowName, + "--deployment-registry", "private", ) initCmd.Dir = tempDir initCmd.Stdout = &stdout diff --git a/test/init_convert_simulate_ts_test.go b/test/init_convert_simulate_ts_test.go index 1a552f7b..e2834d64 100644 --- a/test/init_convert_simulate_ts_test.go +++ b/test/init_convert_simulate_ts_test.go @@ -40,6 +40,7 @@ func TestE2EInit_ConvertToCustomBuild_TS(t *testing.T) { "--project-name", projectName, "--template-id", templateID, "--workflow-name", workflowName, + "--deployment-registry", "private", ) initCmd.Dir = tempDir initCmd.Stdout = &stdout diff --git a/test/multi_command_flows/workflow_happy_path_3.go b/test/multi_command_flows/workflow_happy_path_3.go index 5e125e74..87e62b13 100644 --- a/test/multi_command_flows/workflow_happy_path_3.go +++ b/test/multi_command_flows/workflow_happy_path_3.go @@ -21,6 +21,11 @@ import ( // workflowInit runs cre init to initialize a new workflow project from scratch func workflowInit(t *testing.T, projectRootFlag, projectName, workflowName string) (output string, gqlURL string) { + return workflowInitWithRegistry(t, projectRootFlag, projectName, workflowName, "private") +} + +// workflowInitWithRegistry runs cre init with a specific deployment registry +func workflowInitWithRegistry(t *testing.T, projectRootFlag, projectName, workflowName, registry string) (output string, gqlURL string) { t.Helper() // Set up mock GraphQL server for authentication validation @@ -59,6 +64,7 @@ func workflowInit(t *testing.T, projectRootFlag, projectName, workflowName strin "--project-name", projectName, "--workflow-name", workflowName, "--template", "hello-world-go", // Use the built-in Go template + "--deployment-registry", registry, } cmd := exec.Command(CLIPath, args...) @@ -363,8 +369,8 @@ func RunHappyPath3aWorkflow(t *testing.T, tc TestConfig, projectName, ownerAddre workflowName := "happy-path-3a-workflow" - // Step 1: Initialize new project with workflow - initOut, gqlURL := workflowInit(t, tc.GetProjectRootFlag(), projectName, workflowName) + // Step 1: Initialize new project with workflow using on-chain registry (required for --unsigned) + initOut, gqlURL := workflowInitWithRegistry(t, tc.GetProjectRootFlag(), projectName, workflowName, "onchain:anvil-devnet") require.Contains(t, initOut, "Project created successfully", "expected init to succeed.\nCLI OUTPUT:\n%s", initOut) // Build the project root flag pointing to the newly created project From 29f4477000b50c18abd173d6b033eb9a8a855c27 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Thu, 23 Jul 2026 12:30:03 -0400 Subject: [PATCH 4/6] lint --- cmd/creinit/creinit_test.go | 2 +- test/multi_command_flows/workflow_happy_path_3.go | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/cmd/creinit/creinit_test.go b/cmd/creinit/creinit_test.go index f007094c..ac95a178 100644 --- a/cmd/creinit/creinit_test.go +++ b/cmd/creinit/creinit_test.go @@ -521,7 +521,7 @@ func TestInitWithRpcUrlFlags(t *testing.T) { ProjectName: "rpcProj", TemplateName: "test-multichain", WorkflowName: "rpc-workflow", - RpcURLs: map[string]string{ + RpcURLs: map[string]string{ "ethereum-testnet-sepolia": "https://sepolia.example.com", "ethereum-mainnet": "https://mainnet.example.com", }, diff --git a/test/multi_command_flows/workflow_happy_path_3.go b/test/multi_command_flows/workflow_happy_path_3.go index 87e62b13..d79ad7e7 100644 --- a/test/multi_command_flows/workflow_happy_path_3.go +++ b/test/multi_command_flows/workflow_happy_path_3.go @@ -19,11 +19,6 @@ import ( "github.com/smartcontractkit/cre-cli/internal/testutil" ) -// workflowInit runs cre init to initialize a new workflow project from scratch -func workflowInit(t *testing.T, projectRootFlag, projectName, workflowName string) (output string, gqlURL string) { - return workflowInitWithRegistry(t, projectRootFlag, projectName, workflowName, "private") -} - // workflowInitWithRegistry runs cre init with a specific deployment registry func workflowInitWithRegistry(t *testing.T, projectRootFlag, projectName, workflowName, registry string) (output string, gqlURL string) { t.Helper() From 1ae8f5c7b0d3c45bcc00912dc5f3b01fc02264ae Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Thu, 23 Jul 2026 14:07:17 -0400 Subject: [PATCH 5/6] tests to pass --- cmd/creinit/creinit_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/creinit/creinit_test.go b/cmd/creinit/creinit_test.go index ac95a178..1a551eb6 100644 --- a/cmd/creinit/creinit_test.go +++ b/cmd/creinit/creinit_test.go @@ -517,14 +517,15 @@ func TestInitWithRpcUrlFlags(t *testing.T) { require.NoError(t, err) defer restoreCwd() + rpcURLs := map[string]string{ + "ethereum-testnet-sepolia": "https://sepolia.example.com", + "ethereum-mainnet": "https://mainnet.example.com", + } inputs := Inputs{ ProjectName: "rpcProj", TemplateName: "test-multichain", WorkflowName: "rpc-workflow", - RpcURLs: map[string]string{ - "ethereum-testnet-sepolia": "https://sepolia.example.com", - "ethereum-mainnet": "https://mainnet.example.com", - }, + RpcURLs: rpcURLs, DeploymentRegistry: "private", } From d3f8d3f5fe2c46274c6aba55e21eafe5cbd189f4 Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Mon, 27 Jul 2026 09:20:53 -0400 Subject: [PATCH 6/6] linter --- cmd/creinit/creinit_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/creinit/creinit_test.go b/cmd/creinit/creinit_test.go index 1a551eb6..ea33988e 100644 --- a/cmd/creinit/creinit_test.go +++ b/cmd/creinit/creinit_test.go @@ -625,9 +625,9 @@ func TestInitProjectDirTemplateRpcPatching(t *testing.T) { // Template with ProjectDir set AND Networks — the bug was that RPC URLs // were silently dropped because the patching was inside the ProjectDir=="" block. inputs := Inputs{ - ProjectName: "projectDirProj", - TemplateName: "starter-with-projectdir", - WorkflowName: "my-workflow", + ProjectName: "projectDirProj", + TemplateName: "starter-with-projectdir", + WorkflowName: "my-workflow", RpcURLs: map[string]string{ "ethereum-testnet-sepolia": "https://sepolia.custom.com", "ethereum-mainnet": "https://mainnet.custom.com",