Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
236702a
Merge branch 'main' into ale-fetch-remote-manifest
srtaalej Jul 1, 2026
8639a29
fix: correct indentation from merge resolution
srtaalej Jul 1, 2026
2939882
Merge branch 'main' into ale-fetch-remote-manifest
srtaalej Jul 2, 2026
19cfb59
Merge branch 'main' into ale-fetch-remote-manifest
srtaalej Jul 6, 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
19 changes: 19 additions & 0 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,10 +230,28 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
defer func() {
_ = os.Chdir(originalDir)
}()

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

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{

@srtaalej srtaalej Jun 30, 2026

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.

left a comment about this silent fail 🪂 #585 (comment) @zimeg

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 🤔

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.

👁️‍🗨️ thought: I'm worried I'd miss warnings in CI and overwrite an existing app manifest for "A001" with these commands if the manifest step of "create" fails:

$ slack create my-app -t slack-samples/bolt-js-blank-template -a A001 -E deployed
$ slack deploy

Recommending to save the outputs of manifest info to a file is a good remediation!

}
}
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "house",
Text: "App",
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

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.

🧪 todo: Let's add unit tests alongside internal packages for more strict checks!

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 {

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.

Suggested change
func FetchAndWriteRemoteManifest(ctx context.Context, clients *shared.ClientFactory, token, appID, projectPath string) error {
func SetManifestLocal(ctx context.Context, clients *shared.ClientFactory, slackYaml types.SlackYaml) error {

🧮 thought: This might be nice to pair with other app manifest methods although a best file structure isn't clear to me right now:

  • GetManifestRemote
  • GetManifestLocal
  • SetManifestLocal: New!

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.

📝 note: This suggestion might go well with the idea to extend manifest info too?

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
}
Comment on lines +33 to +39

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: Can we reuse the stringifier in adjacent package?

// JSONMarshalUnescapedIndent converts a struct into an easily readable JSON
// encoding without escaping characters
func JSONMarshalUnescapedIndent(v interface{}) (string, error) {
var buff bytes.Buffer
encoder := json.NewEncoder(&buff)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
err := encoder.Encode(v)
if err != nil {
return "", err
}
return buff.String(), nil
}

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)
Comment on lines +44 to +48

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.

🏆 praise: Thanks for including this!

🦔 thought: I'm now curious if this would be better as part of the app link command? We might pull the manifest to write a hash when linking a specific app which can improve the link experience outside of create command?

}
Loading