Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0d8bfd3
feat: add --app flag support to slack create for linking existing apps
srtaalej May 28, 2026
da2eb75
address pr feedback
srtaalej Jun 1, 2026
e57f7d5
Merge branch 'main' into ale-app-id-flag
srtaalej Jun 2, 2026
aceb7a4
Merge branch 'main' into ale-app-id-flag
srtaalej Jun 2, 2026
906346a
simplify app creation with app link cmd
srtaalej Jun 8, 2026
8128d5e
Merge branch 'main' into ale-app-id-flag
srtaalej Jun 8, 2026
315c9bb
simplify redundant tests
srtaalej Jun 8, 2026
3177aa1
feat: fetch remote manifest after create with app ID
srtaalej Jun 9, 2026
6d6749a
Merge branch 'main' into ale-fetch-remote-manifest
srtaalej Jun 9, 2026
42782a3
Merge branch 'main' into ale-fetch-remote-manifest
srtaalej Jun 12, 2026
3d642ca
Merge branch 'main' into ale-fetch-remote-manifest
srtaalej Jun 16, 2026
f984c33
Merge branch 'main' into ale-fetch-remote-manifest
srtaalej Jun 18, 2026
3396d78
Merge branch 'main' into ale-fetch-remote-manifest
srtaalej Jun 25, 2026
bdbf756
refactor: move confirmation prompt out of LinkExistingApp into callers
srtaalej Jun 30, 2026
1538faf
undo changes to link command
srtaalej Jun 30, 2026
040a3c8
refactor: move fetchAndWriteRemoteManifest to internal/manifest/expor…
srtaalej Jun 30, 2026
1092da0
fix: avoid escaping forward slashes in exported manifest JSON
srtaalej Jun 30, 2026
aecb626
fix: save manifest hash after fetching remote manifest on create
srtaalej Jun 30, 2026
c289a09
refactor: remove auth return from LinkExistingApp, use AuthWithTeamID…
srtaalej Jun 30, 2026
19c2fac
chore: remove unnecessary import alias in create_test.go
srtaalej Jun 30, 2026
2283fbc
Merge branch 'main' into ale-fetch-remote-manifest
srtaalej Jun 30, 2026
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
24 changes: 22 additions & 2 deletions cmd/project/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/slackapi/slack-cli/cmd/app"
"github.com/slackapi/slack-cli/internal/iostreams"
"github.com/slackapi/slack-cli/internal/manifest"
"github.com/slackapi/slack-cli/internal/pkg/create"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/shared/types"
Expand Down Expand Up @@ -229,8 +230,27 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
defer func() {
_ = os.Chdir(originalDir)
}()
if err := app.LinkExistingApp(ctx, clients, &types.App{}, false); err != nil {
return err

linkedApp := &types.App{}
if linkErr := app.LinkExistingApp(ctx, clients, linkedApp, false); linkErr != nil {
return linkErr
}

if linkedApp.AppID != "" {
auth, err := clients.Auth().AuthWithTeamID(ctx, linkedApp.TeamID)
if err != nil {
return err
}
fetchErr := manifest.FetchAndWriteRemoteManifest(ctx, clients, auth.Token, linkedApp.AppID, absProjectPath)
if fetchErr != nil {
clients.IO.PrintWarning(ctx, "%s", style.Sectionf(style.TextSection{
Text: "Could not fetch the remote app manifest",
Secondary: []string{
fetchErr.Error(),
"The template manifest was kept unchanged",
},
}))
Comment on lines +246 to +252

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.

⚠️ question: Could returning the error here bring more confidence for scripts? I'm unsure when this might fail but would hope that invalid app IDs error overall to start

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hmm I'm hesitant because the create+--app command doesn't necessarily fail if fetching remote manifest fails. the app is still created and linked successfully. only one part of the command fails in this case so maybe it would be better to provide a way to try again in follow up PRs?
or simply telling users to run slack manifest info --source remote and copy that into manifest.json 🤔

}
}
}

Expand Down
98 changes: 96 additions & 2 deletions cmd/project/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/slackapi/slack-cli/internal/slackdeps"
"github.com/slackapi/slack-cli/internal/slackerror"
"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"
Expand Down Expand Up @@ -983,6 +984,98 @@ func TestCreateCommand_AppFlag(t *testing.T) {
})
}

func TestCreateCommand_AppFlag_FetchesRemoteManifest(t *testing.T) {
var createClientMock *CreateClientMock

mockAuth := types.SlackAuth{
Token: "xoxp-test-token",
TeamDomain: "test-team",
TeamID: "T001",
UserID: "U001",
}
mockManifest := types.SlackYaml{
AppManifest: types.AppManifest{
DisplayInformation: types.DisplayInformation{
Name: "My Remote App",
Description: "An app from remote settings",
},
},
}

setupAppFlagMocks := func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) string {
projectDir := t.TempDir()
createClientMock = new(CreateClientMock)
createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return(projectDir, nil)
CreateFunc = createClientMock.Create

cm.Os.On("Getwd").Return(projectDir, nil)

err := cm.Fs.MkdirAll(projectDir+"/.slack", 0755)
require.NoError(t, err)
err = afero.WriteFile(cm.Fs, projectDir+"/.slack/hooks.json", []byte("{}"), 0644)
require.NoError(t, err)

cm.IO.On("SelectPrompt", mock.Anything, "Select a category:", mock.Anything, mock.Anything).
Return(iostreams.SelectPromptResponse{Flag: true, Option: "slack-samples/bolt-js-starter-template"}, nil)

cm.Auth.On("Auths", mock.Anything).Return([]types.SlackAuth{mockAuth}, nil)
cm.Auth.On("AuthWithTeamID", mock.Anything, mock.Anything).Return(mockAuth, nil)
cm.IO.On("SelectPrompt", mock.Anything, "Select the existing app team", mock.Anything, mock.Anything, mock.Anything).
Return(iostreams.SelectPromptResponse{Prompt: true, Index: 0, Option: mockAuth.TeamDomain}, nil)
cm.IO.On("SelectPrompt", mock.Anything, "Choose the app environment", mock.Anything, mock.Anything, mock.Anything).
Return(iostreams.SelectPromptResponse{Prompt: true, Option: "local"}, nil)

cm.API.On("GetAppStatus", mock.Anything, mockAuth.Token, []string{"A0123456789"}, mockAuth.TeamID).
Return(api.GetAppStatusResult{}, nil)

return projectDir
}

var projectDir string

testutil.TableTestCommand(t, testutil.CommandTests{
"fetches remote manifest after linking app": {
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789", "--environment", "local"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
projectDir = setupAppFlagMocks(t, ctx, cm, cf)

manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestRemote", mock.Anything, mockAuth.Token, "A0123456789").
Return(mockManifest, nil)
cf.AppClient().Manifest = manifestMock
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)

manifestData, err := afero.ReadFile(cm.Fs, projectDir+"/manifest.json")
require.NoError(t, err)
assert.Contains(t, string(manifestData), `"name": "My Remote App"`)
assert.Contains(t, string(manifestData), `"description": "An app from remote settings"`)
},
},
"warns on manifest fetch failure": {
CmdArgs: []string{"my-app", "--template", "slack-samples/bolt-js-starter-template", "--app", "A0123456789", "--environment", "local"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
projectDir = setupAppFlagMocks(t, ctx, cm, cf)

manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestRemote", mock.Anything, mockAuth.Token, "A0123456789").
Return(types.SlackYaml{}, slackerror.New("network error"))
cf.AppClient().Manifest = manifestMock
},
ExpectedStdoutOutputs: []string{
"Could not fetch the remote app manifest",
"The template manifest was kept unchanged",
},
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)
},
},
}, func(cf *shared.ClientFactory) *cobra.Command {
return NewCreateCommand(cf)
})
}

var mockCreateLinkAuth = types.SlackAuth{
Token: "xoxp-example",
TeamDomain: "team1",
Expand All @@ -991,11 +1084,10 @@ var mockCreateLinkAuth = types.SlackAuth{
UserID: "U001",
}

// setupCreateLinkMocks prepares the in-memory project config and manifest mocks
// needed by app.LinkExistingApp when called from the create command.
func setupCreateLinkMocks(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
projectDirPath := slackdeps.MockWorkingDirectory
cm.Os.On("Getwd").Return(projectDirPath, nil)
cm.Auth.On("AuthWithTeamID", mock.Anything, mock.Anything).Return(mockCreateLinkAuth, nil)

if _, err := config.CreateProjectConfigDir(ctx, cm.Fs, projectDirPath); err != nil {
require.FailNow(t, fmt.Sprintf("Failed to create the project config directory: %s", err))
Expand All @@ -1010,5 +1102,7 @@ func setupCreateLinkMocks(t *testing.T, ctx context.Context, cm *shared.ClientsM
manifestMock := &app.ManifestMockObject{}
manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).
Return(types.SlackYaml{}, nil)
manifestMock.On("GetManifestRemote", mock.Anything, mock.Anything, mock.Anything).
Return(types.SlackYaml{}, nil)
cf.AppClient().Manifest = manifestMock
}
49 changes: 49 additions & 0 deletions internal/manifest/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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 manifest

import (
"bytes"
"context"
"encoding/json"
"path/filepath"

"github.com/slackapi/slack-cli/internal/shared"
"github.com/spf13/afero"
)

// FetchAndWriteRemoteManifest fetches the app manifest from remote settings and writes it to the project.
func FetchAndWriteRemoteManifest(ctx context.Context, clients *shared.ClientFactory, token, appID, projectPath string) error {
slackYaml, err := clients.AppClient().Manifest.GetManifestRemote(ctx, token, appID)
if err != nil {
return err
}
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
if err := encoder.Encode(slackYaml.AppManifest); err != nil {
return err
}
manifestPath := filepath.Join(projectPath, "manifest.json")
if err := afero.WriteFile(clients.Fs, manifestPath, buf.Bytes(), 0644); err != nil {
return err
}
hash, err := clients.Config.ProjectConfig.Cache().NewManifestHash(ctx, slackYaml.AppManifest)
if err != nil {
return err
}
return clients.Config.ProjectConfig.Cache().SetManifestHash(ctx, appID, hash)
}
Loading