From 6fb0b26811ae8b655bafca5d88759d0d0979c9b9 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Aug 2026 18:38:42 +0100 Subject: [PATCH 1/5] refactor: Add `LogCloudInitializationStart` method to `init` command's view implementations --- internal/command/init.go | 2 +- internal/command/views/init.go | 16 ++++++++++++++++ internal/command/views/init_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/internal/command/init.go b/internal/command/init.go index c56db136cc5f..06d8bec59bea 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -146,7 +146,7 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra _ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here defer span.End() - view.Output(views.InitializingTerraformCloudMessage) + view.LogCloudInitializationStart() if len(extraConfig.AllItems()) != 0 { diags = diags.Append(tfdiags.Sourceless( diff --git a/internal/command/views/init.go b/internal/command/views/init.go index cf8d75f1b1f0..09fae610bb68 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -22,6 +22,9 @@ type Init interface { PolicyDiagnostics(diags policy.Diagnostics) Output(messageCode InitMessageCode, params ...any) + // LogCloudInitializationStart describes the start of initializing the cloud backend + LogCloudInitializationStart() + ModuleInstallationLogger ProviderInstallationLogger DependencyLockingLogger @@ -78,6 +81,11 @@ func (v *InitHuman) Output(messageCode InitMessageCode, params ...any) { v.print(v.prepareMessage(messageCode, params...)) } +func (v *InitHuman) LogCloudInitializationStart() { + params := []any{} + v.print(v.prepareMessage(InitializingTerraformCloudMessage, params...)) +} + func (v *InitHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { consSuffix := "" if len(cons) > 0 { @@ -251,6 +259,14 @@ func (v *InitJSON) Output(messageCode InitMessageCode, params ...any) { ) } +func (v *InitJSON) LogCloudInitializationStart() { + params := []any{} + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(InitializingTerraformCloudMessage, params...) +} + // logInitMessage is an internalised version of an old method `LogInitMessage`. // New methods have since been added that replace the old `LogInitMessage` method, // but to ensure that the same JSON output is produced we keep `logInitMessage` to diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 494b1306e4ea..c9c1694e56a7 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -1051,6 +1051,30 @@ func TestNewInit_LogModuleInitialization_json(t *testing.T) { } } +func TestNewInit_LogCloudInitializationStart_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + initView.LogCloudInitializationStart() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Initializing HCP Terraform..."`, + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"initializing_terraform_cloud_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } +} + func TestNewInit_Spacer_json(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) From e005a044baf1e729f2d7c4ebe018add7808d7c97 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Aug 2026 18:50:47 +0100 Subject: [PATCH 2/5] refactor: Add `LogCloudInitializationComplete` and `LogInitializationComplete` methods to `init` command's view implementations --- internal/command/init_run.go | 9 +++--- internal/command/views/init.go | 32 +++++++++++++++++++ internal/command/views/init_test.go | 48 +++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/internal/command/init_run.go b/internal/command/init_run.go index cae8c7567297..7dee32b09100 100644 --- a/internal/command/init_run.go +++ b/internal/command/init_run.go @@ -446,18 +446,17 @@ Please use \"terraform state migrate -upgrade\" to upgrade the state store provi // still the final thing shown. view.Diagnostics(diags) _, cloud := back.(*cloud.Cloud) - output := views.OutputInitSuccessMessage if cloud { - output = views.OutputInitSuccessCloudMessage + view.LogCloudInitializationComplete() + } else { + view.LogInitializationComplete() } - view.Output(output) - if !c.RunningInAutomation { // If we're not running in an automation wrapper, give the user // some more detailed next steps that are appropriate for interactive // shell usage. - output = views.OutputInitSuccessCLIMessage + output := views.OutputInitSuccessCLIMessage if cloud { output = views.OutputInitSuccessCLICloudMessage } diff --git a/internal/command/views/init.go b/internal/command/views/init.go index 09fae610bb68..acc207ecb74a 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -24,6 +24,12 @@ type Init interface { // LogCloudInitializationStart describes the start of initializing the cloud backend LogCloudInitializationStart() + // LogCloudInitializationComplete describes the successful end of initializing the workspace + // while using the cloud backend + LogCloudInitializationComplete() + + // LogInitializationComplete describes the successful end of initializing the workspace + LogInitializationComplete() ModuleInstallationLogger ProviderInstallationLogger @@ -86,6 +92,16 @@ func (v *InitHuman) LogCloudInitializationStart() { v.print(v.prepareMessage(InitializingTerraformCloudMessage, params...)) } +func (v *InitHuman) LogCloudInitializationComplete() { + params := []any{} + v.print(v.prepareMessage(OutputInitSuccessCloudMessage, params...)) +} + +func (v *InitHuman) LogInitializationComplete() { + params := []any{} + v.print(v.prepareMessage(OutputInitSuccessMessage, params...)) +} + func (v *InitHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { consSuffix := "" if len(cons) > 0 { @@ -267,6 +283,22 @@ func (v *InitJSON) LogCloudInitializationStart() { v.Output(InitializingTerraformCloudMessage, params...) } +func (v *InitJSON) LogCloudInitializationComplete() { + params := []any{} + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(OutputInitSuccessCloudMessage, params...) +} + +func (v *InitJSON) LogInitializationComplete() { + params := []any{} + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(OutputInitSuccessMessage, params...) +} + // logInitMessage is an internalised version of an old method `LogInitMessage`. // New methods have since been added that replace the old `LogInitMessage` method, // but to ensure that the same JSON output is produced we keep `logInitMessage` to diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index c9c1694e56a7..296018c24749 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -1075,6 +1075,54 @@ func TestNewInit_LogCloudInitializationStart_json(t *testing.T) { } } +func TestNewInit_LogCloudInitializationComplete_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + initView.LogCloudInitializationComplete() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"HCP Terraform has been successfully initialized!"`, + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_success_cloud_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } +} + +func TestNewInit_LogInitializationComplete_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + initView.LogInitializationComplete() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Terraform has been successfully initialized!"`, + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_success_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } +} + func TestNewInit_Spacer_json(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) From 73e3f0007c209bf6caaad1d9c160bff58ff2fcb6 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Aug 2026 19:12:29 +0100 Subject: [PATCH 3/5] refactor: Add `LogCloudInitializationCompleteCallToAction` and `LogInitializationCompleteCallToAction` methods to `init` command's view implementations --- internal/command/init_run.go | 6 ++-- internal/command/views/init.go | 32 +++++++++++++++++++ internal/command/views/init_test.go | 48 +++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/internal/command/init_run.go b/internal/command/init_run.go index 7dee32b09100..bdfd89fadf36 100644 --- a/internal/command/init_run.go +++ b/internal/command/init_run.go @@ -456,11 +456,11 @@ Please use \"terraform state migrate -upgrade\" to upgrade the state store provi // If we're not running in an automation wrapper, give the user // some more detailed next steps that are appropriate for interactive // shell usage. - output := views.OutputInitSuccessCLIMessage if cloud { - output = views.OutputInitSuccessCLICloudMessage + view.LogCloudInitializationCompleteCallToAction() + } else { + view.LogInitializationCompleteCallToAction() } - view.Output(output) } return 0 } diff --git a/internal/command/views/init.go b/internal/command/views/init.go index acc207ecb74a..af2213e98da0 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -27,9 +27,15 @@ type Init interface { // LogCloudInitializationComplete describes the successful end of initializing the workspace // while using the cloud backend LogCloudInitializationComplete() + // LogCloudInitializationCompleteCallToAction prompts users about what to do next after initialization. + // This is only used if the cloud backend is used, and the CLI is being used outside of automation; a human will see the CTA. + LogCloudInitializationCompleteCallToAction() // LogInitializationComplete describes the successful end of initializing the workspace LogInitializationComplete() + // LogInitializationCompleteCallToAction prompts users about what to do next after initialization. + // This is only used if the CLI is being used outside of automation; a human will see the CTA. + LogInitializationCompleteCallToAction() ModuleInstallationLogger ProviderInstallationLogger @@ -97,6 +103,16 @@ func (v *InitHuman) LogCloudInitializationComplete() { v.print(v.prepareMessage(OutputInitSuccessCloudMessage, params...)) } +func (v *InitHuman) LogCloudInitializationCompleteCallToAction() { + params := []any{} + v.print(v.prepareMessage(OutputInitSuccessCLICloudMessage, params...)) +} + +func (v *InitHuman) LogInitializationCompleteCallToAction() { + params := []any{} + v.print(v.prepareMessage(OutputInitSuccessCLIMessage, params...)) +} + func (v *InitHuman) LogInitializationComplete() { params := []any{} v.print(v.prepareMessage(OutputInitSuccessMessage, params...)) @@ -291,6 +307,22 @@ func (v *InitJSON) LogCloudInitializationComplete() { v.Output(OutputInitSuccessCloudMessage, params...) } +func (v *InitJSON) LogCloudInitializationCompleteCallToAction() { + params := []any{} + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(OutputInitSuccessCLICloudMessage, params...) +} + +func (v *InitJSON) LogInitializationCompleteCallToAction() { + params := []any{} + + // This was previously logged via Output, so we need to match implementation of that method + // to ensure the same JSON log is produced. + v.Output(OutputInitSuccessCLIMessage, params...) +} + func (v *InitJSON) LogInitializationComplete() { params := []any{} diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 296018c24749..2ea26f4eb2f5 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -1123,6 +1123,54 @@ func TestNewInit_LogInitializationComplete_json(t *testing.T) { } } +func TestNewInit_LogCloudInitializationCompleteCallToAction_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + initView.LogCloudInitializationCompleteCallToAction() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"You may now begin working with HCP Terraform. Try running \"terraform plan\"`, // ... incomplete but sufficient for test + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_success_cli_cloud_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } +} + +func TestNewInit_LogInitializationCompleteCallToAction_json(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + initView.LogInitializationCompleteCallToAction() + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"You may now begin working with Terraform. Try running \"terraform plan\" `, // ... incomplete but sufficient for test + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_success_cli_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } +} + func TestNewInit_Spacer_json(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) From a9d129ab627d03bfd1b5c3bd0ed5a7c16dfd629e Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 7 Aug 2026 19:28:27 +0100 Subject: [PATCH 4/5] refactor: Update `LogInitializationComplete` to be able to log about successful initialization of a project with empty config. --- internal/command/init_run.go | 4 +- internal/command/views/init.go | 25 +++++++++--- internal/command/views/init_test.go | 63 ++++++++++++++++++++--------- 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/internal/command/init_run.go b/internal/command/init_run.go index bdfd89fadf36..bd9474f7ea19 100644 --- a/internal/command/init_run.go +++ b/internal/command/init_run.go @@ -110,7 +110,7 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { return 1 } if empty { - view.Output(views.OutputInitEmptyMessage) + view.LogInitializationComplete(true) return 0 } @@ -449,7 +449,7 @@ Please use \"terraform state migrate -upgrade\" to upgrade the state store provi if cloud { view.LogCloudInitializationComplete() } else { - view.LogInitializationComplete() + view.LogInitializationComplete(false) } if !c.RunningInAutomation { diff --git a/internal/command/views/init.go b/internal/command/views/init.go index af2213e98da0..221d9af5d168 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -31,8 +31,9 @@ type Init interface { // This is only used if the cloud backend is used, and the CLI is being used outside of automation; a human will see the CTA. LogCloudInitializationCompleteCallToAction() - // LogInitializationComplete describes the successful end of initializing the workspace - LogInitializationComplete() + // LogInitializationComplete describes the successful end of initializing the workspace. + // Output is different depending on whether the configuration is empty or contains resources. + LogInitializationComplete(emptyDirectory bool) // LogInitializationCompleteCallToAction prompts users about what to do next after initialization. // This is only used if the CLI is being used outside of automation; a human will see the CTA. LogInitializationCompleteCallToAction() @@ -113,9 +114,15 @@ func (v *InitHuman) LogInitializationCompleteCallToAction() { v.print(v.prepareMessage(OutputInitSuccessCLIMessage, params...)) } -func (v *InitHuman) LogInitializationComplete() { +func (v *InitHuman) LogInitializationComplete(emptyDirectory bool) { params := []any{} - v.print(v.prepareMessage(OutputInitSuccessMessage, params...)) + var code InitMessageCode + if emptyDirectory { + code = OutputInitEmptyMessage + } else { + code = OutputInitSuccessMessage + } + v.print(v.prepareMessage(code, params...)) } func (v *InitHuman) LogInitializingStateStoreProviderPlugin(pAddr tfaddr.Provider, cons getproviders.VersionConstraints, storeType string) { @@ -323,12 +330,18 @@ func (v *InitJSON) LogInitializationCompleteCallToAction() { v.Output(OutputInitSuccessCLIMessage, params...) } -func (v *InitJSON) LogInitializationComplete() { +func (v *InitJSON) LogInitializationComplete(emptyDirectory bool) { params := []any{} + var code InitMessageCode + if emptyDirectory { + code = OutputInitEmptyMessage + } else { + code = OutputInitSuccessMessage + } // This was previously logged via Output, so we need to match implementation of that method // to ensure the same JSON log is produced. - v.Output(OutputInitSuccessMessage, params...) + v.Output(code, params...) } // logInitMessage is an internalised version of an old method `LogInitMessage`. diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go index 2ea26f4eb2f5..2a21990a032b 100644 --- a/internal/command/views/init_test.go +++ b/internal/command/views/init_test.go @@ -1100,27 +1100,54 @@ func TestNewInit_LogCloudInitializationComplete_json(t *testing.T) { } func TestNewInit_LogInitializationComplete_json(t *testing.T) { - streams, done := terminal.StreamsForTesting(t) - view := NewView(streams) - initView := NewInit(arguments.ViewJSON, view) + t.Run("empty config", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) - initView.LogInitializationComplete() + empty := true + initView.LogInitializationComplete(empty) - // Assert output - output := done(t) - expectedOutputFields := []string{ - `"@level":"info"`, - `"@message":"Terraform has been successfully initialized!"`, - `"@module":"terraform.ui"`, - //@timestamp is dynamic - `"message_code":"output_init_success_message"`, - `"type":"init_output"`, - } - for _, snippet := range expectedOutputFields { - if !strings.Contains(output.Stdout(), snippet) { - t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Terraform initialized in an empty directory!`, // ... incomplete but sufficient for test + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_empty_message"`, + `"type":"init_output"`, } - } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } + }) + t.Run("non-empty config", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + initView := NewInit(arguments.ViewJSON, view) + + empty := false + initView.LogInitializationComplete(empty) + + // Assert output + output := done(t) + expectedOutputFields := []string{ + `"@level":"info"`, + `"@message":"Terraform has been successfully initialized!"`, + `"@module":"terraform.ui"`, + //@timestamp is dynamic + `"message_code":"output_init_success_message"`, + `"type":"init_output"`, + } + for _, snippet := range expectedOutputFields { + if !strings.Contains(output.Stdout(), snippet) { + t.Fatalf("output didn't include expected snippet:\n expected: %s\n got:\n %s", snippet, output.Stdout()) + } + } + }) } func TestNewInit_LogCloudInitializationCompleteCallToAction_json(t *testing.T) { From 8e1f6493ff15c54fb1f4f6712cbb77b6740464a0 Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Fri, 7 Aug 2026 19:45:10 +0100 Subject: [PATCH 5/5] Make godoc comment more explicit about boolean param --- internal/command/views/init.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/command/views/init.go b/internal/command/views/init.go index 221d9af5d168..54ce725e91fe 100644 --- a/internal/command/views/init.go +++ b/internal/command/views/init.go @@ -32,7 +32,9 @@ type Init interface { LogCloudInitializationCompleteCallToAction() // LogInitializationComplete describes the successful end of initializing the workspace. - // Output is different depending on whether the configuration is empty or contains resources. + // + // Output is different depending on whether the configuration is empty or contains resources. Calling code should report if + // the config is empty via the boolean. LogInitializationComplete(emptyDirectory bool) // LogInitializationCompleteCallToAction prompts users about what to do next after initialization. // This is only used if the CLI is being used outside of automation; a human will see the CTA.