Skip to content

Commit 671b4c8

Browse files
Merge branch 'main' into docs-search-subcommand
2 parents 5650feb + 3621ff8 commit 671b4c8

34 files changed

+573
-209
lines changed

.claude/CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ for name, tc := range tests {
164164
}
165165
```
166166

167+
### Error Handling
168+
169+
- Wrap errors returned across package boundaries with `slackerror.Wrap(err, slackerror.ErrCode)` so they carry a structured error code
170+
- Register new error codes in `internal/slackerror/errors.go`: add a constant and an entry in `ErrorCodeMap`
171+
- Error codes are alphabetically ordered in both the constants block and `ErrorCodeMap`
172+
167173
## Version Management
168174

169175
Versions use semantic versioning with git tags (format: `v*.*.*`).

.github/workflows/dependencies.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
steps:
1616
- name: Collect metadata
1717
id: metadata
18-
uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0
18+
uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v3.0.0
1919
with:
2020
github-token: "${{ secrets.GITHUB_TOKEN }}"
2121
- name: Milestone
@@ -56,7 +56,7 @@ jobs:
5656
ref: main
5757
token: ${{ steps.credentials.outputs.token }}
5858
- name: Install Golang
59-
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
59+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
6060
with:
6161
go-version: "stable"
6262
- name: Get the latest version

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
fetch-depth: 0
2424
persist-credentials: false
2525
- name: Set up Go
26-
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
26+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
2727
with:
2828
go-version: "1.26.1"
2929
- name: Lint
@@ -55,7 +55,7 @@ jobs:
5555
with:
5656
persist-credentials: false
5757
- name: Set up Go
58-
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
58+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
5959
with:
6060
go-version: "1.26.1"
6161
- name: Report health score

cmd/doctor/doctor_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import (
3838
)
3939

4040
func TestDoctorCommand(t *testing.T) {
41-
expectedCLIVersion := version.Get()
41+
expectedCLIVersion := version.Raw()
4242
expectedCredentials := types.SlackAuth{
4343
TeamDomain: "team123",
4444
TeamID: "T123",

cmd/env/list.go

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/slackapi/slack-cli/internal/cmdutil"
2525
"github.com/slackapi/slack-cli/internal/prompts"
2626
"github.com/slackapi/slack-cli/internal/shared"
27+
"github.com/slackapi/slack-cli/internal/slackdotenv"
2728
"github.com/slackapi/slack-cli/internal/slacktrace"
2829
"github.com/slackapi/slack-cli/internal/style"
2930
"github.com/spf13/cobra"
@@ -34,11 +35,13 @@ func NewEnvListCommand(clients *shared.ClientFactory) *cobra.Command {
3435
Use: "list [flags]",
3536
Short: "List all environment variables for the app",
3637
Long: strings.Join([]string{
37-
"List all of the environment variables of an app deployed to Slack managed",
38-
"infrastructure.",
38+
"List environment variables available to the app at runtime.",
3939
"",
40-
"This command is supported for apps deployed to Slack managed infrastructure but",
41-
"other apps can attempt to run the command with the --force flag.",
40+
"Commands that run in the context of a project source environment variables from",
41+
"the \".env\" file. This includes the \"run\" command.",
42+
"",
43+
"The \"deploy\" command gathers environment variables from the \".env\" file as well",
44+
"unless the app is using ROSI features.",
4245
}, "\n"),
4346
Example: style.ExampleCommandsf([]style.ExampleCommand{
4447
{
@@ -58,17 +61,9 @@ func NewEnvListCommand(clients *shared.ClientFactory) *cobra.Command {
5861
return cmd
5962
}
6063

61-
// preRunEnvListCommandFunc determines if the command is supported for a project
62-
// and configures flags
63-
func preRunEnvListCommandFunc(ctx context.Context, clients *shared.ClientFactory) error {
64-
err := cmdutil.IsValidProjectDirectory(clients)
65-
if err != nil {
66-
return err
67-
}
68-
if clients.Config.ForceFlag {
69-
return nil
70-
}
71-
return cmdutil.IsSlackHostedProject(ctx, clients)
64+
// preRunEnvListCommandFunc determines if the command is run in a valid project
65+
func preRunEnvListCommandFunc(_ context.Context, clients *shared.ClientFactory) error {
66+
return cmdutil.IsValidProjectDirectory(clients)
7267
}
7368

7469
// runEnvListCommandFunc outputs environment variables for a selected app
@@ -81,20 +76,34 @@ func runEnvListCommandFunc(
8176
selection, err := appSelectPromptFunc(
8277
ctx,
8378
clients,
84-
prompts.ShowHostedOnly,
79+
prompts.ShowAllEnvironments,
8580
prompts.ShowInstalledAppsOnly,
8681
)
8782
if err != nil {
8883
return err
8984
}
9085

91-
variableNames, err := clients.API().ListVariables(
92-
ctx,
93-
selection.Auth.Token,
94-
selection.App.AppID,
95-
)
96-
if err != nil {
97-
return err
86+
// Gather environment variables for either a ROSI app from the Slack API method
87+
// or read from project files.
88+
var variableNames []string
89+
if !selection.App.IsDev && cmdutil.IsSlackHostedProject(ctx, clients) == nil {
90+
variableNames, err = clients.API().ListVariables(
91+
ctx,
92+
selection.Auth.Token,
93+
selection.App.AppID,
94+
)
95+
if err != nil {
96+
return err
97+
}
98+
} else {
99+
dotEnv, err := slackdotenv.Read(clients.Fs)
100+
if err != nil {
101+
return err
102+
}
103+
variableNames = make([]string, 0, len(dotEnv))
104+
for k := range dotEnv {
105+
variableNames = append(variableNames, k)
106+
}
98107
}
99108

100109
count := len(variableNames)
@@ -112,22 +121,23 @@ func runEnvListCommandFunc(
112121
},
113122
}))
114123

115-
if len(variableNames) <= 0 {
124+
if count <= 0 {
116125
return nil
117126
}
127+
118128
sort.Strings(variableNames)
119-
variableLabel := []string{}
129+
variableLabels := make([]string, 0, count)
120130
for _, v := range variableNames {
121-
variableLabel = append(
122-
variableLabel,
131+
variableLabels = append(
132+
variableLabels,
123133
fmt.Sprintf("%s: %s", v, style.Secondary("***")),
124134
)
125135
}
126136
clients.IO.PrintTrace(ctx, slacktrace.EnvListVariables, variableNames...)
127137
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
128138
Emoji: "evergreen_tree",
129139
Text: "App Environment",
130-
Secondary: variableLabel,
140+
Secondary: variableLabels,
131141
}))
132142

133143
return nil

cmd/env/list_test.go

Lines changed: 88 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -26,95 +26,30 @@ import (
2626
"github.com/slackapi/slack-cli/internal/slackerror"
2727
"github.com/slackapi/slack-cli/internal/slacktrace"
2828
"github.com/slackapi/slack-cli/test/testutil"
29+
"github.com/spf13/afero"
2930
"github.com/spf13/cobra"
3031
"github.com/stretchr/testify/assert"
3132
"github.com/stretchr/testify/mock"
3233
)
3334

3435
func Test_Env_ListCommandPreRun(t *testing.T) {
3536
tests := map[string]struct {
36-
mockFlagForce bool
37-
mockManifestResponse types.SlackYaml
38-
mockManifestError error
39-
mockManifestSource config.ManifestSource
4037
mockWorkingDirectory string
4138
expectedError error
4239
}{
43-
"continues if the application is hosted on slack": {
44-
mockManifestResponse: types.SlackYaml{
45-
AppManifest: types.AppManifest{
46-
Settings: &types.AppSettings{
47-
FunctionRuntime: types.SlackHosted,
48-
},
49-
},
50-
},
51-
mockManifestError: nil,
52-
mockManifestSource: config.ManifestSourceLocal,
53-
mockWorkingDirectory: "/slack/path/to/project",
54-
expectedError: nil,
55-
},
56-
"errors if the application is not hosted on slack": {
57-
mockManifestResponse: types.SlackYaml{
58-
AppManifest: types.AppManifest{
59-
Settings: &types.AppSettings{
60-
FunctionRuntime: types.Remote,
61-
},
62-
},
63-
},
64-
mockManifestError: nil,
65-
mockManifestSource: config.ManifestSourceLocal,
66-
mockWorkingDirectory: "/slack/path/to/project",
67-
expectedError: slackerror.New(slackerror.ErrAppNotHosted),
68-
},
69-
"continues if the force flag is used in a project": {
70-
mockFlagForce: true,
40+
"continues if the command is run in a project": {
7141
mockWorkingDirectory: "/slack/path/to/project",
7242
expectedError: nil,
7343
},
74-
"errors if the project manifest cannot be retrieved": {
75-
mockManifestResponse: types.SlackYaml{},
76-
mockManifestError: slackerror.New(slackerror.ErrSDKHookInvocationFailed),
77-
mockManifestSource: config.ManifestSourceLocal,
78-
mockWorkingDirectory: "/slack/path/to/project",
79-
expectedError: slackerror.New(slackerror.ErrSDKHookInvocationFailed),
80-
},
8144
"errors if the command is not run in a project": {
82-
mockManifestResponse: types.SlackYaml{},
83-
mockManifestError: slackerror.New(slackerror.ErrSDKHookNotFound),
8445
mockWorkingDirectory: "",
8546
expectedError: slackerror.New(slackerror.ErrInvalidAppDirectory),
8647
},
87-
"errors if the manifest source is set to remote": {
88-
mockManifestSource: config.ManifestSourceRemote,
89-
mockWorkingDirectory: "/slack/path/to/project",
90-
expectedError: slackerror.New(slackerror.ErrAppNotHosted),
91-
},
9248
}
9349
for name, tc := range tests {
9450
t.Run(name, func(t *testing.T) {
9551
clientsMock := shared.NewClientsMock()
96-
manifestMock := &app.ManifestMockObject{}
97-
manifestMock.On(
98-
"GetManifestLocal",
99-
mock.Anything,
100-
mock.Anything,
101-
mock.Anything,
102-
).Return(
103-
tc.mockManifestResponse,
104-
tc.mockManifestError,
105-
)
106-
clientsMock.AppClient.Manifest = manifestMock
107-
projectConfigMock := config.NewProjectConfigMock()
108-
projectConfigMock.On(
109-
"GetManifestSource",
110-
mock.Anything,
111-
).Return(
112-
tc.mockManifestSource,
113-
nil,
114-
)
115-
clientsMock.Config.ProjectConfig = projectConfigMock
11652
clients := shared.NewClientFactory(clientsMock.MockClientFactory(), func(cf *shared.ClientFactory) {
117-
cf.Config.ForceFlag = tc.mockFlagForce
11853
cf.SDKConfig.WorkingDirectory = tc.mockWorkingDirectory
11954
})
12055
cmd := NewEnvListCommand(clients)
@@ -129,9 +64,78 @@ func Test_Env_ListCommandPreRun(t *testing.T) {
12964
}
13065

13166
func Test_Env_ListCommand(t *testing.T) {
67+
mockAppSelect := func() {
68+
appSelectMock := prompts.NewAppSelectMock()
69+
appSelectPromptFunc = appSelectMock.AppSelectPrompt
70+
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowAllEnvironments, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{}, nil)
71+
}
72+
13273
testutil.TableTestCommand(t, testutil.CommandTests{
133-
"list variables using arguments": {
74+
"lists variables from the .env file": {
13475
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
76+
mockAppSelect()
77+
err := afero.WriteFile(cf.Fs, ".env", []byte("SECRET_KEY=abc123\nAPI_TOKEN=xyz789\n"), 0644)
78+
assert.NoError(t, err)
79+
},
80+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
81+
cm.IO.AssertCalled(
82+
t,
83+
"PrintTrace",
84+
mock.Anything,
85+
slacktrace.EnvListCount,
86+
[]string{
87+
"2",
88+
},
89+
)
90+
cm.IO.AssertCalled(
91+
t,
92+
"PrintTrace",
93+
mock.Anything,
94+
slacktrace.EnvListVariables,
95+
[]string{
96+
"API_TOKEN",
97+
"SECRET_KEY",
98+
},
99+
)
100+
},
101+
},
102+
"lists no variables when the .env file does not exist": {
103+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
104+
mockAppSelect()
105+
},
106+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
107+
cm.IO.AssertCalled(
108+
t,
109+
"PrintTrace",
110+
mock.Anything,
111+
slacktrace.EnvListCount,
112+
[]string{
113+
"0",
114+
},
115+
)
116+
},
117+
},
118+
"lists no variables when the .env file is empty": {
119+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
120+
mockAppSelect()
121+
err := afero.WriteFile(cf.Fs, ".env", []byte(""), 0644)
122+
assert.NoError(t, err)
123+
},
124+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
125+
cm.IO.AssertCalled(
126+
t,
127+
"PrintTrace",
128+
mock.Anything,
129+
slacktrace.EnvListCount,
130+
[]string{
131+
"0",
132+
},
133+
)
134+
},
135+
},
136+
"lists hosted variables using the API": {
137+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
138+
mockAppSelect()
135139
cm.API.On(
136140
"ListVariables",
137141
mock.Anything,
@@ -145,9 +149,22 @@ func Test_Env_ListCommand(t *testing.T) {
145149
},
146150
nil,
147151
)
148-
appSelectMock := prompts.NewAppSelectMock()
149-
appSelectPromptFunc = appSelectMock.AppSelectPrompt
150-
appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowHostedOnly, prompts.ShowInstalledAppsOnly).Return(prompts.SelectedApp{}, nil)
152+
manifestMock := &app.ManifestMockObject{}
153+
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(
154+
types.SlackYaml{
155+
AppManifest: types.AppManifest{
156+
Settings: &types.AppSettings{
157+
FunctionRuntime: types.SlackHosted,
158+
},
159+
},
160+
},
161+
nil,
162+
)
163+
cm.AppClient.Manifest = manifestMock
164+
projectConfigMock := config.NewProjectConfigMock()
165+
projectConfigMock.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
166+
cm.Config.ProjectConfig = projectConfigMock
167+
cf.SDKConfig.WorkingDirectory = "/slack/path/to/project"
151168
},
152169
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
153170
cm.API.AssertCalled(

cmd/platform/deploy.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ func deployHook(ctx context.Context, clients *shared.ClientFactory) error {
181181
// so we instantiate the default here.
182182
shell := hooks.HookExecutorDefaultProtocol{
183183
IO: clients.IO,
184+
Fs: clients.Fs,
184185
}
185186
if _, err := shell.Execute(ctx, hookExecOpts); err != nil {
186187
return err

cmd/platform/deploy_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ func TestDeployCommand_DeployHook(t *testing.T) {
279279

280280
clients := shared.NewClientFactory(clientsMock.MockClientFactory(), func(clients *shared.ClientFactory) {
281281
clients.SDKConfig = sdkConfigMock
282-
clients.HookExecutor = hooks.GetHookExecutor(clientsMock.IO, sdkConfigMock)
282+
clients.HookExecutor = hooks.GetHookExecutor(clientsMock.IO, clients.Fs, sdkConfigMock)
283283
})
284284
cmd := NewDeployCommand(clients)
285285
cmd.PreRunE = func(cmd *cobra.Command, args []string) error { return nil }

0 commit comments

Comments
 (0)