Skip to content
Open
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
5 changes: 5 additions & 0 deletions cmd/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {
`Explore more: {{LinkText "https://docs.slack.dev/tools/slack-cli/guides/using-environment-variables-with-the-slack-cli"}}`,
}, "\n"),
Example: style.ExampleCommandsf([]style.ExampleCommand{
{
Meaning: "Initialize environment variables from template placeholders",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Minor suggestion to clarify that we're initializing from a file (plus it's shorter, haha).

Suggested change
Meaning: "Initialize environment variables from template placeholders",
Meaning: "Initialize environment variables from a template file",

Command: "env init",
},
{
Meaning: "Set an environment variable",
Command: "env set MAGIC_PASSWORD abracadbra",
Expand All @@ -67,6 +71,7 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {
}

// Add child commands
cmd.AddCommand(NewEnvInitCommand(clients))
cmd.AddCommand(NewEnvSetCommand(clients))
cmd.AddCommand(NewEnvListCommand(clients))
cmd.AddCommand(NewEnvUnsetCommand(clients))
Expand Down
1 change: 1 addition & 0 deletions cmd/env/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func Test_Env_Command(t *testing.T) {
testutil.TableTestCommand(t, testutil.CommandTests{
"shows the help page without commands or arguments or flags": {
ExpectedStdoutOutputs: []string{
"Initialize environment variables from placeholders",
"Set an environment variable",
"List all environment variables",
"Unset an environment variable",
Expand Down
131 changes: 131 additions & 0 deletions cmd/env/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2022-2026 Salesforce, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package env

import (
"context"
"fmt"
"strings"

"github.com/slackapi/slack-cli/internal/cmdutil"
"github.com/slackapi/slack-cli/internal/prompts"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/slackdotenv"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/slacktrace"
"github.com/slackapi/slack-cli/internal/style"
"github.com/spf13/cobra"
)

func NewEnvInitCommand(clients *shared.ClientFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Initialize environment variables from placeholders",
Long: strings.Join([]string{
`Initialize the project ".env" file by copying from a template placeholder file.`,
"",
`Copies content from either the ".env.sample" or ".env.example" file to the`,
`project ".env" file if those project environment variables don't already exist.`,
"",
fmt.Sprintf("Apps using ROSI features should set environment variables with %s.", style.Commandf("env set", false)),
}, "\n"),
Example: style.ExampleCommandsf([]style.ExampleCommand{
{
Meaning: "Initialize environment variables from template placeholders",
Command: "env init",
},
}),
PreRunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
return preRunEnvInitCommandFunc(ctx, clients)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runEnvInitCommandFunc(clients, cmd)
},
}

return cmd
}

// preRunEnvInitCommandFunc determines if the command is run in a valid project
func preRunEnvInitCommandFunc(_ context.Context, clients *shared.ClientFactory) error {
return cmdutil.IsValidProjectDirectory(clients)
}

// runEnvInitCommandFunc copies a sample .env file to .env
func runEnvInitCommandFunc(clients *shared.ClientFactory, cmd *cobra.Command) error {
ctx := cmd.Context()

// Hosted apps manage environment variables through the API, not .env files.
hosted := isHostedRuntime(ctx, clients)
if hosted {
selection, err := appSelectPromptFunc(ctx, clients, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly)
if err != nil {
return err
}
if !selection.App.IsDev {
clients.IO.PrintTrace(ctx, slacktrace.EnvInitSuccess)
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "evergreen_tree",
Text: "Environment Initialize",
Secondary: []string{
fmt.Sprintf("Set environment variables for apps using ROSI features with %s", style.Commandf("env set", false)),
},
}))
return nil
}
}

source, err := slackdotenv.Init(clients.Fs)
if err != nil {
switch slackerror.ToSlackError(err).Code {
case slackerror.ErrDotEnvFileAlreadyExists:
clients.IO.PrintTrace(ctx, slacktrace.EnvInitSuccess)
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "evergreen_tree",
Text: "Environment Initialize",
Secondary: []string{
`A project ".env" file already exists and was left unchanged`,
fmt.Sprintf("Set environment variables with %s", style.Commandf("env set", false)),
},
}))
return nil
case slackerror.ErrDotEnvPlaceholderNotFound:
clients.IO.PrintTrace(ctx, slacktrace.EnvInitSuccess)
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "evergreen_tree",
Text: "Environment Initialize",
Secondary: []string{
`No template placeholder was found for environment variables in this project`,
fmt.Sprintf("Set environment variables with %s", style.Commandf("env set", false)),
},
}))
return nil
default:
return err
}
}

clients.IO.PrintTrace(ctx, slacktrace.EnvInitSuccess)
clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{
Emoji: "evergreen_tree",
Text: "Environment Initialize",
Secondary: []string{
fmt.Sprintf(`Placeholders were copied from "%s" to a project ".env" file`, source),
`This new ".env" file shouldn't be added to version control`,
},
}))
return nil
}
205 changes: 205 additions & 0 deletions cmd/env/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Copyright 2022-2026 Salesforce, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package env

import (
"context"
"testing"

"github.com/slackapi/slack-cli/internal/app"
"github.com/slackapi/slack-cli/internal/hooks"
"github.com/slackapi/slack-cli/internal/prompts"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/shared/types"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/slacktrace"
"github.com/slackapi/slack-cli/test/testutil"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func Test_Env_InitCommandPreRun(t *testing.T) {
tests := map[string]struct {
mockWorkingDirectory string
expectedError error
}{
"continues if the command is run in a project": {
mockWorkingDirectory: "/slack/path/to/project",
expectedError: nil,
},
"errors if the command is not run in a project": {
mockWorkingDirectory: "",
expectedError: slackerror.New(slackerror.ErrInvalidAppDirectory),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
clientsMock := shared.NewClientsMock()
clients := shared.NewClientFactory(clientsMock.MockClientFactory(), func(cf *shared.ClientFactory) {
cf.SDKConfig.WorkingDirectory = tc.mockWorkingDirectory
})
cmd := NewEnvInitCommand(clients)
err := cmd.PreRunE(cmd, nil)
if tc.expectedError != nil {
assert.Equal(t, slackerror.ToSlackError(tc.expectedError).Code, slackerror.ToSlackError(err).Code)
} else {
assert.NoError(t, err)
}
})
}
}

func Test_Env_InitCommand(t *testing.T) {
testutil.TableTestCommand(t, testutil.CommandTests{
"copies .env.sample to .env": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cm.AddDefaultMocks()
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{}, slackerror.New(slackerror.ErrSDKHookNotFound))
cm.AppClient.Manifest = manifestMock
err := afero.WriteFile(cf.Fs, ".env.sample", []byte("SECRET=placeholder\n"), 0600)
assert.NoError(t, err)
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.EnvInitSuccess, mock.Anything)
content, err := afero.ReadFile(cm.Fs, ".env")
assert.NoError(t, err)
assert.Equal(t, "SECRET=placeholder\n", string(content))
},
},
"copies .env.example to .env": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cm.AddDefaultMocks()
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{}, slackerror.New(slackerror.ErrSDKHookNotFound))
cm.AppClient.Manifest = manifestMock
err := afero.WriteFile(cf.Fs, ".env.example", []byte("TOKEN=example\n"), 0600)
assert.NoError(t, err)
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.EnvInitSuccess, mock.Anything)
content, err := afero.ReadFile(cm.Fs, ".env")
assert.NoError(t, err)
assert.Equal(t, "TOKEN=example\n", string(content))
},
},
"prints message when .env already exists": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cm.AddDefaultMocks()
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{}, slackerror.New(slackerror.ErrSDKHookNotFound))
cm.AppClient.Manifest = manifestMock
err := afero.WriteFile(cf.Fs, ".env", []byte("EXISTING=value\n"), 0600)
assert.NoError(t, err)
err = afero.WriteFile(cf.Fs, ".env.sample", []byte("NEW=value\n"), 0600)
assert.NoError(t, err)
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.EnvInitSuccess, mock.Anything)
content, err := afero.ReadFile(cm.Fs, ".env")
assert.NoError(t, err)
assert.Equal(t, "EXISTING=value\n", string(content))
},
},
"prints message when no sample file exists": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cm.AddDefaultMocks()
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{}, slackerror.New(slackerror.ErrSDKHookNotFound))
cm.AppClient.Manifest = manifestMock
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.EnvInitSuccess, mock.Anything)
},
},
"prints ROSI message for hosted non-dev app": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cf.SDKConfig = hooks.NewSDKConfigMock()
cm.AddDefaultMocks()
_ = cf.AppClient().SaveDeployed(ctx, mockApp)

appSelectMock := prompts.NewAppSelectMock()
appSelectPromptFunc = appSelectMock.AppSelectPrompt
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{Auth: mockAuth, App: mockApp}, nil)

manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
types.SlackYaml{
AppManifest: types.AppManifest{
Settings: &types.AppSettings{
FunctionRuntime: types.SlackHosted,
},
},
},
nil,
)
cm.AppClient.Manifest = manifestMock
},
ExpectedStdoutOutputs: []string{
"ROSI features",
"env set",
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.EnvInitSuccess, mock.Anything)
_, err := afero.ReadFile(cm.Fs, ".env")
assert.Error(t, err)
},
},
"copies placeholder for hosted dev app": {
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cf.SDKConfig = hooks.NewSDKConfigMock()
cm.AddDefaultMocks()

devApp := types.App{
TeamID: "T1",
TeamDomain: "team1",
AppID: "A0123456789",
IsDev: true,
}
appSelectMock := prompts.NewAppSelectMock()
appSelectPromptFunc = appSelectMock.AppSelectPrompt
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{Auth: mockAuth, App: devApp}, nil)

manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
types.SlackYaml{
AppManifest: types.AppManifest{
Settings: &types.AppSettings{
FunctionRuntime: types.SlackHosted,
},
},
},
nil,
)
cm.AppClient.Manifest = manifestMock

err := afero.WriteFile(cf.Fs, ".env.sample", []byte("SECRET=placeholder\n"), 0600)
assert.NoError(t, err)
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.EnvInitSuccess, mock.Anything)
content, err := afero.ReadFile(cm.Fs, ".env")
assert.NoError(t, err)
assert.Equal(t, "SECRET=placeholder\n", string(content))
},
},
}, func(cf *shared.ClientFactory) *cobra.Command {
cmd := NewEnvInitCommand(cf)
cmd.PreRunE = func(cmd *cobra.Command, args []string) error { return nil }
return cmd
})
}
Loading
Loading