From ff87bbf6d182a65619107d27aeecd62aa3ebc071 Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Thu, 29 Oct 2026 10:00:22 -0700 Subject: [PATCH] fix: error before installing an app on an org when the manifest is not org ready An app created on an Enterprise org without "org_deploy_enabled": true installs at the org level but never receives events. Stop before the app is created and explain how to fix the manifest. Fixes #723 --- cmd/app/add_test.go | 36 ++++++++++++++++++ internal/pkg/apps/install.go | 25 +++++++++++++ internal/pkg/apps/install_test.go | 62 +++++++++++++++++++++++++++++++ internal/slackerror/errors.go | 11 ++++++ 4 files changed, 134 insertions(+) diff --git a/cmd/app/add_test.go b/cmd/app/add_test.go index a7ecddde..a16fd30e 100644 --- a/cmd/app/add_test.go +++ b/cmd/app/add_test.go @@ -434,6 +434,39 @@ func TestAppAddCommand(t *testing.T) { cm.API.AssertCalled(t, "DeveloperAppInstall", mock.Anything, mock.Anything, mockOrgAuth.Token, mock.Anything, mock.Anything, mock.Anything, "T123", mock.Anything) }, }, + "errors before creating a new app on an org when the manifest is not org ready": { + CmdArgs: []string{"--" + cmdutil.OrgGrantWorkspaceFlag, "T123"}, + ExpectedErrorStrings: []string{slackerror.ErrAppNotOrgReady}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + prepareAddMocks(t, cf, cm, "deployed") + manifestMock := &app.ManifestMockObject{} + manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).Return(types.SlackYaml{ + AppManifest: types.AppManifest{ + DisplayInformation: types.DisplayInformation{ + Name: team1TeamDomain, + }, + }, + }, nil) + cf.AppClient().Manifest = manifestMock + // Select workspace + appSelectMock := prompts.NewAppSelectMock() + appSelectPromptFunc = appSelectMock.AppSelectPrompt + appSelectMock.On("AppSelectPrompt", mock.Anything, mock.Anything, prompts.ShowHostedOnly, prompts.ShowAllApps).Return(prompts.SelectedApp{App: types.NewApp(), Auth: mockOrgAuth}, nil) + // Mock calls + cm.API.On("ValidateSession", mock.Anything, mock.Anything).Return(api.AuthSession{ + UserID: &mockOrgAuth.UserID, + TeamID: &mockOrgAuth.TeamID, + TeamName: &mockOrgAuth.TeamDomain, + }, nil) + mockProjectConfig := config.NewProjectConfigMock() + mockProjectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil) + cm.Config.ProjectConfig = mockProjectConfig + }, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + cm.API.AssertNotCalled(t, "CreateApp", mock.Anything, mock.Anything, mock.Anything, mock.Anything) + cm.API.AssertNotCalled(t, "DeveloperAppInstall", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) + }, + }, "adds a new local app when --environment local": { CmdArgs: []string{"--team", "T123", "--environment", "local"}, ExpectedOutputs: []string{"Creating app manifest", "Installing"}, @@ -904,6 +937,9 @@ func prepareAddMocks(t *testing.T, clients *shared.ClientFactory, clientsMock *s Name: team1TeamDomain, }, Workflows: map[string]types.Workflow{"test_workflow": {Title: "test workflow", InputParameters: types.ToRawJSON(`{}`)}}, + Settings: &types.AppSettings{ + OrgDeployEnabled: new(true), + }, }, }, nil) clients.AppClient().Manifest = manifestMock diff --git a/internal/pkg/apps/install.go b/internal/pkg/apps/install.go index a05f1f1b..971bb05d 100644 --- a/internal/pkg/apps/install.go +++ b/internal/pkg/apps/install.go @@ -103,6 +103,11 @@ func Install(ctx context.Context, clients *shared.ClientFactory, auth types.Slac configureHostedManifest(ctx, clients, &manifest) } + err = validateManifestOrgReady(app, auth, manifest) + if err != nil { + return app, "", err + } + err = validateManifestForInstall(ctx, clients, token, app, manifest) if err != nil { return app, "", err @@ -271,6 +276,21 @@ func printNonSuccessInstallState(ctx context.Context, clients *shared.ClientFact clients.IO.PrintInfo(ctx, false, "%s", status) } +// validateManifestOrgReady errors when an app would be created or reinstalled on +// an organization without "org_deploy_enabled" set to true in the manifest. Such +// an app installs on the organization but never receives events. +func validateManifestOrgReady(app types.App, auth types.SlackAuth, appManifest types.AppManifest) error { + newOrgApp := app.AppID == "" && auth.IsEnterpriseInstall + uninstalledOrgApp := app.IsUninstalled() && types.IsEnterpriseTeamID(app.TeamID) + if !newOrgApp && !uninstalledOrgApp { + return nil + } + if appManifest.Settings != nil && appManifest.Settings.OrgDeployEnabled != nil && *appManifest.Settings.OrgDeployEnabled { + return nil + } + return slackerror.New(slackerror.ErrAppNotOrgReady) +} + func validateManifestForInstall(ctx context.Context, clients *shared.ClientFactory, token string, app types.App, appManifest types.AppManifest) error { validationResult, err := clients.API().ValidateAppManifest(ctx, token, appManifest, app.AppID) @@ -402,6 +422,11 @@ func InstallLocalApp(ctx context.Context, clients *shared.ClientFactory, orgGran configureLocalManifest(ctx, clients, &manifest) } + err = validateManifestOrgReady(app, auth, manifest) + if err != nil { + return app, api.DeveloperAppInstallResult{}, "", err + } + err = validateManifestForInstall(ctx, clients, token, app, manifest) if err != nil { return app, api.DeveloperAppInstallResult{}, "", err diff --git a/internal/pkg/apps/install_test.go b/internal/pkg/apps/install_test.go index 4543e14d..61d0cdd5 100644 --- a/internal/pkg/apps/install_test.go +++ b/internal/pkg/apps/install_test.go @@ -1407,6 +1407,68 @@ func TestInstallLocalApp(t *testing.T) { } } +func TestValidateManifestOrgReady(t *testing.T) { + orgReady := types.AppManifest{Settings: &types.AppSettings{OrgDeployEnabled: new(true)}} + notOrgReady := types.AppManifest{Settings: &types.AppSettings{OrgDeployEnabled: new(false)}} + unset := types.AppManifest{} + orgAuth := types.SlackAuth{TeamID: "E123", IsEnterpriseInstall: true} + workspaceAuth := types.SlackAuth{TeamID: "T123"} + uninstalledOrgApp := types.App{AppID: "A123", TeamID: "E123", InstallStatus: types.AppStatusUninstalled} + installedOrgApp := types.App{AppID: "A123", TeamID: "E123", InstallStatus: types.AppStatusInstalled} + + tests := map[string]struct { + app types.App + auth types.SlackAuth + manifest types.AppManifest + err bool + }{ + "new org app that is org ready": { + app: types.NewApp(), + auth: orgAuth, + manifest: orgReady, + }, + "new org app with org deploy disabled": { + app: types.NewApp(), + auth: orgAuth, + manifest: notOrgReady, + err: true, + }, + "new org app with org deploy unset": { + app: types.NewApp(), + auth: orgAuth, + manifest: unset, + err: true, + }, + "uninstalled org app with org deploy disabled": { + app: uninstalledOrgApp, + auth: orgAuth, + manifest: notOrgReady, + err: true, + }, + "installed org app with org deploy disabled": { + app: installedOrgApp, + auth: orgAuth, + manifest: notOrgReady, + }, + "new workspace app with org deploy unset": { + app: types.NewApp(), + auth: workspaceAuth, + manifest: unset, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := validateManifestOrgReady(tc.app, tc.auth, tc.manifest) + if tc.err { + require.Error(t, err) + assert.Equal(t, slackerror.ErrAppNotOrgReady, slackerror.ToSlackError(err).Code) + } else { + assert.NoError(t, err) + } + }) + } +} + func TestValidateManifestForInstall(t *testing.T) { tests := map[string]struct { app types.App diff --git a/internal/slackerror/errors.go b/internal/slackerror/errors.go index 6549f0fd..83265c57 100644 --- a/internal/slackerror/errors.go +++ b/internal/slackerror/errors.go @@ -55,6 +55,7 @@ const ( ErrAppNotFound = "app_not_found" ErrAppNotHosted = "app_not_hosted" ErrAppNotInstalled = "app_not_installed" + ErrAppNotOrgReady = "app_not_org_ready" ErrAppRemove = "app_remove_error" ErrAppRenameApp = "app_rename_app" ErrAppsList = "apps_list_error" @@ -475,6 +476,16 @@ Otherwise start your app for local development with: %s`, Message: "The provided app must be installed on this team", }, + ErrAppNotOrgReady: { + Code: ErrAppNotOrgReady, + Message: "Apps installed on an organization must set \"org_deploy_enabled\" to true in the app manifest", + Remediation: fmt.Sprintf(`Add "org_deploy_enabled": true to the "settings" of the app manifest and try again. + +Otherwise create the app on a workspace from app settings, then add it to this project with: %s`, + style.Commandf("app link", false), + ), + }, + ErrAppRemove: { Code: ErrAppRemove, Message: "Couldn't remove your app",