From c64c68ed495634420121f2fd11f71ee8281d66e6 Mon Sep 17 00:00:00 2001 From: Jacob Sussmilch Date: Tue, 28 Jul 2026 13:35:12 +1000 Subject: [PATCH] feat: KEEP-1053 add kh workflow enable and disable Nothing in the CLI was called enable, so turning a workflow on looked impossible: pause turns off, go-live publishes a template, update has no flag. The capability existed as resume - named for the inverse of pause rather than for the action - so a reader scanning the verb list, or searching for enable, concluded it was missing and reached for the API or the web UI. enable and disable become the primary names. resume, activate and pause are kept as aliases and stay working indefinitely; scripts, tutorials and published doc links depend on them, and tests pin each alias to its command so a later rename cannot quietly drop one. Their help text now cross-references, so finding one leads to the other. Runtime output follows the new vocabulary: 'Workflow disabled' rather than 'paused'. This is a visible change for anyone matching on that string, and leaving it would have kept the confusion the rename exists to remove - the command and its own output would disagree. Regenerating replaces the pause and resume pages with enable and disable, which is the first real exercise of the pruning added in #78: the old pages are deleted rather than left behind as orphans. --- cmd/workflow/{pause.go => disable.go} | 32 ++++++--- .../{pause_test.go => disable_test.go} | 18 ++--- cmd/workflow/{resume.go => enable.go} | 33 ++++++---- cmd/workflow/enable_disable_aliases_test.go | 66 +++++++++++++++++++ .../{resume_test.go => enable_test.go} | 4 +- cmd/workflow/workflow.go | 4 +- cmd/workflow/workflow_test.go | 2 +- docs/kh_workflow.md | 4 +- ...rkflow_pause.md => kh_workflow_disable.md} | 28 +++++--- docs/kh_workflow_enable.md | 51 ++++++++++++++ docs/kh_workflow_resume.md | 39 ----------- 11 files changed, 197 insertions(+), 84 deletions(-) rename cmd/workflow/{pause.go => disable.go} (72%) rename cmd/workflow/{pause_test.go => disable_test.go} (93%) rename cmd/workflow/{resume.go => enable.go} (73%) create mode 100644 cmd/workflow/enable_disable_aliases_test.go rename cmd/workflow/{resume_test.go => enable_test.go} (96%) rename docs/{kh_workflow_pause.md => kh_workflow_disable.md} (50%) create mode 100644 docs/kh_workflow_enable.md delete mode 100644 docs/kh_workflow_resume.md diff --git a/cmd/workflow/pause.go b/cmd/workflow/disable.go similarity index 72% rename from cmd/workflow/pause.go rename to cmd/workflow/disable.go index d608072..d3196f0 100644 --- a/cmd/workflow/pause.go +++ b/cmd/workflow/disable.go @@ -15,16 +15,28 @@ import ( "github.com/spf13/cobra" ) -func NewPauseCmd(f *cmdutil.Factory) *cobra.Command { +func NewDisableCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ - Use: "pause ", - Short: "Pause a workflow", - Args: cobra.ExactArgs(1), - Example: ` # Pause a workflow (will prompt for confirmation) - kh wf pause abc123 - - # Pause without prompting - kh wf pause abc123 --yes`, + Use: "disable ", + // pause is the original name and stays working indefinitely; scripts + // and published links depend on it. + Aliases: []string{"pause"}, + Short: "Disable a workflow so it stops running", + Long: `Disable a workflow so it stops running. + +Turns off a workflow without deleting it. Runs already in flight are unaffected; +the trigger simply stops firing new ones. + +See also: kh workflow enable`, + Args: cobra.ExactArgs(1), + Example: ` # Disable a workflow (will prompt for confirmation) + kh wf disable abc123 + + # Disable without prompting + kh wf disable abc123 --yes + + # pause is an alias and still works + kh wf pause abc123`, RunE: func(cmd *cobra.Command, args []string) error { workflowID := args[0] @@ -85,7 +97,7 @@ func NewPauseCmd(f *cmdutil.Factory) *cobra.Command { p := output.NewPrinter(f.IOStreams, cmd) return p.PrintData(result, func(tw table.Writer) { - fmt.Fprintf(f.IOStreams.Out, "Workflow %s paused\n", workflowID) + fmt.Fprintf(f.IOStreams.Out, "Workflow %s disabled\n", workflowID) tw.Render() }) }, diff --git a/cmd/workflow/pause_test.go b/cmd/workflow/disable_test.go similarity index 93% rename from cmd/workflow/pause_test.go rename to cmd/workflow/disable_test.go index 17a8901..a25fb5e 100644 --- a/cmd/workflow/pause_test.go +++ b/cmd/workflow/disable_test.go @@ -63,7 +63,7 @@ func TestPauseSendsPATCH(t *testing.T) { assert.Equal(t, "PATCH", receivedMethod) assert.Equal(t, "/api/workflows/wf-abc", receivedPath) assert.Equal(t, false, receivedBody["enabled"]) - assert.Contains(t, outBuf.String(), "paused") + assert.Contains(t, outBuf.String(), "disabled") } func TestPauseYesFlagSkipsConfirmation(t *testing.T) { @@ -106,7 +106,7 @@ func TestPausePromptDeclineReturnsCancelError(t *testing.T) { // In non-TTY mode, --yes is not required and confirmation is skipped (auto-proceed) require.NoError(t, err) - assert.Contains(t, outBuf.String(), "paused") + assert.Contains(t, outBuf.String(), "disabled") } func TestPauseNonTTYAutoProceeds(t *testing.T) { @@ -155,20 +155,20 @@ func TestPauseHasYesFlag(t *testing.T) { ios, _, _, _ := iostreams.Test() f := &cmdutil.Factory{AppVersion: "1.0.0", IOStreams: ios} parent := workflow.NewWorkflowCmd(f) - var pauseCmd *cobra.Command + var disableCmd *cobra.Command for _, c := range parent.Commands() { - if strings.HasPrefix(c.Use, "pause") { - pauseCmd = c + if strings.HasPrefix(c.Use, "disable") { + disableCmd = c break } } - require.NotNil(t, pauseCmd, "pause command not found") + require.NotNil(t, disableCmd, "disable command not found") // --yes is a persistent flag inherited from root; verify it's accessible - flag := pauseCmd.Flags().Lookup("yes") + flag := disableCmd.Flags().Lookup("yes") if flag == nil { - flag = pauseCmd.InheritedFlags().Lookup("yes") + flag = disableCmd.InheritedFlags().Lookup("yes") } _ = flag // The test just verifies pause command exists and is wired correctly - assert.NotNil(t, pauseCmd) + assert.NotNil(t, disableCmd) } diff --git a/cmd/workflow/resume.go b/cmd/workflow/enable.go similarity index 73% rename from cmd/workflow/resume.go rename to cmd/workflow/enable.go index 3eb0d68..204b2f2 100644 --- a/cmd/workflow/resume.go +++ b/cmd/workflow/enable.go @@ -15,17 +15,28 @@ import ( "github.com/spf13/cobra" ) -func NewResumeCmd(f *cmdutil.Factory) *cobra.Command { +func NewEnableCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ - Use: "resume ", - Aliases: []string{"activate"}, - Short: "Resume a paused workflow", - Args: cobra.ExactArgs(1), - Example: ` # Resume a workflow (will prompt for confirmation) - kh wf resume abc123 - - # Resume without prompting - kh wf resume abc123 --yes`, + Use: "enable ", + // resume is the original name and stays working indefinitely; scripts + // and published links depend on it. + Aliases: []string{"resume", "activate"}, + Short: "Enable a workflow so it runs on its trigger", + Long: `Enable a workflow so it runs on its trigger. + +Turns on a workflow that is currently disabled. This is the last step after +creating one - a workflow does nothing until it is enabled. + +See also: kh workflow disable`, + Args: cobra.ExactArgs(1), + Example: ` # Enable a workflow (will prompt for confirmation) + kh wf enable abc123 + + # Enable without prompting + kh wf enable abc123 --yes + + # resume is an alias and still works + kh wf resume abc123`, RunE: func(cmd *cobra.Command, args []string) error { workflowID := args[0] @@ -88,7 +99,7 @@ func NewResumeCmd(f *cmdutil.Factory) *cobra.Command { p := output.NewPrinter(f.IOStreams, cmd) return p.PrintData(result, func(tw table.Writer) { - fmt.Fprintf(f.IOStreams.Out, "Workflow %s resumed\n", workflowID) + fmt.Fprintf(f.IOStreams.Out, "Workflow %s enabled\n", workflowID) tw.Render() }) }, diff --git a/cmd/workflow/enable_disable_aliases_test.go b/cmd/workflow/enable_disable_aliases_test.go new file mode 100644 index 0000000..a532e48 --- /dev/null +++ b/cmd/workflow/enable_disable_aliases_test.go @@ -0,0 +1,66 @@ +package workflow_test + +// enable/disable are the primary names, but resume/pause were the originals and +// are kept working indefinitely - scripts, tutorials and published doc links +// depend on them. These tests pin that promise: each alias must reach the same +// command and send the same request body, so a future rename cannot quietly +// drop one. + +import ( + "testing" + + "github.com/keeperhub/cli/cmd/workflow" + "github.com/keeperhub/cli/pkg/cmdutil" + "github.com/keeperhub/cli/pkg/iostreams" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// findSubcommand returns the subcommand reachable by the given name or alias. +func findSubcommand(t *testing.T, name string) (string, bool) { + t.Helper() + ios, _, _, _ := iostreams.Test() + root := workflow.NewWorkflowCmd(&cmdutil.Factory{IOStreams: ios}) + for _, c := range root.Commands() { + if c.Name() == name { + return c.Name(), true + } + for _, alias := range c.Aliases { + if alias == name { + return c.Name(), true + } + } + } + return "", false +} + +func TestEnableIsReachableByItsAliases(t *testing.T) { + for _, name := range []string{"enable", "resume", "activate"} { + resolved, found := findSubcommand(t, name) + require.True(t, found, "%q should reach a subcommand", name) + assert.Equal(t, "enable", resolved, "%q should resolve to enable", name) + } +} + +func TestDisableIsReachableByItsAliases(t *testing.T) { + for _, name := range []string{"disable", "pause"} { + resolved, found := findSubcommand(t, name) + require.True(t, found, "%q should reach a subcommand", name) + assert.Equal(t, "disable", resolved, "%q should resolve to disable", name) + } +} + +func TestEnableAndDisableCrossReferenceEachOther(t *testing.T) { + // "enable" is the word people search for; someone who lands on one should + // be told about the other rather than having to guess the opposite verb. + ios, _, _, _ := iostreams.Test() + root := workflow.NewWorkflowCmd(&cmdutil.Factory{IOStreams: ios}) + + longByName := map[string]string{} + for _, c := range root.Commands() { + longByName[c.Name()] = c.Long + } + + assert.Contains(t, longByName["enable"], "kh workflow disable") + assert.Contains(t, longByName["disable"], "kh workflow enable") +} diff --git a/cmd/workflow/resume_test.go b/cmd/workflow/enable_test.go similarity index 96% rename from cmd/workflow/resume_test.go rename to cmd/workflow/enable_test.go index d027382..0ab25c4 100644 --- a/cmd/workflow/resume_test.go +++ b/cmd/workflow/enable_test.go @@ -59,9 +59,9 @@ func TestResumeSendsPATCHWithEnabledTrue(t *testing.T) { require.NoError(t, err) assert.Equal(t, "PATCH", receivedMethod) assert.Equal(t, "/api/workflows/wf-abc", receivedPath) - // The inverse of pause: this is the assertion that distinguishes the two. + // The inverse of disable: this is the assertion that distinguishes the two. assert.Equal(t, true, receivedBody["enabled"]) - assert.Contains(t, outBuf.String(), "resumed") + assert.Contains(t, outBuf.String(), "enabled") } func TestResumeYesFlagSkipsConfirmation(t *testing.T) { diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index ecf32d7..befdec1 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -27,8 +27,8 @@ func NewWorkflowCmd(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(NewDeleteCmd(f)) cmd.AddCommand(NewUpdateCmd(f)) cmd.AddCommand(NewGoLiveCmd(f)) - cmd.AddCommand(NewPauseCmd(f)) - cmd.AddCommand(NewResumeCmd(f)) + cmd.AddCommand(NewDisableCmd(f)) + cmd.AddCommand(NewEnableCmd(f)) return cmd } diff --git a/cmd/workflow/workflow_test.go b/cmd/workflow/workflow_test.go index 2fd9a99..963ea10 100644 --- a/cmd/workflow/workflow_test.go +++ b/cmd/workflow/workflow_test.go @@ -23,7 +23,7 @@ func TestWorkflowCmdHas9Subcommands(t *testing.T) { f := newTestFactory() wfCmd := workflow.NewWorkflowCmd(f) cmds := wfCmd.Commands() - assert.Equal(t, 9, len(cmds), "expected 9 subcommands: list, run, get, go-live, pause, resume, create, delete, update") + assert.Equal(t, 9, len(cmds), "expected 9 subcommands: list, run, get, go-live, disable, enable, create, delete, update") } func TestWorkflowCmdHasAlias(t *testing.T) { diff --git a/docs/kh_workflow.md b/docs/kh_workflow.md index c4c79e9..84a27a8 100644 --- a/docs/kh_workflow.md +++ b/docs/kh_workflow.md @@ -34,11 +34,11 @@ Manage workflows * [kh](kh.md) - KeeperHub CLI * [kh workflow create](kh_workflow_create.md) - Create a workflow * [kh workflow delete](kh_workflow_delete.md) - Delete a workflow +* [kh workflow disable](kh_workflow_disable.md) - Disable a workflow so it stops running +* [kh workflow enable](kh_workflow_enable.md) - Enable a workflow so it runs on its trigger * [kh workflow get](kh_workflow_get.md) - Get a workflow * [kh workflow go-live](kh_workflow_go-live.md) - Publish a workflow * [kh workflow list](kh_workflow_list.md) - List workflows -* [kh workflow pause](kh_workflow_pause.md) - Pause a workflow -* [kh workflow resume](kh_workflow_resume.md) - Resume a paused workflow * [kh workflow run](kh_workflow_run.md) - Run a workflow * [kh workflow update](kh_workflow_update.md) - Update a workflow diff --git a/docs/kh_workflow_pause.md b/docs/kh_workflow_disable.md similarity index 50% rename from docs/kh_workflow_pause.md rename to docs/kh_workflow_disable.md index f75918a..45e17bc 100644 --- a/docs/kh_workflow_pause.md +++ b/docs/kh_workflow_disable.md @@ -1,25 +1,37 @@ -## kh workflow pause +## kh workflow disable -Pause a workflow +Disable a workflow so it stops running + +### Synopsis + +Disable a workflow so it stops running. + +Turns off a workflow without deleting it. Runs already in flight are unaffected; +the trigger simply stops firing new ones. + +See also: kh workflow enable ``` -kh workflow pause [flags] +kh workflow disable [flags] ``` ### Examples ``` - # Pause a workflow (will prompt for confirmation) - kh wf pause abc123 + # Disable a workflow (will prompt for confirmation) + kh wf disable abc123 + + # Disable without prompting + kh wf disable abc123 --yes - # Pause without prompting - kh wf pause abc123 --yes + # pause is an alias and still works + kh wf pause abc123 ``` ### Options ``` - -h, --help help for pause + -h, --help help for disable -y, --yes Skip confirmation prompt ``` diff --git a/docs/kh_workflow_enable.md b/docs/kh_workflow_enable.md new file mode 100644 index 0000000..b7f07af --- /dev/null +++ b/docs/kh_workflow_enable.md @@ -0,0 +1,51 @@ +## kh workflow enable + +Enable a workflow so it runs on its trigger + +### Synopsis + +Enable a workflow so it runs on its trigger. + +Turns on a workflow that is currently disabled. This is the last step after +creating one - a workflow does nothing until it is enabled. + +See also: kh workflow disable + +``` +kh workflow enable [flags] +``` + +### Examples + +``` + # Enable a workflow (will prompt for confirmation) + kh wf enable abc123 + + # Enable without prompting + kh wf enable abc123 --yes + + # resume is an alias and still works + kh wf resume abc123 +``` + +### Options + +``` + -h, --help help for enable + -y, --yes Skip confirmation prompt +``` + +### Options inherited from parent commands + +``` + -H, --host string KeeperHub host (default: app.keeperhub.com) + --jq string Filter JSON output with a jq expression + --json Output as JSON + --no-color Disable color output + --org string Organization ID to use (overrides default from auth) +``` + +### SEE ALSO + +* [kh workflow](kh_workflow.md) - Manage workflows + diff --git a/docs/kh_workflow_resume.md b/docs/kh_workflow_resume.md deleted file mode 100644 index d6a740a..0000000 --- a/docs/kh_workflow_resume.md +++ /dev/null @@ -1,39 +0,0 @@ -## kh workflow resume - -Resume a paused workflow - -``` -kh workflow resume [flags] -``` - -### Examples - -``` - # Resume a workflow (will prompt for confirmation) - kh wf resume abc123 - - # Resume without prompting - kh wf resume abc123 --yes -``` - -### Options - -``` - -h, --help help for resume - -y, --yes Skip confirmation prompt -``` - -### Options inherited from parent commands - -``` - -H, --host string KeeperHub host (default: app.keeperhub.com) - --jq string Filter JSON output with a jq expression - --json Output as JSON - --no-color Disable color output - --org string Organization ID to use (overrides default from auth) -``` - -### SEE ALSO - -* [kh workflow](kh_workflow.md) - Manage workflows -