Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
58e107c
feat: add two-way manifest sync between project and app settings
mwbrooks May 13, 2026
cd86717
Merge remote-tracking branch 'origin/main' into mwbrooks-2-way-manife…
mwbrooks Jun 11, 2026
38f720f
fix: refuse manifest sync when project source is remote
mwbrooks Jun 11, 2026
6f49023
fix: gate manifest sync command behind experiment
mwbrooks Jun 12, 2026
f531346
fix: return error on path collisions when unflattening manifests
mwbrooks Jun 12, 2026
52a2fbf
fix: write manifest.json atomically via temp file rename
mwbrooks Jun 12, 2026
be92c96
fix: refresh cached manifest hash after sync pushes to app settings
mwbrooks Jun 12, 2026
4dc53cc
fix: honor --force flag in non-TTY manifest sync
mwbrooks Jun 12, 2026
faf52bc
fix: surface marshal errors in writeback instead of dropping them
mwbrooks Jun 12, 2026
c911ada
fix: surface a warning when manifest.json key order is rewritten
mwbrooks Jun 12, 2026
68ff27e
fix: propagate marshal errors from valuesEqual instead of declaring i…
mwbrooks Jun 12, 2026
49f12f3
fix: escape dots in flattened path segments to preserve dotted keys
mwbrooks Jun 12, 2026
c2b199c
Merge remote-tracking branch 'origin/main' into mwbrooks-2-way-manife…
mwbrooks Jun 12, 2026
d57b965
chore: add Apache 2.0 license headers to new manifest files
mwbrooks Jun 12, 2026
22e3a29
refactor: move manifest package out of internal/pkg
mwbrooks Jun 12, 2026
f4c93e5
Merge remote-tracking branch 'origin/main' into mwbrooks-2-way-manife…
mwbrooks Jun 13, 2026
0da44a8
Merge branch 'main' into mwbrooks-2-way-manifest-sync
srtaalej Jul 15, 2026
0b98837
test: add coverage for manifest sync and display
srtaalej Jul 15, 2026
be0f881
test: remove redundant formatValue truncation case
srtaalej Jul 15, 2026
4849b44
feat: add --force-remote flag to manifest sync command
srtaalej Jul 16, 2026
879f3a6
Merge branch 'main' into mwbrooks-2-way-manifest-sync
srtaalej Jul 21, 2026
b183197
Merge branch 'mwbrooks-2-way-manifest-sync' into ale-manifest-force-r…
srtaalej Jul 21, 2026
f2fc5be
chore: retrigger e2e tests
srtaalej Jul 27, 2026
0c1eae1
fix: resolve merge conflicts with main after PR #591
srtaalej Jul 27, 2026
748326c
Merge branch 'mwbrooks-2-way-manifest-sync' into ale-manifest-force-r…
srtaalej Jul 27, 2026
b7e485f
fix: resolve merge conflicts with main
zimeg Jul 30, 2026
eeeab35
Merge branch 'main' into ale-manifest-force-remote
zimeg Jul 30, 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
7 changes: 7 additions & 0 deletions cmd/manifest/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}
10 changes: 10 additions & 0 deletions cmd/manifest/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type Config struct {
DeprecatedWorkspaceFlag string
DisableTelemetryFlag bool
ForceFlag bool
ForceRemoteFlag bool

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: We should avoid adding command flags to these configurations. For now it's alright with suggestions of #543 making it more clear that this is a flag for the sync command.

👾 ramble: I do find it curious to workaround the flag structures otherwise but fear it blurs concern of what a command can do here.

LogstashHostResolved string
NoColor bool
RuntimeFlag string
Expand Down
8 changes: 7 additions & 1 deletion internal/manifest/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.Commandf("manifest sync", false),
style.CommandText("--force"),
style.CommandText("--force-remote"),
)
default:
merged, err = resolveInteractively(ctx, clients, localManifest.AppManifest, remoteManifest.AppManifest, diffs)
Expand Down
24 changes: 24 additions & 0 deletions internal/manifest/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading