From 4849b44e44901395e3cea910a9184111362c5246 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 16 Jul 2026 11:28:45 -0400 Subject: [PATCH] feat: add --force-remote flag to manifest sync command --- cmd/manifest/sync.go | 7 +++++++ cmd/manifest/sync_test.go | 10 ++++++++++ internal/config/config.go | 1 + internal/manifest/sync.go | 8 +++++++- internal/manifest/sync_test.go | 24 ++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) diff --git a/cmd/manifest/sync.go b/cmd/manifest/sync.go index 16bc5072..3b78a359 100644 --- a/cmd/manifest/sync.go +++ b/cmd/manifest/sync.go @@ -37,6 +37,8 @@ func NewSyncCommand(clients *shared.ClientFactory) *cobra.Command { Hidden: true, Example: style.ExampleCommandsf([]style.ExampleCommand{ {Command: "manifest sync", Meaning: "Sync project manifest with app settings"}, + {Command: "manifest sync --force", Meaning: "Push project manifest to app settings without prompting"}, + {Command: "manifest sync --force-remote", Meaning: "Pull app settings to project manifest without prompting"}, }), Args: cobra.NoArgs, PreRunE: func(cmd *cobra.Command, args []string) error { @@ -47,6 +49,10 @@ func NewSyncCommand(clients *shared.ClientFactory) *cobra.Command { style.CommandText("--experiment manifest-sync"), ) } + if clients.Config.ForceFlag && clients.Config.ForceRemoteFlag { + return slackerror.New(slackerror.ErrMismatchedFlags). + WithMessage("Cannot use both %s and %s flags", style.CommandText("--force"), style.CommandText("--force-remote")) + } return cmdutil.IsValidProjectDirectory(clients) }, RunE: func(cmd *cobra.Command, args []string) error { @@ -65,5 +71,6 @@ func NewSyncCommand(clients *shared.ClientFactory) *cobra.Command { return err }, } + cmd.Flags().BoolVar(&clients.Config.ForceRemoteFlag, "force-remote", false, "use all app settings values without prompting") return cmd } diff --git a/cmd/manifest/sync_test.go b/cmd/manifest/sync_test.go index 63c12951..2ca0da96 100644 --- a/cmd/manifest/sync_test.go +++ b/cmd/manifest/sync_test.go @@ -45,6 +45,16 @@ func TestSyncCommand(t *testing.T) { // the gate itself should pass. ExpectedErrorStrings: []string{}, }, + "errors when both --force and --force-remote are set": { + CmdArgs: []string{"--force-remote"}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.AddDefaultMocks() + cf.Config.ExperimentsFlag = []string{string(experiment.ManifestSync)} + cf.Config.LoadExperiments(ctx, cf.IO.PrintDebug) + cf.Config.ForceFlag = true + }, + ExpectedErrorStrings: []string{"Cannot use both", "--force", "--force-remote"}, + }, }, func(clients *shared.ClientFactory) *cobra.Command { return NewSyncCommand(clients) }) diff --git a/internal/config/config.go b/internal/config/config.go index 9d3fc3d5..979a0afe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -53,6 +53,7 @@ type Config struct { DeprecatedWorkspaceFlag string DisableTelemetryFlag bool ForceFlag bool + ForceRemoteFlag bool LogstashHostResolved string NoColor bool RuntimeFlag string diff --git a/internal/manifest/sync.go b/internal/manifest/sync.go index 59d09d96..4022e634 100644 --- a/internal/manifest/sync.go +++ b/internal/manifest/sync.go @@ -82,11 +82,17 @@ func Sync(ctx context.Context, clients *shared.ClientFactory, app types.App, aut if err != nil { return nil, err } + case clients.Config.ForceRemoteFlag: + merged, err = MergeAllFrom(localManifest.AppManifest, remoteManifest.AppManifest, diffs, MergeAllRemote) + if err != nil { + return nil, err + } case !clients.IO.IsTTY(): 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", + 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.CommandText("slack manifest sync"), style.CommandText("--force"), + style.CommandText("--force-remote"), ) default: merged, err = resolveInteractively(ctx, clients, localManifest.AppManifest, remoteManifest.AppManifest, diffs) diff --git a/internal/manifest/sync_test.go b/internal/manifest/sync_test.go index 65dd5a3f..a9feeb6c 100644 --- a/internal/manifest/sync_test.go +++ b/internal/manifest/sync_test.go @@ -196,6 +196,30 @@ func Test_Sync(t *testing.T) { f.clientsMock.API.AssertCalled(t, "UpdateApp", mock.Anything, "xoxb-test", "A123", mock.Anything, true, true) }) + t.Run("force-remote flag 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.ForceRemoteFlag = true + 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.True(t, result.WriteBack.Written) + // Verify remote value was used — the merged manifest should have "Remote" description + assert.Equal(t, "Remote", result.Merged.DisplayInformation.Description) + }) + t.Run("API UpdateApp failure is propagated", func(t *testing.T) { f := newSyncTestFixture(t) f.projectConfig.On("GetManifestSource", mock.Anything).Return(config.ManifestSourceLocal, nil)