Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 0 additions & 4 deletions cmd/app/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"github.com/slackapi/slack-cli/internal/app"
"github.com/slackapi/slack-cli/internal/cmdutil"
"github.com/slackapi/slack-cli/internal/config"
"github.com/slackapi/slack-cli/internal/experiment"
"github.com/slackapi/slack-cli/internal/logger"
"github.com/slackapi/slack-cli/internal/pkg/apps"
"github.com/slackapi/slack-cli/internal/prompts"
Expand Down Expand Up @@ -85,9 +84,6 @@ func preRunAddCommand(ctx context.Context, clients *shared.ClientFactory, cmd *c
if err != nil {
return err
}
if !clients.Config.WithExperimentOn(experiment.BoltFrameworks) {
return nil
}
clients.Config.SetFlags(cmd)
return nil
}
Expand Down
23 changes: 14 additions & 9 deletions cmd/app/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"github.com/slackapi/slack-cli/internal/cache"
"github.com/slackapi/slack-cli/internal/cmdutil"
"github.com/slackapi/slack-cli/internal/config"
"github.com/slackapi/slack-cli/internal/experiment"
"github.com/slackapi/slack-cli/internal/iostreams"
"github.com/slackapi/slack-cli/internal/prompts"
"github.com/slackapi/slack-cli/internal/shared"
Expand Down Expand Up @@ -85,25 +84,21 @@ func TestAppAddCommandPreRun(t *testing.T) {
cf.SDKConfig.WorkingDirectory = "."
},
},
"proceeds if manifest.source is local with the bolt experiment": {
"proceeds if manifest.source is local": {
ExpectedError: nil,
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cf.SDKConfig.WorkingDirectory = "."
cm.AddDefaultMocks()
cm.Config.ExperimentsFlag = append(cm.Config.ExperimentsFlag, string(experiment.BoltFrameworks))
cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
cm.Config.ProjectConfig = mockProjectConfig
},
},
"proceeds if manifest.source is remote with the bolt experiment": {
"proceeds if manifest.source is remote": {
ExpectedError: nil,
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cf.SDKConfig.WorkingDirectory = "."
cm.AddDefaultMocks()
cm.Config.ExperimentsFlag = append(cm.Config.ExperimentsFlag, string(experiment.BoltFrameworks))
cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceRemote, nil)
cm.Config.ProjectConfig = mockProjectConfig
Expand Down Expand Up @@ -207,6 +202,7 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down Expand Up @@ -279,6 +275,7 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down Expand Up @@ -343,7 +340,7 @@ func TestAppAddCommand(t *testing.T) {
nil,
)

// Mock existing and updated cache
// Mock existing and updated cache - hashes must match for update to proceed
cm.API.On(
"ExportAppManifest",
mock.Anything,
Expand All @@ -357,10 +354,11 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("GetManifestHash", mock.Anything, mock.Anything).
Return(cache.Hash("b4b4"), nil)
mockProjectCache.On("NewManifestHash", mock.Anything, mock.Anything).
Return(cache.Hash("xoxo"), nil)
Return(cache.Hash("b4b4"), nil) // matching hash allows update to proceed
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down Expand Up @@ -428,6 +426,7 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down Expand Up @@ -515,6 +514,7 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down Expand Up @@ -599,6 +599,7 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down Expand Up @@ -685,6 +686,7 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down Expand Up @@ -760,6 +762,7 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down Expand Up @@ -816,6 +819,7 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down Expand Up @@ -872,6 +876,7 @@ func TestAppAddCommand(t *testing.T) {
mockProjectCache.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockProjectConfig := config.NewProjectConfigMock()
mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
mockProjectConfig.On("Cache").Return(mockProjectCache)
cm.Config.ProjectConfig = mockProjectConfig
},
Expand Down
4 changes: 1 addition & 3 deletions docs/reference/experiments.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@ The Slack CLI has an experiment (`-e`) flag behind which we put features current

The following is a list of currently available experiments. We'll remove experiments from this page if we decide they are no longer needed or once they are released, in which case we'll make an announcement about the feature's general availability in the [developer changelog](https://docs.slack.dev/changelog).

- `bolt-install`: enables creating, installing, and running Bolt projects that manage their app manifest on app settings (remote manifest).
- `slack create` and `slack init` now set manifest source to "app settings" (remote) for Bolt JS & Bolt Python projects ([PR#96](https://github.com/slackapi/slack-cli/pull/96)).
- `slack run` and `slack install` support creating and installing Bolt Framework apps that have the manifest source set to "app settings (remote)" ([PR#111](https://github.com/slackapi/slack-cli/pull/111), [PR#154](https://github.com/slackapi/slack-cli/pull/154)).
- `charm`: shows beautiful prompts ([PR#348](https://github.com/slackapi/slack-cli/pull/348)).

## Experiments changelog

Below is a list of updates related to experiments.

- **March 2026**: Concluded the `bolt` and `bolt-install` experiments with full Bolt framework support now enabled by default in the Slack CLI. All Bolt project features including remote manifest management are now standard functionality. See the announcement [here](https://slack.dev/slackcli-supports-bolt-apps/).
- **February 2026**: Added the `charm` experiment.
- **December 2025**: Concluded the `read-only-collaborators` experiment with full support introduced to the Slack CLI. See the changelog announcement [here](https://docs.slack.dev/changelog/2025/12/04/slack-cli).
- **June 2025**:
Expand Down
15 changes: 1 addition & 14 deletions internal/experiment/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,6 @@ type Experiment string
// e.g. --experiment=first-toggle,second-toggle

const (
// BoltFrameworks experiment adds CLI support for Bolt JavaScript & Bolt Python.
// These frameworks will be introducing remote function support.
BoltFrameworks Experiment = "bolt"

// BoltInstall experiment enables developerInstall to work with Bolt projects that
// manage their app manifest on app settings (remote manifest).
BoltInstall Experiment = "bolt-install"

// Charm experiment enables beautiful prompts.
Charm Experiment = "charm"

Expand All @@ -48,18 +40,13 @@ const (
// AllExperiments is a list of all available experiments that can be enabled
// Please also add here 👇
var AllExperiments = []Experiment{
BoltFrameworks,
BoltInstall,
Charm,
Placeholder,
}

// EnabledExperiments is a list of experiments that are permanently enabled
// Please also add here 👇
var EnabledExperiments = []Experiment{
BoltFrameworks,
BoltInstall,
}
var EnabledExperiments = []Experiment{}

// Includes checks that a supplied experiment is included within AllExperiments
func Includes(expToCheck Experiment) bool {
Expand Down
1 change: 0 additions & 1 deletion internal/experiment/experiment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ func Test_Includes(t *testing.T) {
require.Equal(t, true, Includes(Experiment(Placeholder)))

// Test expected experiments
require.Equal(t, true, Includes(Experiment("bolt")))
require.Equal(t, true, Includes(Experiment("charm")))

// Test invalid experiment
Expand Down
94 changes: 21 additions & 73 deletions internal/pkg/apps/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"github.com/opentracing/opentracing-go"
"github.com/slackapi/slack-cli/internal/api"
"github.com/slackapi/slack-cli/internal/config"
"github.com/slackapi/slack-cli/internal/experiment"
"github.com/slackapi/slack-cli/internal/logger"
"github.com/slackapi/slack-cli/internal/pkg/manifest"
"github.com/slackapi/slack-cli/internal/shared"
Expand Down Expand Up @@ -55,12 +54,6 @@ func Install(ctx context.Context, clients *shared.ClientFactory, log *logger.Log
return types.App{}, "", err
}

if !clients.Config.WithExperimentOn(experiment.BoltInstall) {
if !manifestUpdates && !manifestCreates {
return app, "", nil
}
}

// Get the token for the authenticated workspace
apiInterface := clients.API()
token := auth.Token
Expand All @@ -84,31 +77,21 @@ func Install(ctx context.Context, clients *shared.ClientFactory, log *logger.Log
app.EnterpriseID = *authSession.EnterpriseID
}

// When the BoltInstall experiment is enabled, we need to get the manifest from the local file
// if the manifest source is local or if we are creating a new app. After an app is created,
// app settings becomes the source of truth for remote manifests, so updates and installs always
// get the latest manifest from app settings.
// When the BoltInstall experiment is disabled, we get the manifest from the local file because
// this is how the original implementation worked.
// Get the manifest from the local file if the manifest source is local or if we are creating
// a new app. After an app is created, app settings becomes the source of truth for remote
// manifests, so updates and installs always get the latest manifest from app settings.
var slackManifest types.SlackYaml
if clients.Config.WithExperimentOn(experiment.BoltInstall) {
manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
if err != nil {
return app, "", err
}
if manifestSource.Equals(config.ManifestSourceLocal) || manifestCreates {
slackManifest, err = clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor)
if err != nil {
return app, "", err
}
if manifestSource.Equals(config.ManifestSourceLocal) || manifestCreates {
slackManifest, err = clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor)
if err != nil {
return app, "", err
}
} else {
slackManifest, err = clients.AppClient().Manifest.GetManifestRemote(ctx, auth.Token, app.AppID)
if err != nil {
return app, "", err
}
}
} else {
slackManifest, err = clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor)
slackManifest, err = clients.AppClient().Manifest.GetManifestRemote(ctx, auth.Token, app.AppID)
if err != nil {
return app, "", err
}
Expand Down Expand Up @@ -369,12 +352,6 @@ func InstallLocalApp(ctx context.Context, clients *shared.ClientFactory, orgGran
return types.App{}, api.DeveloperAppInstallResult{}, "", err
}

if !clients.Config.WithExperimentOn(experiment.BoltInstall) {
if !manifestUpdates && !manifestCreates {
return app, api.DeveloperAppInstallResult{}, "", nil
}
}

apiInterface := clients.API()
token := auth.Token
authSession, err := apiInterface.ValidateSession(ctx, token)
Expand All @@ -398,31 +375,21 @@ func InstallLocalApp(ctx context.Context, clients *shared.ClientFactory, orgGran
// app.EnterpriseID = *authSession.EnterpriseID
}

// When the BoltInstall experiment is enabled, we need to get the manifest from the local file
// if the manifest source is local or if we are creating a new app. After an app is created,
// app settings becomes the source of truth for remote manifests, so updates and installs always
// get the latest manifest from app settings.
// When the BoltInstall experiment is disabled, we get the manifest from the local file because
// this is how the original implementation worked.
// Get the manifest from the local file if the manifest source is local or if we are creating
// a new app. After an app is created, app settings becomes the source of truth for remote
// manifests, so updates and installs always get the latest manifest from app settings.
var slackManifest types.SlackYaml
if clients.Config.WithExperimentOn(experiment.BoltInstall) {
manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
if err != nil {
return app, api.DeveloperAppInstallResult{}, "", err
}
if manifestSource.Equals(config.ManifestSourceLocal) || manifestCreates {
slackManifest, err = clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor)
if err != nil {
return app, api.DeveloperAppInstallResult{}, "", err
}
if manifestSource.Equals(config.ManifestSourceLocal) || manifestCreates {
slackManifest, err = clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor)
if err != nil {
return app, api.DeveloperAppInstallResult{}, "", err
}
} else {
slackManifest, err = clients.AppClient().Manifest.GetManifestRemote(ctx, auth.Token, app.AppID)
if err != nil {
return app, api.DeveloperAppInstallResult{}, "", err
}
}
} else {
slackManifest, err = clients.AppClient().Manifest.GetManifestLocal(ctx, clients.SDKConfig, clients.HookExecutor)
slackManifest, err = clients.AppClient().Manifest.GetManifestRemote(ctx, auth.Token, app.AppID)
if err != nil {
return app, api.DeveloperAppInstallResult{}, "", err
}
Expand Down Expand Up @@ -700,27 +667,11 @@ func updateIcon(ctx context.Context, clients *shared.ClientFactory, iconPath, ap
// shouldCreateManifest decides if an app manifest needs to be created for an
// app to exist
func shouldCreateManifest(ctx context.Context, clients *shared.ClientFactory, app types.App) (bool, error) {
if !clients.Config.WithExperimentOn(experiment.BoltFrameworks) {
return app.AppID == "", nil
}

// When the BoltInstall experiment is enabled, apps can always be created with any manifest source.
if clients.Config.WithExperimentOn(experiment.BoltInstall) {
return app.AppID == "", nil
}

manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
if err != nil {
return false, err
}
return app.AppID == "" && manifestSource == config.ManifestSourceLocal, nil
return app.AppID == "", nil
}

// shouldCacheManifest decides if an app manifest hash should be saved to cache
func shouldCacheManifest(ctx context.Context, clients *shared.ClientFactory, app types.App) (bool, error) {
if !clients.Config.WithExperimentOn(experiment.BoltFrameworks) {
return false, nil
}
manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
if err != nil {
return false, err
Expand Down Expand Up @@ -753,9 +704,6 @@ func shouldUpdateManifest(ctx context.Context, clients *shared.ClientFactory, ap
if app.AppID == "" {
return false, nil
}
if !clients.Config.WithExperimentOn(experiment.BoltFrameworks) {
return true, nil
}
manifestSource, err := clients.Config.ProjectConfig.GetManifestSource(ctx)
if err != nil {
return false, err
Expand Down
Loading
Loading