Skip to content

Commit 24c1d2c

Browse files
authored
feat: 'env' command no longer prompts for an app for Bolt projects (#456)
1 parent 6958c8a commit 24c1d2c

File tree

8 files changed

+178
-66
lines changed

8 files changed

+178
-66
lines changed

cmd/env/add.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,44 +85,49 @@ func preRunEnvAddCommandFunc(ctx context.Context, clients *shared.ClientFactory,
8585
func runEnvAddCommandFunc(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error {
8686
ctx := cmd.Context()
8787

88-
// Get the workspace from the flag or prompt
89-
selection, err := appSelectPromptFunc(ctx, clients, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly)
90-
if err != nil {
91-
return err
88+
// Hosted apps require selecting an app before gathering variable inputs.
89+
hosted := isHostedRuntime(ctx, clients)
90+
var selection prompts.SelectedApp
91+
if hosted {
92+
s, err := appSelectPromptFunc(ctx, clients, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly)
93+
if err != nil {
94+
return err
95+
}
96+
selection = s
9297
}
9398

9499
// Get the variable name from the args or prompt
95-
var variableName string
100+
variableName := ""
96101
if len(args) < 1 {
97-
variableName, err = clients.IO.InputPrompt(ctx, "Variable name", iostreams.InputPromptConfig{
102+
name, err := clients.IO.InputPrompt(ctx, "Variable name", iostreams.InputPromptConfig{
98103
Required: false,
99104
})
100105
if err != nil {
101106
return err
102107
}
108+
variableName = name
103109
} else {
104110
variableName = args[0]
105111
}
106112

107113
// Get the variable value from the args or prompt
108-
var variableValue string
114+
variableValue := ""
109115
if len(args) < 2 {
110116
response, err := clients.IO.PasswordPrompt(ctx, "Variable value", iostreams.PasswordPromptConfig{
111117
Flag: clients.Config.Flags.Lookup("value"),
112118
})
113119
if err != nil {
114120
return err
115-
} else {
116-
variableValue = response.Value
117121
}
122+
variableValue = response.Value
118123
} else {
119124
variableValue = args[1]
120125
}
121126

122127
// Add the environment variable using either the Slack API method or the
123128
// project ".env" file depending on the app hosting.
124-
if !selection.App.IsDev && cmdutil.IsSlackHostedProject(ctx, clients) == nil {
125-
err = clients.API().AddVariable(
129+
if hosted && !selection.App.IsDev {
130+
err := clients.API().AddVariable(
126131
ctx,
127132
selection.Auth.Token,
128133
selection.App.AppID,

cmd/env/add_test.go

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -204,33 +204,16 @@ func Test_Env_AddCommand(t *testing.T) {
204204
)
205205
},
206206
},
207-
"add a numeric variable using prompts to the .env file": {
208-
CmdArgs: []string{},
207+
"add a numeric variable to the .env file for remote runtime": {
208+
CmdArgs: []string{"PORT", "3000"},
209209
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
210210
setupEnvAddDotenvMocks(ctx, cm, cf)
211-
cm.IO.On(
212-
"InputPrompt",
213-
mock.Anything,
214-
"Variable name",
215-
mock.Anything,
216-
).Return(
217-
"PORT",
218-
nil,
219-
)
220-
cm.IO.On(
221-
"PasswordPrompt",
222-
mock.Anything,
223-
"Variable value",
224-
iostreams.MatchPromptConfig(iostreams.PasswordPromptConfig{
225-
Flag: cm.Config.Flags.Lookup("value"),
226-
}),
227-
).Return(
228-
iostreams.PasswordPromptResponse{
229-
Prompt: true,
230-
Value: "3000",
231-
},
211+
manifestMock := &app.ManifestMockObject{}
212+
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
213+
types.SlackYaml{AppManifest: types.AppManifest{Settings: &types.AppSettings{FunctionRuntime: types.Remote}}},
232214
nil,
233215
)
216+
cm.AppClient.Manifest = manifestMock
234217
},
235218
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
236219
cm.API.AssertNotCalled(t, "AddVariable")
@@ -239,18 +222,39 @@ func Test_Env_AddCommand(t *testing.T) {
239222
assert.Equal(t, "PORT=3000\n", string(content))
240223
},
241224
},
242-
"add a variable to the .env file for non-hosted app": {
225+
"add a variable to the .env file when no runtime is set": {
243226
CmdArgs: []string{"NEW_VAR", "new_value"},
244227
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
245228
setupEnvAddDotenvMocks(ctx, cm, cf)
229+
manifestMock := &app.ManifestMockObject{}
230+
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{}, nil)
231+
cm.AppClient.Manifest = manifestMock
246232
err := afero.WriteFile(cf.Fs, ".env", []byte("# Config\nEXISTING=value\n"), 0600)
247233
assert.NoError(t, err)
248234
},
249235
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
250236
cm.API.AssertNotCalled(t, "AddVariable")
251237
content, err := afero.ReadFile(cm.Fs, ".env")
252238
assert.NoError(t, err)
253-
assert.Equal(t, "# Config\nEXISTING=value\nNEW_VAR=\"new_value\"\n", string(content))
239+
assert.Equal(t, "# Config\nEXISTING=value\n"+`NEW_VAR="new_value"`+"\n", string(content))
240+
},
241+
},
242+
"add a variable to the .env file when manifest fetch errors": {
243+
CmdArgs: []string{"API_KEY", "sk-1234"},
244+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
245+
setupEnvAddDotenvMocks(ctx, cm, cf)
246+
manifestMock := &app.ManifestMockObject{}
247+
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
248+
types.SlackYaml{},
249+
slackerror.New(slackerror.ErrSDKHookNotFound),
250+
)
251+
cm.AppClient.Manifest = manifestMock
252+
},
253+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
254+
cm.API.AssertNotCalled(t, "AddVariable")
255+
content, err := afero.ReadFile(cm.Fs, ".env")
256+
assert.NoError(t, err)
257+
assert.Equal(t, `API_KEY="sk-1234"`+"\n", string(content))
254258
},
255259
},
256260
}, func(cf *shared.ClientFactory) *cobra.Command {
@@ -292,20 +296,11 @@ func setupEnvAddHostedMocks(ctx context.Context, cm *shared.ClientsMock, cf *sha
292296
cf.SDKConfig.WorkingDirectory = "/slack/path/to/project"
293297
}
294298

295-
// setupEnvAddDotenvMocks prepares common mocks for non-hosted (dotenv) app tests
299+
// setupEnvAddDotenvMocks prepares common mocks for non-hosted (dotenv) app tests.
300+
// Callers must set their own manifest mock on cm.AppClient.Manifest.
296301
func setupEnvAddDotenvMocks(_ context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
297302
cf.SDKConfig = hooks.NewSDKConfigMock()
298303
cm.AddDefaultMocks()
299304

300-
mockDevApp := types.App{
301-
TeamID: "T1",
302-
TeamDomain: "team1",
303-
AppID: "A0123456789",
304-
IsDev: true,
305-
}
306-
appSelectMock := prompts.NewAppSelectMock()
307-
appSelectPromptFunc = appSelectMock.AppSelectPrompt
308-
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{Auth: mockAuth, App: mockDevApp}, nil)
309-
310305
cm.Config.Flags.String("value", "", "mock value flag")
311306
}

cmd/env/env.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package env
1616

1717
import (
18+
"context"
1819
"strings"
1920

2021
"github.com/slackapi/slack-cli/internal/prompts"
@@ -72,3 +73,16 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command {
7273

7374
return cmd
7475
}
76+
77+
// isHostedRuntime returns true if the local manifest is for an app that uses
78+
// the Deno Slack SDK function runtime.
79+
//
80+
// It defaults to false when the manifest cannot be fetched, which directs the
81+
// command to use the project ".env" file. Otherwise the API is used.
82+
func isHostedRuntime(ctx context.Context, clients *shared.ClientFactory) bool {
83+
manifest, err := clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor)
84+
if err != nil {
85+
return false
86+
}
87+
return manifest.IsFunctionRuntimeSlackHosted() || manifest.IsFunctionRuntimeLocal()
88+
}

cmd/env/env_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ package env
1717
import (
1818
"testing"
1919

20+
"github.com/slackapi/slack-cli/internal/app"
2021
"github.com/slackapi/slack-cli/internal/shared"
22+
"github.com/slackapi/slack-cli/internal/shared/types"
23+
"github.com/slackapi/slack-cli/internal/slackcontext"
24+
"github.com/slackapi/slack-cli/internal/slackerror"
2125
"github.com/slackapi/slack-cli/test/testutil"
2226
"github.com/spf13/cobra"
27+
"github.com/stretchr/testify/assert"
28+
"github.com/stretchr/testify/mock"
2329
)
2430

2531
func Test_Env_Command(t *testing.T) {
@@ -36,3 +42,65 @@ func Test_Env_Command(t *testing.T) {
3642
return cmd
3743
})
3844
}
45+
46+
func Test_isHostedRuntime(t *testing.T) {
47+
tests := map[string]struct {
48+
mockManifest types.SlackYaml
49+
mockError error
50+
expected bool
51+
}{
52+
"returns true for slack hosted runtime": {
53+
mockManifest: types.SlackYaml{
54+
AppManifest: types.AppManifest{
55+
Settings: &types.AppSettings{
56+
FunctionRuntime: types.SlackHosted,
57+
},
58+
},
59+
},
60+
expected: true,
61+
},
62+
"returns true for local runtime": {
63+
mockManifest: types.SlackYaml{
64+
AppManifest: types.AppManifest{
65+
Settings: &types.AppSettings{
66+
FunctionRuntime: types.LocallyRun,
67+
},
68+
},
69+
},
70+
expected: true,
71+
},
72+
"returns false for remote runtime": {
73+
mockManifest: types.SlackYaml{
74+
AppManifest: types.AppManifest{
75+
Settings: &types.AppSettings{
76+
FunctionRuntime: types.Remote,
77+
},
78+
},
79+
},
80+
expected: false,
81+
},
82+
"returns false for empty runtime": {
83+
mockManifest: types.SlackYaml{
84+
AppManifest: types.AppManifest{
85+
Settings: &types.AppSettings{},
86+
},
87+
},
88+
expected: false,
89+
},
90+
"returns false when manifest fetch fails": {
91+
mockError: slackerror.New(slackerror.ErrSDKHookInvocationFailed),
92+
expected: false,
93+
},
94+
}
95+
for name, tc := range tests {
96+
t.Run(name, func(t *testing.T) {
97+
ctx := slackcontext.MockContext(t.Context())
98+
clientsMock := shared.NewClientsMock()
99+
manifestMock := &app.ManifestMockObject{}
100+
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(tc.mockManifest, tc.mockError)
101+
clientsMock.AppClient.Manifest = manifestMock
102+
clients := shared.NewClientFactory(clientsMock.MockClientFactory())
103+
assert.Equal(t, tc.expected, isHostedRuntime(ctx, clients))
104+
})
105+
}
106+
}

cmd/env/list.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,27 @@ func runEnvListCommandFunc(
7373
) error {
7474
ctx := cmd.Context()
7575

76-
selection, err := appSelectPromptFunc(
77-
ctx,
78-
clients,
79-
prompts.ShowAllEnvironments,
80-
prompts.ShowInstalledAppsOnly,
81-
)
82-
if err != nil {
83-
return err
76+
// Hosted apps require selecting an app before gathering variables.
77+
hosted := isHostedRuntime(ctx, clients)
78+
var selection prompts.SelectedApp
79+
if hosted {
80+
var err error
81+
selection, err = appSelectPromptFunc(
82+
ctx,
83+
clients,
84+
prompts.ShowAllEnvironments,
85+
prompts.ShowInstalledAppsOnly,
86+
)
87+
if err != nil {
88+
return err
89+
}
8490
}
8591

86-
// Gather environment variables for either a ROSI app from the Slack API method
92+
// Gather environment variables from the Slack API for deployed hosted apps
8793
// or read from project files.
8894
var variableNames []string
89-
if !selection.App.IsDev && cmdutil.IsSlackHostedProject(ctx, clients) == nil {
95+
if hosted && !selection.App.IsDev {
96+
var err error
9097
variableNames, err = clients.API().ListVariables(
9198
ctx,
9299
selection.Auth.Token,

cmd/env/list_test.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"testing"
2020

2121
"github.com/slackapi/slack-cli/internal/app"
22-
"github.com/slackapi/slack-cli/internal/config"
2322
"github.com/slackapi/slack-cli/internal/prompts"
2423
"github.com/slackapi/slack-cli/internal/shared"
2524
"github.com/slackapi/slack-cli/internal/shared/types"
@@ -71,9 +70,14 @@ func Test_Env_ListCommand(t *testing.T) {
7170
}
7271

7372
testutil.TableTestCommand(t, testutil.CommandTests{
74-
"lists variables from the .env file": {
73+
"lists variables from the .env file for remote runtime": {
7574
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
76-
mockAppSelect()
75+
manifestMock := &app.ManifestMockObject{}
76+
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
77+
types.SlackYaml{AppManifest: types.AppManifest{Settings: &types.AppSettings{FunctionRuntime: types.Remote}}},
78+
nil,
79+
)
80+
cm.AppClient.Manifest = manifestMock
7781
err := afero.WriteFile(cf.Fs, ".env", []byte("SECRET_KEY=abc123\nAPI_TOKEN=xyz789\n"), 0644)
7882
assert.NoError(t, err)
7983
},
@@ -101,7 +105,9 @@ func Test_Env_ListCommand(t *testing.T) {
101105
},
102106
"lists no variables when the .env file does not exist": {
103107
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
104-
mockAppSelect()
108+
manifestMock := &app.ManifestMockObject{}
109+
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{}, nil)
110+
cm.AppClient.Manifest = manifestMock
105111
},
106112
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
107113
cm.IO.AssertCalled(
@@ -117,7 +123,12 @@ func Test_Env_ListCommand(t *testing.T) {
117123
},
118124
"lists no variables when the .env file is empty": {
119125
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
120-
mockAppSelect()
126+
manifestMock := &app.ManifestMockObject{}
127+
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
128+
types.SlackYaml{},
129+
slackerror.New(slackerror.ErrSDKHookNotFound),
130+
)
131+
cm.AppClient.Manifest = manifestMock
121132
err := afero.WriteFile(cf.Fs, ".env", []byte(""), 0644)
122133
assert.NoError(t, err)
123134
},
@@ -161,9 +172,6 @@ func Test_Env_ListCommand(t *testing.T) {
161172
nil,
162173
)
163174
cm.AppClient.Manifest = manifestMock
164-
projectConfigMock := config.NewProjectConfigMock()
165-
projectConfigMock.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
166-
cm.Config.ProjectConfig = projectConfigMock
167175
cf.SDKConfig.WorkingDirectory = "/slack/path/to/project"
168176
},
169177
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {

internal/shared/types/app_manifest.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,12 @@ func (manifest *AppManifest) FunctionRuntime() FunctionRuntime {
321321
return manifest.Settings.FunctionRuntime
322322
}
323323

324+
// IsFunctionRuntimeLocal returns true when the function runtime setting
325+
// is local
326+
func (manifest *AppManifest) IsFunctionRuntimeLocal() bool {
327+
return manifest.Settings != nil && manifest.Settings.FunctionRuntime == LocallyRun
328+
}
329+
324330
// IsFunctionRuntimeSlackHosted returns true when the function runtime setting
325331
// is slack hosted
326332
func (manifest *AppManifest) IsFunctionRuntimeSlackHosted() bool {

0 commit comments

Comments
 (0)