Skip to content

Commit 3177aa1

Browse files
committed
feat: fetch remote manifest after create with app ID
1 parent 315c9bb commit 3177aa1

4 files changed

Lines changed: 143 additions & 11 deletions

File tree

cmd/app/link.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func LinkCommandRunE(ctx context.Context, clients *shared.ClientFactory, app *ty
9797
// Add empty line between executed command and first output
9898
clients.IO.PrintInfo(ctx, false, "")
9999

100-
err = LinkExistingApp(ctx, clients, app, false)
100+
_, err = LinkExistingApp(ctx, clients, app, false)
101101
if err != nil {
102102
return err
103103
}
@@ -130,7 +130,7 @@ func LinkAppHeaderSection(ctx context.Context, clients *shared.ClientFactory, sh
130130
// When shouldConfirm is true, a confirmation prompt will ask the user if they want to
131131
// link an existing app and additional information is included in the header.
132132
// The shouldConfirm option is encouraged for third-party callers.
133-
func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *types.App, shouldConfirm bool) (err error) {
133+
func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *types.App, shouldConfirm bool) (_ *types.SlackAuth, err error) {
134134
// Header section
135135
LinkAppHeaderSection(ctx, clients, shouldConfirm)
136136

@@ -139,21 +139,21 @@ func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *ty
139139
proceed, err := clients.IO.ConfirmPrompt(ctx, LinkAppConfirmPromptText, true)
140140
if err != nil {
141141
clients.IO.PrintDebug(ctx, "Error prompting to add an existing app: %s", err)
142-
return err
142+
return nil, err
143143
}
144144

145145
// Add newline to match the trailing newline inserted from the footer section
146146
clients.IO.PrintInfo(ctx, false, "")
147147

148148
if !proceed {
149-
return nil
149+
return nil, nil
150150
}
151151
}
152152

153153
// App Manifest section
154154
manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
155155
if err != nil {
156-
return err
156+
return nil, err
157157
}
158158

159159
configPath := filepath.Join(config.ProjectConfigDirName, config.ProjectConfigJSONFilename)
@@ -170,26 +170,26 @@ func LinkExistingApp(ctx context.Context, clients *shared.ClientFactory, app *ty
170170
var auth *types.SlackAuth
171171
*app, auth, err = promptExistingApp(ctx, clients)
172172
if err != nil {
173-
return err
173+
return nil, err
174174
}
175175

176176
appIDs := []string{app.AppID}
177177
_, err = clients.API().GetAppStatus(ctx, auth.Token, appIDs, app.TeamID)
178178
if err != nil {
179-
return err
179+
return nil, err
180180
}
181181

182182
// Save the app to the project
183183
err = saveAppToJSON(ctx, clients, *app)
184184
if err != nil {
185185
clients.IO.PrintDebug(ctx, "Error saving app to file when linking existing app: %s", err)
186-
return err
186+
return nil, err
187187
}
188188

189189
// Footer section
190190
LinkAppFooterSection(ctx, clients, app)
191191

192-
return nil
192+
return auth, nil
193193
}
194194

195195
// LinkAppFooterSection displays the details of app that was added to the project.

cmd/project/create.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package project
1616

1717
import (
1818
"context"
19+
"encoding/json"
1920
"fmt"
2021
"math/rand"
2122
"os"
@@ -31,6 +32,7 @@ import (
3132
"github.com/slackapi/slack-cli/internal/slackerror"
3233
"github.com/slackapi/slack-cli/internal/slacktrace"
3334
"github.com/slackapi/slack-cli/internal/style"
35+
"github.com/spf13/afero"
3436
"github.com/spf13/cobra"
3537
)
3638

@@ -211,11 +213,26 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
211213
if err := os.Chdir(absProjectPath); err != nil {
212214
return slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
213215
}
214-
linkErr := app.LinkExistingApp(ctx, clients, &types.App{}, false)
216+
217+
linkedApp := &types.App{}
218+
auth, linkErr := app.LinkExistingApp(ctx, clients, linkedApp, false)
215219
_ = os.Chdir(originalDir)
216220
if linkErr != nil {
217221
return linkErr
218222
}
223+
224+
if auth != nil && linkedApp.AppID != "" {
225+
fetchErr := fetchAndWriteRemoteManifest(ctx, clients, auth.Token, linkedApp.AppID, absProjectPath)
226+
if fetchErr != nil {
227+
clients.IO.PrintWarning(ctx, "%s", style.Sectionf(style.TextSection{
228+
Text: "Could not fetch the remote app manifest",
229+
Secondary: []string{
230+
fetchErr.Error(),
231+
"The template manifest was kept unchanged",
232+
},
233+
}))
234+
}
235+
}
219236
}
220237

221238
printCreateSuccess(ctx, clients, appDirPath)
@@ -273,6 +290,21 @@ func printCreateSuccess(ctx context.Context, clients *shared.ClientFactory, appP
273290
clients.IO.PrintTrace(ctx, slacktrace.CreateSuccess)
274291
}
275292

293+
// fetchAndWriteRemoteManifest fetches the app manifest from remote settings and writes it to the project.
294+
func fetchAndWriteRemoteManifest(ctx context.Context, clients *shared.ClientFactory, token, appID, projectPath string) error {
295+
slackYaml, err := clients.AppClient().Manifest.GetManifestRemote(ctx, token, appID)
296+
if err != nil {
297+
return err
298+
}
299+
data, err := json.MarshalIndent(slackYaml.AppManifest, "", " ")
300+
if err != nil {
301+
return err
302+
}
303+
data = append(data, '\n')
304+
manifestPath := filepath.Join(projectPath, "manifest.json")
305+
return afero.WriteFile(clients.Fs, manifestPath, data, 0644)
306+
}
307+
276308
// generateRandomAppName will create a random app name based on two words and a number
277309
func generateRandomAppName() string {
278310
rand.New(rand.NewSource(time.Now().UnixNano()))

cmd/project/create_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ import (
1818
"context"
1919
"testing"
2020

21+
"github.com/slackapi/slack-cli/internal/api"
22+
internalApp "github.com/slackapi/slack-cli/internal/app"
2123
"github.com/slackapi/slack-cli/internal/config"
2224
"github.com/slackapi/slack-cli/internal/iostreams"
2325
"github.com/slackapi/slack-cli/internal/pkg/create"
2426
"github.com/slackapi/slack-cli/internal/shared"
27+
"github.com/slackapi/slack-cli/internal/shared/types"
2528
"github.com/slackapi/slack-cli/internal/slackerror"
2629
"github.com/slackapi/slack-cli/test/testutil"
30+
"github.com/spf13/afero"
2731
"github.com/spf13/cobra"
2832
"github.com/stretchr/testify/assert"
2933
"github.com/stretchr/testify/mock"
@@ -884,3 +888,99 @@ func TestCreateCommand_AppFlag(t *testing.T) {
884888
return NewCreateCommand(cf)
885889
})
886890
}
891+
892+
func TestCreateCommand_AppFlag_FetchesRemoteManifest(t *testing.T) {
893+
var createClientMock *CreateClientMock
894+
895+
mockAuth := types.SlackAuth{
896+
Token: "xoxp-test-token",
897+
TeamDomain: "test-team",
898+
TeamID: "T001",
899+
UserID: "U001",
900+
}
901+
mockManifest := types.SlackYaml{
902+
AppManifest: types.AppManifest{
903+
DisplayInformation: types.DisplayInformation{
904+
Name: "My Remote App",
905+
Description: "An app from remote settings",
906+
},
907+
},
908+
}
909+
910+
setupAppFlagMocks := func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) string {
911+
projectDir := t.TempDir()
912+
createClientMock = new(CreateClientMock)
913+
createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(projectDir, nil)
914+
CreateFunc = createClientMock.Create
915+
916+
// Getwd is called to save original dir, then GetProjectDirPath calls it inside LinkExistingApp.
917+
// After os.Chdir, the second call should return the project dir.
918+
cm.Os.On("Getwd").Return(projectDir, nil)
919+
920+
// Set up .slack/hooks.json so GetProjectDirPath validates the project
921+
err := cm.Fs.MkdirAll(projectDir+"/.slack", 0755)
922+
require.NoError(t, err)
923+
err = afero.WriteFile(cm.Fs, projectDir+"/.slack/hooks.json", []byte("{}"), 0644)
924+
require.NoError(t, err)
925+
926+
// Template selection returns via flag
927+
cm.IO.On("SelectPrompt", mock.Anything, "Select a category:", mock.Anything, mock.Anything).
928+
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil)
929+
930+
// Link prompts
931+
cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{mockAuth}, nil)
932+
cm.IO.On("SelectPrompt", mock.Anything, "Select the existing app team", mock.Anything, mock.Anything, mock.Anything).
933+
Return(iostreams.SelectPromptResponse{Prompt: true, Index: 0, Option: mockAuth.TeamDomain}, nil)
934+
cm.IO.On("SelectPrompt", mock.Anything, "Choose the app environment", mock.Anything, mock.Anything, mock.Anything).
935+
Return(iostreams.SelectPromptResponse{Prompt: true, Option: "local"}, nil)
936+
937+
cm.API.On("GetAppStatus", mock.Anything, mockAuth.Token, []string{"A0123456789"}, mockAuth.TeamID).
938+
Return(api.GetAppStatusResult{}, nil)
939+
940+
return projectDir
941+
}
942+
943+
var projectDir string
944+
945+
testutil.TableTestCommand(t, testutil.CommandTests{
946+
"fetches remote manifest after linking app": {
947+
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789", "--environment", "local"},
948+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
949+
projectDir = setupAppFlagMocks(t, ctx, cm, cf)
950+
951+
manifestMock := &internalApp.ManifestMockObject{}
952+
manifestMock.On("GetManifestRemote", mock.Anything, mockAuth.Token, "A0123456789").
953+
Return(mockManifest, nil)
954+
cf.AppClient().Manifest = manifestMock
955+
},
956+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
957+
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
958+
959+
manifestData, err := afero.ReadFile(cm.Fs, projectDir+"/manifest.json")
960+
require.NoError(t, err)
961+
assert.Contains(t, string(manifestData), `"name": "My Remote App"`)
962+
assert.Contains(t, string(manifestData), `"description": "An app from remote settings"`)
963+
},
964+
},
965+
"warns on manifest fetch failure": {
966+
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789", "--environment", "local"},
967+
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
968+
projectDir = setupAppFlagMocks(t, ctx, cm, cf)
969+
970+
manifestMock := &internalApp.ManifestMockObject{}
971+
manifestMock.On("GetManifestRemote", mock.Anything, mockAuth.Token, "A0123456789").
972+
Return(types.SlackYaml{}, slackerror.New("network error"))
973+
cf.AppClient().Manifest = manifestMock
974+
},
975+
ExpectedStdoutOutputs: []string{
976+
"Could not fetch the remote app manifest",
977+
"The template manifest was kept unchanged",
978+
},
979+
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
980+
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
981+
},
982+
},
983+
}, func(cf *shared.ClientFactory) *cobra.Command {
984+
return NewCreateCommand(cf)
985+
})
986+
}

cmd/project/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func projectInitCommandRunE(clients *shared.ClientFactory, cmd *cobra.Command, a
110110
_ = create.InstallProjectDependencies(ctx, clients, projectDirPath)
111111

112112
// Add an existing app to the project
113-
err = app.LinkExistingApp(ctx, clients, &types.App{}, true)
113+
_, err = app.LinkExistingApp(ctx, clients, &types.App{}, true)
114114
if err != nil {
115115
// Display the error but continue to init
116116
clients.IO.PrintError(ctx, "%s", err.Error())

0 commit comments

Comments
 (0)