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: 4 additions & 0 deletions cmd/platform/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ func NewDeployCommand(clients *shared.ClientFactory) *cobra.Command {
{Command: "platform deploy --team T0123456", Meaning: "Deploy to a specific team"},
}),
PreRunE: func(cmd *cobra.Command, args []string) error {
if err := cmdutil.ValidateManifestSourceFlag(clients); err != nil {
return err
}
Comment on lines +62 to +64

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
if err := cmdutil.ValidateManifestSourceFlag(clients); err != nil {
return err
}

🪓 quibble: I'd favor this validation happening with the switch case in internal/manifest/sync.go to avoid duplicate checks in code, although I understand this might error earlier.

return cmdutil.IsValidProjectDirectory(clients)
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -108,6 +111,7 @@ func NewDeployCommand(clients *shared.ClientFactory) *cobra.Command {
}

cmd.Flags().BoolVar(&deployFlags.hideTriggers, "hide-triggers", false, "do not list triggers and skip trigger creation prompts")
cmd.Flags().StringVar(&clients.Config.ManifestSourceFlag, "manifest-source", "", "resolve manifest differences using this source (project or remote)")
cmd.Flags().StringVar(&deployFlags.orgGrantWorkspaceID, cmdutil.OrgGrantWorkspaceFlag, "", cmdutil.OrgGrantWorkspaceDescription())

return cmd
Expand Down
18 changes: 18 additions & 0 deletions cmd/platform/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,24 @@ func TestDeployCommand(t *testing.T) {
deployPkgMock.AssertCalled(t, "Deploy", mock.Anything, mock.Anything, mock.Anything, mock.Anything)
}

func TestDeployCommand_ManifestSourceFlag_InvalidValue(t *testing.T) {
ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
clientsMock.AddDefaultMocks()
clients := shared.NewClientFactory(clientsMock.MockClientFactory(), func(clients *shared.ClientFactory) {
clients.Config.ProjectConfig = config.NewProjectConfigMock()
clients.SDKConfig = hooks.NewSDKConfigMock()
})

cmd := NewDeployCommand(clients)
testutil.MockCmdIO(clients.IO, cmd)
cmd.SetArgs([]string{"--manifest-source", "invalid"})

err := cmd.ExecuteContext(ctx)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid")
}

func TestDeployCommand_HasValidDeploymentMethod(t *testing.T) {
tests := map[string]struct {
app types.App
Expand Down
5 changes: 4 additions & 1 deletion cmd/platform/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ func NewRunCommand(clients *shared.ClientFactory) *cobra.Command {
{Command: "platform run --cleanup", Meaning: "Run a local development server with cleanup"},
}),
PreRunE: func(cmd *cobra.Command, args []string) error {
// Verify command is run in a project directory
if err := cmdutil.ValidateManifestSourceFlag(clients); err != nil {
return err
}
return cmdutil.IsValidProjectDirectory(clients)
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -70,6 +72,7 @@ func NewRunCommand(clients *shared.ClientFactory) *cobra.Command {
cmd.Flags().StringVar(&runFlags.activityLevel, "activity-level", platform.ActivityMinLevelDefault, "activity level to display")
cmd.Flags().BoolVar(&runFlags.noActivity, "no-activity", false, "hide Slack Platform log activity")
cmd.Flags().BoolVar(&runFlags.cleanup, "cleanup", false, "uninstall the local app after exiting")
cmd.Flags().StringVar(&clients.Config.ManifestSourceFlag, "manifest-source", "", "resolve manifest differences using this source (project or remote)")
cmd.Flags().StringVar(&runFlags.orgGrantWorkspaceID, cmdutil.OrgGrantWorkspaceFlag, "", cmdutil.OrgGrantWorkspaceDescription())
cmd.Flags().BoolVar(&runFlags.hideTriggers, "hide-triggers", false, "do not list triggers and skip trigger creation prompts")

Expand Down
19 changes: 19 additions & 0 deletions cmd/platform/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

// Setup a mock for the package
Expand Down Expand Up @@ -277,6 +278,24 @@ func TestRunCommand_Flags(t *testing.T) {
}
}

func TestRunCommand_ManifestSourceFlag_InvalidValue(t *testing.T) {
ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
clientsMock.IO.On("IsTTY").Return(true)
clientsMock.IO.AddDefaultMocks()
clients := shared.NewClientFactory(clientsMock.MockClientFactory(), func(clients *shared.ClientFactory) {
clients.SDKConfig = hooks.NewSDKConfigMock()
})

cmd := NewRunCommand(clients)
testutil.MockCmdIO(clients.IO, cmd)
cmd.SetArgs([]string{"--manifest-source", "invalid"})

err := cmd.ExecuteContext(ctx)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid")
}

func TestRunCommand_Help(t *testing.T) {
ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
Expand Down
25 changes: 25 additions & 0 deletions internal/cmdutil/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package cmdutil
import (
"fmt"

"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/style"
"github.com/spf13/cobra"
)
Expand All @@ -35,6 +37,29 @@ var OrgGrantWorkspaceDescription = func() string {
style.Secondary("(or 'all' for all workspaces in the org)"))
}

// ManifestSourceFlag values
const (
ManifestSourceProject = "project"
ManifestSourceRemote = "remote"
)
Comment on lines +41 to +44

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
const (
ManifestSourceProject = "project"
ManifestSourceRemote = "remote"
)
const (
ManifestSourceProject = "local"
ManifestSourceRemote = "remote"
)

🪬 suggestion(blocking): Earlier suggestion might've hinted at "project" terms but we should match existing configuration options I realize. Perhaps reusing logic from this package instead of validations here?

const (
ManifestSourceLocal ManifestSource = "local"
ManifestSourceRemote ManifestSource = "remote"
)


// ValidateManifestSourceFlag checks that --manifest-source has a valid value if set
func ValidateManifestSourceFlag(clients *shared.ClientFactory) error {
v := clients.Config.ManifestSourceFlag
if v == "" {
return nil
}
if v != ManifestSourceProject && v != ManifestSourceRemote {
return slackerror.New(slackerror.ErrInvalidFlag).
WithMessage("Invalid value %q for %s flag", v, style.CommandText("--manifest-source")).
WithRemediation("Valid values are %s or %s",
style.Highlight(ManifestSourceProject),
style.Highlight(ManifestSourceRemote),
)
}
return nil
}

// IsFlagChanged checks if a certain flag has been set in the command
func IsFlagChanged(cmd *cobra.Command, flag string) bool {
IsFlagSet := cmd.Flags().Lookup(flag)
Expand Down
45 changes: 45 additions & 0 deletions internal/cmdutil/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,55 @@ package cmdutil
import (
"testing"

"github.com/slackapi/slack-cli/internal/config"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_ValidateManifestSourceFlag(t *testing.T) {
tests := map[string]struct {
value string
expectErr bool
}{
"flag not provided is valid": {
value: "",
expectErr: false,
},
"project is valid": {
value: "project",
expectErr: false,
},
"remote is valid": {
value: "remote",
expectErr: false,
},
"invalid value returns error": {
value: "invalid",
expectErr: true,
},
"local is not valid": {
value: "local",
expectErr: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
clients := &shared.ClientFactory{
Config: &config.Config{ManifestSourceFlag: tc.value},
}
err := ValidateManifestSourceFlag(clients)
if tc.expectErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.value)
} else {
require.NoError(t, err)
}
})
}
}

func Test_IsFlagChanged(t *testing.T) {
tests := map[string]struct {
flag string
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Config struct {
DisableTelemetryFlag bool
ForceFlag bool
ForceRemoteFlag bool
ManifestSourceFlag string
LogstashHostResolved string
Comment on lines +57 to 58

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.

🧮 suggestion: Let's keep this in alphabetical order!

NoColor bool
RuntimeFlag string
Expand Down
9 changes: 5 additions & 4 deletions internal/manifest/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"fmt"

"github.com/slackapi/slack-cli/internal/cmdutil"
"github.com/slackapi/slack-cli/internal/config"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/shared/types"
Expand Down Expand Up @@ -77,12 +78,12 @@ func Sync(ctx context.Context, clients *shared.ClientFactory, app types.App, aut

var merged types.AppManifest
switch {
case clients.Config.ForceFlag:
case clients.Config.ManifestSourceFlag == cmdutil.ManifestSourceProject || clients.Config.ForceFlag:
merged, err = MergeAllFrom(localManifest.AppManifest, remoteManifest.AppManifest, diffs, MergeAllLocal)
if err != nil {
return nil, err
}
case clients.Config.ForceRemoteFlag:
case clients.Config.ManifestSourceFlag == cmdutil.ManifestSourceRemote || clients.Config.ForceRemoteFlag:
merged, err = MergeAllFrom(localManifest.AppManifest, remoteManifest.AppManifest, diffs, MergeAllRemote)
if err != nil {
return nil, err
Expand All @@ -91,8 +92,8 @@ func Sync(ctx context.Context, clients *shared.ClientFactory, app types.App, aut
return nil, slackerror.New(slackerror.ErrAppManifestUpdate).
WithRemediation("Run %s interactively to resolve manifest differences, or pass %s to push the project manifest to app settings or %s to pull app settings to project",
style.Commandf("manifest sync", false),
style.CommandText("--force"),
style.CommandText("--force-remote"),
style.CommandText("--manifest-source=project / --force"),
style.CommandText("--manifest-source=remote / --force-remote"),
Comment on lines +95 to +96

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: Are we alright to replace the --force and --force-remote options altogether while the sync command is under experiment?

)
default:
merged, err = resolveInteractively(ctx, clients, localManifest.AppManifest, remoteManifest.AppManifest, diffs)
Expand Down
63 changes: 63 additions & 0 deletions internal/manifest/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/slackapi/slack-cli/internal/api"
"github.com/slackapi/slack-cli/internal/app"
"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/hooks"
"github.com/slackapi/slack-cli/internal/iostreams"
Expand Down Expand Up @@ -220,6 +221,68 @@ func Test_Sync(t *testing.T) {
assert.Equal(t, "Remote", result.Merged.DisplayInformation.Description)
})

t.Run("manifest-source=project merges all local and pushes to API", func(t *testing.T) {
f := newSyncTestFixture(t)
f.projectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
f.manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).
Return(localManifest, nil)
f.manifestMock.On("GetManifestRemote", mock.Anything, mock.Anything, mock.Anything).
Return(remoteManifest, nil)
f.clients.Config.ManifestSourceFlag = cmdutil.ManifestSourceProject
f.clientsMock.API.On("UpdateApp", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(api.UpdateAppResult{}, nil)
f.cacheMock.On("NewManifestHash", mock.Anything, mock.Anything).Return(cache.Hash("newhash"), nil)
f.cacheMock.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).Return(nil)
_ = afero.WriteFile(f.fs, "/project/manifest.json", []byte(`{"display_information":{"name":"App"}}`), 0644)

result, err := Sync(f.ctx, f.clients, testApp, testAuth)

require.NoError(t, err)
require.NotNil(t, result)
assert.True(t, result.HasDifferences)
assert.Equal(t, "Local", result.Merged.DisplayInformation.Description)
})

t.Run("manifest-source=remote merges all remote and pushes to API", func(t *testing.T) {
f := newSyncTestFixture(t)
f.projectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
f.manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).
Return(localManifest, nil)
f.manifestMock.On("GetManifestRemote", mock.Anything, mock.Anything, mock.Anything).
Return(remoteManifest, nil)
f.clients.Config.ManifestSourceFlag = cmdutil.ManifestSourceRemote
f.clientsMock.API.On("UpdateApp", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(api.UpdateAppResult{}, nil)
f.cacheMock.On("NewManifestHash", mock.Anything, mock.Anything).Return(cache.Hash("newhash"), nil)
f.cacheMock.On("SetManifestHash", mock.Anything, mock.Anything, mock.Anything).Return(nil)
_ = afero.WriteFile(f.fs, "/project/manifest.json", []byte(`{"display_information":{"name":"App"}}`), 0644)

result, err := Sync(f.ctx, f.clients, testApp, testAuth)

require.NoError(t, err)
require.NotNil(t, result)
assert.True(t, result.HasDifferences)
assert.Equal(t, "Remote", result.Merged.DisplayInformation.Description)
})

t.Run("non-TTY error mentions --manifest-source in remediation", func(t *testing.T) {
f := newSyncTestFixture(t)
f.projectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
f.manifestMock.On("GetManifestLocal", mock.Anything, mock.Anything, mock.Anything).
Return(localManifest, nil)
f.manifestMock.On("GetManifestRemote", mock.Anything, mock.Anything, mock.Anything).
Return(remoteManifest, nil)

_, err := Sync(f.ctx, f.clients, testApp, testAuth)

require.Error(t, err)
slackErr := slackerror.ToSlackError(err)
assert.Contains(t, slackErr.Remediation, "--manifest-source=project")
assert.Contains(t, slackErr.Remediation, "--manifest-source=remote")
assert.Contains(t, slackErr.Remediation, "--force")
assert.Contains(t, slackErr.Remediation, "--force-remote")
})

t.Run("API UpdateApp failure is propagated", func(t *testing.T) {
f := newSyncTestFixture(t)
f.projectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)
Expand Down
7 changes: 7 additions & 0 deletions internal/pkg/apps/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/opentracing/opentracing-go"
"github.com/slackapi/slack-cli/internal/api"
"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/icon"
Expand Down Expand Up @@ -701,6 +702,12 @@ func shouldUpdateManifest(ctx context.Context, clients *shared.ClientFactory, ap
if err != nil {
return false, err
}
if clients.Config.ManifestSourceFlag == cmdutil.ManifestSourceRemote {
return false, nil
}
if clients.Config.ManifestSourceFlag == cmdutil.ManifestSourceProject {
return true, nil
}
if manifestSource.Equals(config.ManifestSourceRemote) {
return false, nil
}
Expand Down
Loading