Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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",
Comment thread
zimeg marked this conversation as resolved.
Outdated
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