From 13a2f14235379fc4f0f49c2a9dcedfde2de16328 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 19 Aug 2026 16:27:06 +1000 Subject: [PATCH 1/8] Add --priority to release deploy and runbook run Mirrors the existing --guided-failure tri-state flag and maps true/false/default onto the server's PriorityMode. Needs the go-octopusdeploy Priority field, so it won't build until that lands and go.mod is bumped. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/cmd/release/deploy/deploy.go | 8 +++ pkg/cmd/release/deploy/deploy_test.go | 97 +++++++++++++++++++++++++++ pkg/cmd/runbook/run/run.go | 11 +++ pkg/cmd/runbook/run/run_by_tag.go | 1 + pkg/cmd/runbook/run/run_test.go | 4 ++ pkg/executor/executor.go | 18 +++++ pkg/executor/release.go | 6 ++ pkg/executor/runbook.go | 13 ++++ 8 files changed, 158 insertions(+) diff --git a/pkg/cmd/release/deploy/deploy.go b/pkg/cmd/release/deploy/deploy.go index 0df6d614..1f9f3354 100644 --- a/pkg/cmd/release/deploy/deploy.go +++ b/pkg/cmd/release/deploy/deploy.go @@ -70,6 +70,8 @@ const ( FlagAliasGuidedFailureMode = "guided-failure-mode" FlagAliasGuidedFailureModeLegacy = "guidedFailure" + FlagPriority = "priority" + FlagForcePackageDownload = "force-package-download" FlagAliasForcePackageDownloadLegacy = "forcePackageDownload" @@ -107,6 +109,7 @@ type DeployFlags struct { UpdateVariables *flag.Flag[bool] ExcludedSteps *flag.Flag[[]string] GuidedFailureMode *flag.Flag[string] // tri-state: true, false, or "use default". Can we model it with an optional bool? + Priority *flag.Flag[string] // tri-state: true, false, or "use default" ForcePackageDownload *flag.Flag[bool] DeploymentTargets *flag.Flag[[]string] ExcludeTargets *flag.Flag[[]string] @@ -127,6 +130,7 @@ func NewDeployFlags() *DeployFlags { UpdateVariables: flag.New[bool](FlagUpdateVariables, false), ExcludedSteps: flag.New[[]string](FlagSkip, false), GuidedFailureMode: flag.New[string](FlagGuidedFailure, false), + Priority: flag.New[string](FlagPriority, false), ForcePackageDownload: flag.New[bool](FlagForcePackageDownload, false), DeploymentTargets: flag.New[[]string](FlagDeploymentTarget, false), ExcludeTargets: flag.New[[]string](FlagExcludeDeploymentTarget, false), @@ -169,6 +173,7 @@ func NewCmdDeploy(f factory.Factory) *cobra.Command { flags.BoolVarP(&deployFlags.UpdateVariables.Value, deployFlags.UpdateVariables.Name, "", false, "Overwrite the release variable snapshot by re-importing variables from the project.") flags.StringArrayVarP(&deployFlags.ExcludedSteps.Value, deployFlags.ExcludedSteps.Name, "", nil, "Exclude specific steps from the deployment") flags.StringVarP(&deployFlags.GuidedFailureMode.Value, deployFlags.GuidedFailureMode.Name, "", "", "Enable Guided failure mode (true/false/default)") + flags.StringVarP(&deployFlags.Priority.Value, deployFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature and the TaskPrioritize permission.") flags.BoolVarP(&deployFlags.ForcePackageDownload.Value, deployFlags.ForcePackageDownload.Name, "", false, "Force re-download of packages") flags.StringArrayVarP(&deployFlags.DeploymentTargets.Value, deployFlags.DeploymentTargets.Name, "", nil, "Deploy to this target (can be specified multiple times)") flags.StringArrayVarP(&deployFlags.ExcludeTargets.Value, deployFlags.ExcludeTargets.Name, "", nil, "Deploy to targets except for this (can be specified multiple times)") @@ -223,6 +228,7 @@ func deployRun(cmd *cobra.Command, f factory.Factory, flags *DeployFlags) error ScheduledExpiryTime: flags.MaxQueueTime.Value, ExcludedSteps: flags.ExcludedSteps.Value, GuidedFailureMode: flags.GuidedFailureMode.Value, + Priority: flags.Priority.Value, ForcePackageDownload: flags.ForcePackageDownload.Value, DeploymentTargets: flags.DeploymentTargets.Value, ExcludeTargets: flags.ExcludeTargets.Value, @@ -262,6 +268,7 @@ func deployRun(cmd *cobra.Command, f factory.Factory, flags *DeployFlags) error resolvedFlags.MaxQueueTime.Value = options.ScheduledExpiryTime resolvedFlags.ExcludedSteps.Value = options.ExcludedSteps resolvedFlags.GuidedFailureMode.Value = options.GuidedFailureMode + resolvedFlags.Priority.Value = options.Priority resolvedFlags.DeploymentTargets.Value = options.DeploymentTargets resolvedFlags.ExcludeTargets.Value = options.ExcludeTargets resolvedFlags.DeploymentFreezeNames.Value = options.DeploymentFreezeNames @@ -297,6 +304,7 @@ func deployRun(cmd *cobra.Command, f factory.Factory, flags *DeployFlags) error resolvedFlags.MaxQueueTime, resolvedFlags.ExcludedSteps, resolvedFlags.GuidedFailureMode, + resolvedFlags.Priority, resolvedFlags.ForcePackageDownload, resolvedFlags.DeploymentTargets, resolvedFlags.ExcludeTargets, diff --git a/pkg/cmd/release/deploy/deploy_test.go b/pkg/cmd/release/deploy/deploy_test.go index fde01017..c2a7215f 100644 --- a/pkg/cmd/release/deploy/deploy_test.go +++ b/pkg/cmd/release/deploy/deploy_test.go @@ -1861,6 +1861,99 @@ func TestDeployCreate_AutomationMode(t *testing.T) { assert.Equal(t, "", stdErr.String()) }}, + {"release deploy with --priority false sends Priority Off", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { + cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) { + defer api.Close() + rootCmd.SetArgs([]string{"release", "deploy", "--project", fireProject.Name, "--version", "1.0", "--environment", "dev", "--priority", "false", "--output-format", "basic"}) + return rootCmd.ExecuteC() + }) + + api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource) + api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource) + api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProject.GetName()).RespondWith(fireProject) + + req := api.ExpectRequest(t, "POST", "/api/Spaces-1/deployments/create/untenanted/v1") + requestBody, err := testutil.ReadJson[deployments.CreateDeploymentUntenantedCommandV1](req.Request.Body) + assert.Nil(t, err) + + assert.Equal(t, deployments.CreateDeploymentUntenantedCommandV1{ + ReleaseVersion: "1.0", + EnvironmentNames: []string{"dev"}, + CreateExecutionAbstractCommandV1: deployments.CreateExecutionAbstractCommandV1{ + SpaceID: "Spaces-1", + ProjectIDOrName: fireProject.Name, + Priority: "Off", + }, + }, requestBody) + + req.RespondWith(&deployments.CreateDeploymentResponseV1{ + DeploymentServerTasks: []*deployments.DeploymentServerTask{ + {DeploymentID: "Deployments-203", ServerTaskID: "ServerTasks-29394"}, + }, + }) + + _, err = testutil.ReceivePair(cmdReceiver) + assert.Nil(t, err) + + assert.Equal(t, "ServerTasks-29394\n", stdOut.String()) + assert.Equal(t, "", stdErr.String()) + }}, + + {"release deploy with --priority default omits Priority so the server uses the lifecycle setting", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { + cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) { + defer api.Close() + rootCmd.SetArgs([]string{"release", "deploy", "--project", fireProject.Name, "--version", "1.0", "--environment", "dev", "--priority", "default", "--output-format", "basic"}) + return rootCmd.ExecuteC() + }) + + api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource) + api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource) + api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProject.GetName()).RespondWith(fireProject) + + req := api.ExpectRequest(t, "POST", "/api/Spaces-1/deployments/create/untenanted/v1") + requestBody, err := testutil.ReadJson[deployments.CreateDeploymentUntenantedCommandV1](req.Request.Body) + assert.Nil(t, err) + + assert.Equal(t, deployments.CreateDeploymentUntenantedCommandV1{ + ReleaseVersion: "1.0", + EnvironmentNames: []string{"dev"}, + CreateExecutionAbstractCommandV1: deployments.CreateExecutionAbstractCommandV1{ + SpaceID: "Spaces-1", + ProjectIDOrName: fireProject.Name, + }, + }, requestBody) + + req.RespondWith(&deployments.CreateDeploymentResponseV1{ + DeploymentServerTasks: []*deployments.DeploymentServerTask{ + {DeploymentID: "Deployments-203", ServerTaskID: "ServerTasks-29394"}, + }, + }) + + _, err = testutil.ReceivePair(cmdReceiver) + assert.Nil(t, err) + + assert.Equal(t, "ServerTasks-29394\n", stdOut.String()) + assert.Equal(t, "", stdErr.String()) + }}, + + {"release deploy rejects an unrecognised --priority value", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { + cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) { + defer api.Close() + rootCmd.SetArgs([]string{"release", "deploy", "--project", fireProject.Name, "--version", "1.0", "--environment", "dev", "--priority", "urgent"}) + return rootCmd.ExecuteC() + }) + + api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource) + api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource) + api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProject.GetName()).RespondWith(fireProject) + + _, err := testutil.ReceivePair(cmdReceiver) + assert.EqualError(t, err, "'urgent' is not a valid value for priority") + + assert.Equal(t, "", stdOut.String()) + assert.Equal(t, "", stdErr.String()) + }}, + {"release deploy specifying all the args; untentanted", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) { defer api.Close() @@ -1873,6 +1966,7 @@ func TestDeployCreate_AutomationMode(t *testing.T) { "--deploy-at-expiry", "2022-09-10 13:37:03 +10:00", "--skip", "Install", "--skip", "Cleanup", "--guided-failure", "true", + "--priority", "true", "--force-package-download", "--update-variables", "--target", "firstMachine", "--target", "secondMachine", @@ -1908,6 +2002,7 @@ func TestDeployCreate_AutomationMode(t *testing.T) { ExcludedMachineNames: []string{"thirdMachine"}, SkipStepNames: []string{"Install", "Cleanup"}, UseGuidedFailure: &trueVal, + Priority: "On", RunAt: "2022-09-10 13:32:03 +10:00", NoRunAfter: "2022-09-10 13:37:03 +10:00", Variables: map[string]string{ @@ -1947,6 +2042,7 @@ func TestDeployCreate_AutomationMode(t *testing.T) { "--tenant", "Coke", "--tenant", "Pepsi", "--tenant-tag", "Region/us-east", "--guided-failure", "true", + "--priority", "true", "--force-package-download", "--update-variables", "--target", "firstMachine", "--target", "secondMachine", @@ -1983,6 +2079,7 @@ func TestDeployCreate_AutomationMode(t *testing.T) { ExcludedMachineNames: []string{"thirdMachine"}, SkipStepNames: []string{"Install", "Cleanup"}, UseGuidedFailure: &trueVal, + Priority: "On", RunAt: "2022-09-10 13:32:03 +10:00", NoRunAfter: "2022-09-10 13:37:03 +10:00", Variables: map[string]string{ diff --git a/pkg/cmd/runbook/run/run.go b/pkg/cmd/runbook/run/run.go index ad57eb89..20455c5f 100644 --- a/pkg/cmd/runbook/run/run.go +++ b/pkg/cmd/runbook/run/run.go @@ -74,6 +74,8 @@ const ( FlagAliasGuidedFailureMode = "guided-failure-mode" FlagAliasGuidedFailureModeLegacy = "guidedFailure" + FlagPriority = "priority" + FlagForcePackageDownload = "force-package-download" FlagAliasForcePackageDownloadLegacy = "forcePackageDownload" @@ -106,6 +108,7 @@ type RunFlags struct { Snapshot *flag.Flag[string] ExcludedSteps *flag.Flag[[]string] GuidedFailureMode *flag.Flag[string] // tri-state: true, false, or "use default". Can we model it with an optional bool? + Priority *flag.Flag[string] // tri-state: true, false, or "use default" ForcePackageDownload *flag.Flag[bool] RunTargets *flag.Flag[[]string] ExcludeTargets *flag.Flag[[]string] @@ -129,6 +132,7 @@ func NewRunFlags() *RunFlags { Snapshot: flag.New[string](FlagSnapshot, false), ExcludedSteps: flag.New[[]string](FlagSkip, false), GuidedFailureMode: flag.New[string](FlagGuidedFailure, false), + Priority: flag.New[string](FlagPriority, false), ForcePackageDownload: flag.New[bool](FlagForcePackageDownload, false), RunTargets: flag.New[[]string](FlagRunTarget, false), ExcludeTargets: flag.New[[]string](FlagExcludeRunTarget, false), @@ -171,6 +175,7 @@ func NewCmdRun(f factory.Factory) *cobra.Command { flags.StringVarP(&runFlags.Snapshot.Value, runFlags.Snapshot.Name, "", "", "Name or ID of the snapshot to run. If not supplied, the command will attempt to use the published snapshot.") flags.StringArrayVarP(&runFlags.ExcludedSteps.Value, runFlags.ExcludedSteps.Name, "", nil, "Exclude specific steps from the runbook") flags.StringVarP(&runFlags.GuidedFailureMode.Value, runFlags.GuidedFailureMode.Name, "", "", "Enable Guided failure mode (true/false/default)") + flags.StringVarP(&runFlags.Priority.Value, runFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature and the TaskPrioritize permission.") flags.BoolVarP(&runFlags.ForcePackageDownload.Value, runFlags.ForcePackageDownload.Name, "", false, "Force re-download of packages") flags.StringArrayVarP(&runFlags.RunTargets.Value, runFlags.RunTargets.Name, "", nil, "Run on this target (can be specified multiple times)") flags.StringArrayVarP(&runFlags.ExcludeTargets.Value, runFlags.ExcludeTargets.Name, "", nil, "Run on targets except for this (can be specified multiple times)") @@ -289,6 +294,7 @@ func runDbRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octopu ScheduledExpiryTime: flags.MaxQueueTime.Value, ExcludedSteps: flags.ExcludedSteps.Value, GuidedFailureMode: flags.GuidedFailureMode.Value, + Priority: flags.Priority.Value, ForcePackageDownload: flags.ForcePackageDownload.Value, RunTargets: flags.RunTargets.Value, ExcludeTargets: flags.ExcludeTargets.Value, @@ -330,6 +336,7 @@ func runDbRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octopu resolvedFlags.MaxQueueTime.Value = options.ScheduledExpiryTime resolvedFlags.ExcludedSteps.Value = options.ExcludedSteps resolvedFlags.GuidedFailureMode.Value = options.GuidedFailureMode + resolvedFlags.Priority.Value = options.Priority resolvedFlags.RunTargets.Value = options.RunTargets resolvedFlags.ExcludeTargets.Value = options.ExcludeTargets @@ -364,6 +371,7 @@ func runDbRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octopu resolvedFlags.MaxQueueTime, resolvedFlags.ExcludedSteps, resolvedFlags.GuidedFailureMode, + resolvedFlags.Priority, resolvedFlags.ForcePackageDownload, resolvedFlags.RunTargets, resolvedFlags.ExcludeTargets, @@ -420,6 +428,7 @@ func runGitRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octop ScheduledExpiryTime: flags.MaxQueueTime.Value, ExcludedSteps: flags.ExcludedSteps.Value, GuidedFailureMode: flags.GuidedFailureMode.Value, + Priority: flags.Priority.Value, ForcePackageDownload: flags.ForcePackageDownload.Value, RunTargets: flags.RunTargets.Value, ExcludeTargets: flags.ExcludeTargets.Value, @@ -464,6 +473,7 @@ func runGitRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octop resolvedFlags.MaxQueueTime.Value = options.ScheduledExpiryTime resolvedFlags.ExcludedSteps.Value = options.ExcludedSteps resolvedFlags.GuidedFailureMode.Value = options.GuidedFailureMode + resolvedFlags.Priority.Value = options.Priority resolvedFlags.RunTargets.Value = options.RunTargets resolvedFlags.ExcludeTargets.Value = options.ExcludeTargets resolvedFlags.GitRef.Value = options.GitReference @@ -502,6 +512,7 @@ func runGitRunbook(cmd *cobra.Command, f factory.Factory, flags *RunFlags, octop resolvedFlags.MaxQueueTime, resolvedFlags.ExcludedSteps, resolvedFlags.GuidedFailureMode, + resolvedFlags.Priority, resolvedFlags.ForcePackageDownload, resolvedFlags.RunTargets, resolvedFlags.ExcludeTargets, diff --git a/pkg/cmd/runbook/run/run_by_tag.go b/pkg/cmd/runbook/run/run_by_tag.go index f72a030f..d281bff5 100644 --- a/pkg/cmd/runbook/run/run_by_tag.go +++ b/pkg/cmd/runbook/run/run_by_tag.go @@ -364,6 +364,7 @@ func runRunbooksByTag(cmd *cobra.Command, f factory.Factory, flags *RunFlags, oc ScheduledExpiryTime: flags.MaxQueueTime.Value, ExcludedSteps: flags.ExcludedSteps.Value, GuidedFailureMode: flags.GuidedFailureMode.Value, + Priority: flags.Priority.Value, ForcePackageDownload: flags.ForcePackageDownload.Value, RunTargets: flags.RunTargets.Value, ExcludeTargets: flags.ExcludeTargets.Value, diff --git a/pkg/cmd/runbook/run/run_test.go b/pkg/cmd/runbook/run/run_test.go index 33c1904d..a67d17e0 100644 --- a/pkg/cmd/runbook/run/run_test.go +++ b/pkg/cmd/runbook/run/run_test.go @@ -287,6 +287,7 @@ func TestRunbookRun_AutomationMode(t *testing.T) { "--skip", "Install", "--skip", "Cleanup", "--snapshot", "Snapshot FWKMLUX", "--guided-failure", "true", + "--priority", "true", "--force-package-download", "--target", "firstMachine", "--target", "secondMachine", "--exclude-target", "thirdMachine", @@ -317,6 +318,7 @@ func TestRunbookRun_AutomationMode(t *testing.T) { ExcludedMachineNames: []string{"thirdMachine"}, SkipStepNames: []string{"Install", "Cleanup"}, UseGuidedFailure: &trueVar, + Priority: "On", RunAt: "2022-09-10 13:32:03 +10:00", NoRunAfter: "2022-09-10 13:37:03 +10:00", Variables: map[string]string{ @@ -636,6 +638,7 @@ func TestGitRunbookRun_AutomationMode(t *testing.T) { "--run-at-expiry", "2022-09-10 13:37:03 +10:00", "--skip", "Install", "--skip", "Cleanup", "--guided-failure", "true", + "--priority", "true", "--force-package-download", "--target", "firstMachine", "--target", "secondMachine", "--exclude-target", "thirdMachine", @@ -669,6 +672,7 @@ func TestGitRunbookRun_AutomationMode(t *testing.T) { ExcludedMachineNames: []string{"thirdMachine"}, SkipStepNames: []string{"Install", "Cleanup"}, UseGuidedFailure: &trueVar, + Priority: "On", RunAt: "2022-09-10 13:32:03 +10:00", NoRunAfter: "2022-09-10 13:37:03 +10:00", Variables: map[string]string{ diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go index 80fed5c0..b91e3e8e 100644 --- a/pkg/executor/executor.go +++ b/pkg/executor/executor.go @@ -2,6 +2,8 @@ package executor import ( "fmt" + "strconv" + "strings" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" @@ -34,6 +36,22 @@ func NewTask(taskType TaskType, options any) *Task { } } +// parsePriorityMode maps the CLI's tri-state priority value onto the server's PriorityMode. +// An empty result means 'not specified', which leaves the server to apply the lifecycle default. +func parsePriorityMode(value string) (string, error) { + b, err := strconv.ParseBool(value) + if err == nil { + if b { + return "On", nil + } + return "Off", nil + } + if value == "" || strings.EqualFold("default", value) { + return "", nil + } + return "", fmt.Errorf("'%s' is not a valid value for priority", value) +} + // ProcessTasks iterates over the list of tasks and attempts to run them all. // If everything goes well, a nil error will be returned. // On the first failure, the error will be returned and the process will halt. diff --git a/pkg/executor/release.go b/pkg/executor/release.go index 7e57a9c1..ee42e35f 100644 --- a/pkg/executor/release.go +++ b/pkg/executor/release.go @@ -101,6 +101,7 @@ type TaskOptionsDeployRelease struct { ScheduledExpiryTime string ExcludedSteps []string GuidedFailureMode string // ["", "true", "false", "default"]. Note default and "" are the same, the only difference is whether interactive mode prompts you + Priority string // ["", "true", "false", "default"]. "" and "default" both defer to the lifecycle's priority setting ForcePackageDownload bool DeploymentTargets []string ExcludeTargets []string @@ -171,6 +172,11 @@ func releaseDeploy(octopus *client.Client, space *spaces.Space, input any) error } } + abstractCmd.Priority, err = parsePriorityMode(params.Priority) + if err != nil { + return err + } + // If either tenants or tenantTags are specified then it must be a tenanted deployment. // Otherwise it must be untenanted. // If the server has a tenanted deployment and both TenantNames+Tags are empty, the request fails, diff --git a/pkg/executor/runbook.go b/pkg/executor/runbook.go index e6ca7038..886f1a25 100644 --- a/pkg/executor/runbook.go +++ b/pkg/executor/runbook.go @@ -31,6 +31,7 @@ type TaskOptionsRunbookRunBase struct { ScheduledExpiryTime string ExcludedSteps []string GuidedFailureMode string // ["", "true", "false", "default"]. Note default and "" are the same, the only difference is whether interactive mode prompts you + Priority string // ["", "true", "false", "default"]. "" and "default" both leave the server to decide ForcePackageDownload bool RunTargets []string ExcludeTargets []string @@ -95,6 +96,12 @@ func runbookRun(octopus *client.Client, space *spaces.Space, input any) error { return fmt.Errorf("'%s' is not a valid value for guided failure mode", params.GuidedFailureMode) } } + + abstractCmd.Priority, err = parsePriorityMode(params.Priority) + if err != nil { + return err + } + runCommand := runbooks.NewRunbookRunCommandV1(space.ID, params.ProjectName) runCommand.RunbookName = params.RunbookName runCommand.EnvironmentNames = params.Environments @@ -169,6 +176,12 @@ func gitRunbookRun(octopus *client.Client, space *spaces.Space, input any) error return fmt.Errorf("'%s' is not a valid value for guided failure mode", params.GuidedFailureMode) } } + + abstractCmd.Priority, err = parsePriorityMode(params.Priority) + if err != nil { + return err + } + runCommand := runbooks.NewGitRunbookRunCommandV1(space.ID, params.ProjectName) runCommand.RunbookName = params.RunbookName runCommand.EnvironmentNames = params.Environments From 44ad441ac56882c3e887f1b06bb1c64605fceb5b Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 19 Aug 2026 16:58:49 +1000 Subject: [PATCH 2/8] Show priority in the advanced options summary and correct the flag help The summary listed every other advanced option but not priority, so passing --priority read as if the flag had been ignored. Also drops the permission claim from the help text, since the server only enforces it on deployments, and says what valid values are when one is rejected. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/cmd/release/deploy/deploy.go | 7 +++++-- pkg/cmd/release/deploy/deploy_test.go | 13 ++++++++++--- pkg/cmd/runbook/run/run.go | 7 +++++-- pkg/cmd/runbook/run/run_test.go | 5 +++++ pkg/executionscommon/executionscommon.go | 13 +++++++++++++ pkg/executor/executor.go | 4 ++-- 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/release/deploy/deploy.go b/pkg/cmd/release/deploy/deploy.go index 1f9f3354..b036df0f 100644 --- a/pkg/cmd/release/deploy/deploy.go +++ b/pkg/cmd/release/deploy/deploy.go @@ -173,7 +173,7 @@ func NewCmdDeploy(f factory.Factory) *cobra.Command { flags.BoolVarP(&deployFlags.UpdateVariables.Value, deployFlags.UpdateVariables.Name, "", false, "Overwrite the release variable snapshot by re-importing variables from the project.") flags.StringArrayVarP(&deployFlags.ExcludedSteps.Value, deployFlags.ExcludedSteps.Name, "", nil, "Exclude specific steps from the deployment") flags.StringVarP(&deployFlags.GuidedFailureMode.Value, deployFlags.GuidedFailureMode.Name, "", "", "Enable Guided failure mode (true/false/default)") - flags.StringVarP(&deployFlags.Priority.Value, deployFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature and the TaskPrioritize permission.") + flags.StringVarP(&deployFlags.Priority.Value, deployFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature.") flags.BoolVarP(&deployFlags.ForcePackageDownload.Value, deployFlags.ForcePackageDownload.Name, "", false, "Force re-download of packages") flags.StringArrayVarP(&deployFlags.DeploymentTargets.Value, deployFlags.DeploymentTargets.Name, "", nil, "Deploy to this target (can be specified multiple times)") flags.StringArrayVarP(&deployFlags.ExcludeTargets.Value, deployFlags.ExcludeTargets.Name, "", nil, "Deploy to targets except for this (can be specified multiple times)") @@ -1041,6 +1041,8 @@ func PrintAdvancedSummary(stdout io.Writer, options *executor.TaskOptionsDeployR gfmStr := executionscommon.LookupGuidedFailureModeString(options.GuidedFailureMode) + priorityStr := executionscommon.LookupPriorityString(options.Priority) + pkgDownloadStr := executionscommon.LookupPackageDownloadString(!options.ForcePackageDownload) depTargetsStr := "All included" @@ -1076,9 +1078,10 @@ func PrintAdvancedSummary(stdout io.Writer, options *executor.TaskOptionsDeployR Deploy Time: cyan(%s) Skipped Steps: cyan(%s) Guided Failure Mode: cyan(%s) + Priority: cyan(%s) Package Download: cyan(%s) Deployment Targets: cyan(%s) - `)), deployAtStr, skipStepsStr, gfmStr, pkgDownloadStr, depTargetsStr) + `)), deployAtStr, skipStepsStr, gfmStr, priorityStr, pkgDownloadStr, depTargetsStr) } func selectRelease(octopus *octopusApiClient.Client, ask question.Asker, questionText string, space *spaces.Space, project *projects.Project, channel *channels.Channel) (*releases.Release, error) { diff --git a/pkg/cmd/release/deploy/deploy_test.go b/pkg/cmd/release/deploy/deploy_test.go index c2a7215f..238b214e 100644 --- a/pkg/cmd/release/deploy/deploy_test.go +++ b/pkg/cmd/release/deploy/deploy_test.go @@ -1948,7 +1948,7 @@ func TestDeployCreate_AutomationMode(t *testing.T) { api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProject.GetName()).RespondWith(fireProject) _, err := testutil.ReceivePair(cmdReceiver) - assert.EqualError(t, err, "'urgent' is not a valid value for priority") + assert.EqualError(t, err, "'urgent' is not a valid value for priority, expected true, false or default") assert.Equal(t, "", stdOut.String()) assert.Equal(t, "", stdErr.String()) @@ -2180,7 +2180,7 @@ func TestDeployCreate_GenerationOfAutomationCommand_MasksSensitiveVariables(t *t // need to very it's wired up properly receiver := testutil.GoBegin2(func() (*cobra.Command, error) { defer testutil.Close(api, qa) - rootCmd.SetArgs([]string{"release", "deploy", "--project", "fire project", "--version", "2.0", "--environment", "dev"}) + rootCmd.SetArgs([]string{"release", "deploy", "--project", "fire project", "--version", "2.0", "--environment", "dev", "--priority", "true"}) return rootCmd.ExecuteC() }) @@ -2270,6 +2270,7 @@ func TestDeployCreate_GenerationOfAutomationCommand_MasksSensitiveVariables(t *t CreateExecutionAbstractCommandV1: deployments.CreateExecutionAbstractCommandV1{ SpaceID: "Spaces-1", ProjectIDOrName: fireProject.Name, + Priority: "On", Variables: map[string]string{ "Boring Variable": "BORING", "Nuclear Launch Codes": "9001", @@ -2296,10 +2297,11 @@ func TestDeployCreate_GenerationOfAutomationCommand_MasksSensitiveVariables(t *t Deploy Time: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment + Priority: Jump the task queue Package Download: Use cached packages (if available) Deployment Targets: All included - Automation Command: octopus release deploy --space 'Default Space' --project 'Fire Project' --version '2.0' --environment 'dev' --variable 'Boring Variable:BORING' --variable 'Nuclear Launch Codes:*****' --variable 'Secret Password:*****' --no-prompt + Automation Command: octopus release deploy --space 'Default Space' --project 'Fire Project' --version '2.0' --environment 'dev' --priority 'true' --variable 'Boring Variable:BORING' --variable 'Nuclear Launch Codes:*****' --variable 'Secret Password:*****' --no-prompt Warning: Command includes some sensitive variable values which have been replaced with placeholders. Successfully started 2 deployment(s) @@ -2322,6 +2324,7 @@ func TestDeployCreate_PrintAdvancedSummary(t *testing.T) { Deploy Time: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment + Priority: Use default setting Package Download: Use cached packages (if available) Deployment Targets: All included `), stdout.String()) @@ -2331,6 +2334,7 @@ func TestDeployCreate_PrintAdvancedSummary(t *testing.T) { options := &executor.TaskOptionsDeployRelease{ ScheduledStartTime: "2022-09-23", GuidedFailureMode: "false", + Priority: "true", ForcePackageDownload: true, ExcludedSteps: []string{"Step 1", "Step 37"}, DeploymentTargets: []string{"vm-1", "vm-2"}, @@ -2343,6 +2347,7 @@ func TestDeployCreate_PrintAdvancedSummary(t *testing.T) { Deploy Time: 2022-09-23 Skipped Steps: Step 1,Step 37 Guided Failure Mode: Do not use guided failure mode + Priority: Jump the task queue Package Download: Re-download packages from feed Deployment Targets: Include vm-1,vm-2; Exclude vm-3,vm-4 `), stdout.String()) @@ -2359,6 +2364,7 @@ func TestDeployCreate_PrintAdvancedSummary(t *testing.T) { Deploy Time: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment + Priority: Use default setting Package Download: Use cached packages (if available) Deployment Targets: Include vm-2 `), stdout.String()) @@ -2375,6 +2381,7 @@ func TestDeployCreate_PrintAdvancedSummary(t *testing.T) { Deploy Time: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment + Priority: Use default setting Package Download: Use cached packages (if available) Deployment Targets: Exclude vm-4 `), stdout.String()) diff --git a/pkg/cmd/runbook/run/run.go b/pkg/cmd/runbook/run/run.go index 20455c5f..073a745b 100644 --- a/pkg/cmd/runbook/run/run.go +++ b/pkg/cmd/runbook/run/run.go @@ -175,7 +175,7 @@ func NewCmdRun(f factory.Factory) *cobra.Command { flags.StringVarP(&runFlags.Snapshot.Value, runFlags.Snapshot.Name, "", "", "Name or ID of the snapshot to run. If not supplied, the command will attempt to use the published snapshot.") flags.StringArrayVarP(&runFlags.ExcludedSteps.Value, runFlags.ExcludedSteps.Name, "", nil, "Exclude specific steps from the runbook") flags.StringVarP(&runFlags.GuidedFailureMode.Value, runFlags.GuidedFailureMode.Name, "", "", "Enable Guided failure mode (true/false/default)") - flags.StringVarP(&runFlags.Priority.Value, runFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature and the TaskPrioritize permission.") + flags.StringVarP(&runFlags.Priority.Value, runFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature. Runbook runs have no lifecycle default, so 'default' behaves as 'false'.") flags.BoolVarP(&runFlags.ForcePackageDownload.Value, runFlags.ForcePackageDownload.Name, "", false, "Force re-download of packages") flags.StringArrayVarP(&runFlags.RunTargets.Value, runFlags.RunTargets.Name, "", nil, "Run on this target (can be specified multiple times)") flags.StringArrayVarP(&runFlags.ExcludeTargets.Value, runFlags.ExcludeTargets.Name, "", nil, "Run on targets except for this (can be specified multiple times)") @@ -1258,6 +1258,8 @@ func PrintAdvancedSummary(stdout io.Writer, options *executor.TaskOptionsRunbook gfmStr := executionscommon.LookupGuidedFailureModeString(options.GuidedFailureMode) + priorityStr := executionscommon.LookupPriorityString(options.Priority) + pkgDownloadStr := executionscommon.LookupPackageDownloadString(!options.ForcePackageDownload) runTargetsStr := "All included" @@ -1293,9 +1295,10 @@ func PrintAdvancedSummary(stdout io.Writer, options *executor.TaskOptionsRunbook Run At: cyan(%s) Skipped Steps: cyan(%s) Guided Failure Mode: cyan(%s) + Priority: cyan(%s) Package Download: cyan(%s) Run Targets: cyan(%s) - `)), runAtStr, skipStepsStr, gfmStr, pkgDownloadStr, runTargetsStr) + `)), runAtStr, skipStepsStr, gfmStr, priorityStr, pkgDownloadStr, runTargetsStr) } func selectRunbook(octopus *octopusApiClient.Client, ask question.Asker, questionText string, space *spaces.Space, project *projects.Project) (*runbooks.Runbook, error) { diff --git a/pkg/cmd/runbook/run/run_test.go b/pkg/cmd/runbook/run/run_test.go index a67d17e0..a32c4bb3 100644 --- a/pkg/cmd/runbook/run/run_test.go +++ b/pkg/cmd/runbook/run/run_test.go @@ -732,6 +732,7 @@ func TestRunbookRun_PrintAdvancedSummary(t *testing.T) { Run At: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment + Priority: Use default setting Package Download: Use cached packages (if available) Run Targets: All included `), stdout.String()) @@ -742,6 +743,7 @@ func TestRunbookRun_PrintAdvancedSummary(t *testing.T) { TaskOptionsRunbookRunBase: executor.TaskOptionsRunbookRunBase{ ScheduledStartTime: "2022-09-23", GuidedFailureMode: "false", + Priority: "true", ForcePackageDownload: true, ExcludedSteps: []string{"Step 1", "Step 37"}, RunTargets: []string{"vm-1", "vm-2"}, @@ -755,6 +757,7 @@ func TestRunbookRun_PrintAdvancedSummary(t *testing.T) { Run At: 2022-09-23 Skipped Steps: Step 1,Step 37 Guided Failure Mode: Do not use guided failure mode + Priority: Jump the task queue Package Download: Re-download packages from feed Run Targets: Include vm-1,vm-2; Exclude vm-3,vm-4 `), stdout.String()) @@ -773,6 +776,7 @@ func TestRunbookRun_PrintAdvancedSummary(t *testing.T) { Run At: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment + Priority: Use default setting Package Download: Use cached packages (if available) Run Targets: Include vm-2 `), stdout.String()) @@ -791,6 +795,7 @@ func TestRunbookRun_PrintAdvancedSummary(t *testing.T) { Run At: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment + Priority: Use default setting Package Download: Use cached packages (if available) Run Targets: Exclude vm-4 `), stdout.String()) diff --git a/pkg/executionscommon/executionscommon.go b/pkg/executionscommon/executionscommon.go index 4348d7bd..acf13eb7 100644 --- a/pkg/executionscommon/executionscommon.go +++ b/pkg/executionscommon/executionscommon.go @@ -338,6 +338,19 @@ func LookupGuidedFailureModeString(value string) string { } } +func LookupPriorityString(value string) string { + switch value { + case "", "default": + return "Use default setting" + case "true", "True": + return "Jump the task queue" + case "false", "False": + return "Do not jump the task queue" + default: + return fmt.Sprintf("Unknown %s", value) + } +} + func LookupPackageDownloadString(value bool) string { if value { return "Use cached packages (if available)" diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go index b91e3e8e..b717fb45 100644 --- a/pkg/executor/executor.go +++ b/pkg/executor/executor.go @@ -37,7 +37,7 @@ func NewTask(taskType TaskType, options any) *Task { } // parsePriorityMode maps the CLI's tri-state priority value onto the server's PriorityMode. -// An empty result means 'not specified', which leaves the server to apply the lifecycle default. +// An empty result is omitted from the request, leaving the server to apply its own default. func parsePriorityMode(value string) (string, error) { b, err := strconv.ParseBool(value) if err == nil { @@ -49,7 +49,7 @@ func parsePriorityMode(value string) (string, error) { if value == "" || strings.EqualFold("default", value) { return "", nil } - return "", fmt.Errorf("'%s' is not a valid value for priority", value) + return "", fmt.Errorf("'%s' is not a valid value for priority, expected true, false or default", value) } // ProcessTasks iterates over the list of tasks and attempts to run them all. From 752af58dd09025059fa522ac5ec226965babddcc Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 19 Aug 2026 17:48:12 +1000 Subject: [PATCH 3/8] Point at the go-octopusdeploy branch so CI can build this Temporary pseudo-version pin. Swap to the tagged release once go-octopusdeploy#460 is merged, before this comes out of draft. Co-Authored-By: Claude Opus 5 (1M context) --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a46e3a7b..de8ddcf1 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/OctopusDeploy/go-octodiff v1.0.0 - github.com/OctopusDeploy/go-octopusdeploy/v2 v2.114.1 + github.com/OctopusDeploy/go-octopusdeploy/v2 v2.115.1-0.20260819065856-b6f0f290712d github.com/bmatcuk/doublestar/v4 v4.10.0 github.com/briandowns/spinner v1.23.2 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 15f5c58f..ace87230 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4 github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU= github.com/OctopusDeploy/go-octopusdeploy/v2 v2.114.1 h1:7lrYQSCo2HeixFRBGGKA8a7QjuBw9L+4A4kmUDUEzSE= github.com/OctopusDeploy/go-octopusdeploy/v2 v2.114.1/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.115.1-0.20260819065856-b6f0f290712d h1:iCMRfyFo2hKQ0zu95s5T9FUwQQ3ucvQxrjZ8So6uM5c= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.115.1-0.20260819065856-b6f0f290712d/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= From 070fe9c8e88d9bacf5e4ec2e7924bc5d19d1414d Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 20 Aug 2026 15:10:42 +1000 Subject: [PATCH 4/8] Tidy go.sum The pseudo-version bump added the new hashes without dropping the v2.114.1 ones. Co-Authored-By: Claude Opus 5 (1M context) --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index ace87230..0f9615d4 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0= github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.114.1 h1:7lrYQSCo2HeixFRBGGKA8a7QjuBw9L+4A4kmUDUEzSE= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.114.1/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus= github.com/OctopusDeploy/go-octopusdeploy/v2 v2.115.1-0.20260819065856-b6f0f290712d h1:iCMRfyFo2hKQ0zu95s5T9FUwQQ3ucvQxrjZ8So6uM5c= github.com/OctopusDeploy/go-octopusdeploy/v2 v2.115.1-0.20260819065856-b6f0f290712d/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= From 32f157effb3dd50ae28451e402e3ba892ba5fe1d Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 20 Aug 2026 15:12:26 +1000 Subject: [PATCH 5/8] Rename the --priority default test to match what it asserts The body comparison deserialises through the same struct tags, so an absent field and an empty one both land on "". The test proves default maps to empty rather than to LifecycleDefault, which is the useful part, but the old name claimed more than that. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/cmd/release/deploy/deploy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/release/deploy/deploy_test.go b/pkg/cmd/release/deploy/deploy_test.go index 238b214e..fdff522e 100644 --- a/pkg/cmd/release/deploy/deploy_test.go +++ b/pkg/cmd/release/deploy/deploy_test.go @@ -1899,7 +1899,7 @@ func TestDeployCreate_AutomationMode(t *testing.T) { assert.Equal(t, "", stdErr.String()) }}, - {"release deploy with --priority default omits Priority so the server uses the lifecycle setting", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { + {"release deploy with --priority default leaves Priority unset", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) { defer api.Close() rootCmd.SetArgs([]string{"release", "deploy", "--project", fireProject.Name, "--version", "1.0", "--environment", "dev", "--priority", "default", "--output-format", "basic"}) From 3e3a2ce22f0554a0bf003dba28ea922db3583056 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 20 Aug 2026 15:17:46 +1000 Subject: [PATCH 6/8] Make the priority help and summary accurate per command release deploy really does assert TaskPrioritize, so putting the permission back there and only dropping it from runbook run. The summary said "Use default setting" on both, which contradicted the runbook help saying default behaves as false, so each command now supplies its own wording. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/cmd/release/deploy/deploy.go | 4 ++-- pkg/cmd/release/deploy/deploy_test.go | 6 +++--- pkg/cmd/runbook/run/run.go | 4 ++-- pkg/cmd/runbook/run/run_test.go | 6 +++--- pkg/executionscommon/executionscommon.go | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/release/deploy/deploy.go b/pkg/cmd/release/deploy/deploy.go index b036df0f..21353869 100644 --- a/pkg/cmd/release/deploy/deploy.go +++ b/pkg/cmd/release/deploy/deploy.go @@ -173,7 +173,7 @@ func NewCmdDeploy(f factory.Factory) *cobra.Command { flags.BoolVarP(&deployFlags.UpdateVariables.Value, deployFlags.UpdateVariables.Name, "", false, "Overwrite the release variable snapshot by re-importing variables from the project.") flags.StringArrayVarP(&deployFlags.ExcludedSteps.Value, deployFlags.ExcludedSteps.Name, "", nil, "Exclude specific steps from the deployment") flags.StringVarP(&deployFlags.GuidedFailureMode.Value, deployFlags.GuidedFailureMode.Name, "", "", "Enable Guided failure mode (true/false/default)") - flags.StringVarP(&deployFlags.Priority.Value, deployFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature.") + flags.StringVarP(&deployFlags.Priority.Value, deployFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature, and the TaskPrioritize permission to set true.") flags.BoolVarP(&deployFlags.ForcePackageDownload.Value, deployFlags.ForcePackageDownload.Name, "", false, "Force re-download of packages") flags.StringArrayVarP(&deployFlags.DeploymentTargets.Value, deployFlags.DeploymentTargets.Name, "", nil, "Deploy to this target (can be specified multiple times)") flags.StringArrayVarP(&deployFlags.ExcludeTargets.Value, deployFlags.ExcludeTargets.Name, "", nil, "Deploy to targets except for this (can be specified multiple times)") @@ -1041,7 +1041,7 @@ func PrintAdvancedSummary(stdout io.Writer, options *executor.TaskOptionsDeployR gfmStr := executionscommon.LookupGuidedFailureModeString(options.GuidedFailureMode) - priorityStr := executionscommon.LookupPriorityString(options.Priority) + priorityStr := executionscommon.LookupPriorityString(options.Priority, "Use default setting from the lifecycle phase") pkgDownloadStr := executionscommon.LookupPackageDownloadString(!options.ForcePackageDownload) diff --git a/pkg/cmd/release/deploy/deploy_test.go b/pkg/cmd/release/deploy/deploy_test.go index fdff522e..27ed339c 100644 --- a/pkg/cmd/release/deploy/deploy_test.go +++ b/pkg/cmd/release/deploy/deploy_test.go @@ -2324,7 +2324,7 @@ func TestDeployCreate_PrintAdvancedSummary(t *testing.T) { Deploy Time: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment - Priority: Use default setting + Priority: Use default setting from the lifecycle phase Package Download: Use cached packages (if available) Deployment Targets: All included `), stdout.String()) @@ -2364,7 +2364,7 @@ func TestDeployCreate_PrintAdvancedSummary(t *testing.T) { Deploy Time: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment - Priority: Use default setting + Priority: Use default setting from the lifecycle phase Package Download: Use cached packages (if available) Deployment Targets: Include vm-2 `), stdout.String()) @@ -2381,7 +2381,7 @@ func TestDeployCreate_PrintAdvancedSummary(t *testing.T) { Deploy Time: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment - Priority: Use default setting + Priority: Use default setting from the lifecycle phase Package Download: Use cached packages (if available) Deployment Targets: Exclude vm-4 `), stdout.String()) diff --git a/pkg/cmd/runbook/run/run.go b/pkg/cmd/runbook/run/run.go index 073a745b..40420dac 100644 --- a/pkg/cmd/runbook/run/run.go +++ b/pkg/cmd/runbook/run/run.go @@ -175,7 +175,7 @@ func NewCmdRun(f factory.Factory) *cobra.Command { flags.StringVarP(&runFlags.Snapshot.Value, runFlags.Snapshot.Name, "", "", "Name or ID of the snapshot to run. If not supplied, the command will attempt to use the published snapshot.") flags.StringArrayVarP(&runFlags.ExcludedSteps.Value, runFlags.ExcludedSteps.Name, "", nil, "Exclude specific steps from the runbook") flags.StringVarP(&runFlags.GuidedFailureMode.Value, runFlags.GuidedFailureMode.Name, "", "", "Enable Guided failure mode (true/false/default)") - flags.StringVarP(&runFlags.Priority.Value, runFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature. Runbook runs have no lifecycle default, so 'default' behaves as 'false'.") + flags.StringVarP(&runFlags.Priority.Value, runFlags.Priority.Name, "", "", "Jump the task queue ahead of other queued tasks (true/false/default). Requires the Priority Tasks feature. For runbook runs, 'default' is the same as 'false'.") flags.BoolVarP(&runFlags.ForcePackageDownload.Value, runFlags.ForcePackageDownload.Name, "", false, "Force re-download of packages") flags.StringArrayVarP(&runFlags.RunTargets.Value, runFlags.RunTargets.Name, "", nil, "Run on this target (can be specified multiple times)") flags.StringArrayVarP(&runFlags.ExcludeTargets.Value, runFlags.ExcludeTargets.Name, "", nil, "Run on targets except for this (can be specified multiple times)") @@ -1258,7 +1258,7 @@ func PrintAdvancedSummary(stdout io.Writer, options *executor.TaskOptionsRunbook gfmStr := executionscommon.LookupGuidedFailureModeString(options.GuidedFailureMode) - priorityStr := executionscommon.LookupPriorityString(options.Priority) + priorityStr := executionscommon.LookupPriorityString(options.Priority, "Do not jump the task queue") pkgDownloadStr := executionscommon.LookupPackageDownloadString(!options.ForcePackageDownload) diff --git a/pkg/cmd/runbook/run/run_test.go b/pkg/cmd/runbook/run/run_test.go index a32c4bb3..4febc4e2 100644 --- a/pkg/cmd/runbook/run/run_test.go +++ b/pkg/cmd/runbook/run/run_test.go @@ -732,7 +732,7 @@ func TestRunbookRun_PrintAdvancedSummary(t *testing.T) { Run At: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment - Priority: Use default setting + Priority: Do not jump the task queue Package Download: Use cached packages (if available) Run Targets: All included `), stdout.String()) @@ -776,7 +776,7 @@ func TestRunbookRun_PrintAdvancedSummary(t *testing.T) { Run At: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment - Priority: Use default setting + Priority: Do not jump the task queue Package Download: Use cached packages (if available) Run Targets: Include vm-2 `), stdout.String()) @@ -795,7 +795,7 @@ func TestRunbookRun_PrintAdvancedSummary(t *testing.T) { Run At: Now Skipped Steps: None Guided Failure Mode: Use default setting from the target environment - Priority: Use default setting + Priority: Do not jump the task queue Package Download: Use cached packages (if available) Run Targets: Exclude vm-4 `), stdout.String()) diff --git a/pkg/executionscommon/executionscommon.go b/pkg/executionscommon/executionscommon.go index acf13eb7..241ea8bd 100644 --- a/pkg/executionscommon/executionscommon.go +++ b/pkg/executionscommon/executionscommon.go @@ -338,10 +338,10 @@ func LookupGuidedFailureModeString(value string) string { } } -func LookupPriorityString(value string) string { +func LookupPriorityString(value string, defaultDescription string) string { switch value { case "", "default": - return "Use default setting" + return defaultDescription case "true", "True": return "Jump the task queue" case "false", "False": From 37d0244d75f9a6f6f8812973751697ed2d853ddd Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 20 Aug 2026 15:21:23 +1000 Subject: [PATCH 7/8] Validate --priority up front and derive its summary text from the parser The summary hand-listed six values while the parser accepted ParseBool's full set, so --priority 1 printed "Unknown 1" and then worked. It now shares the parser, which also moves to executionscommon alongside the guided-failure lookups. Validating in the command means a bad value fails immediately instead of after the whole interactive prompt and a generated command that could not work. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/cmd/release/deploy/deploy.go | 4 ++++ pkg/cmd/release/deploy/deploy_test.go | 6 +---- pkg/cmd/runbook/run/run.go | 4 ++++ pkg/executionscommon/executionscommon.go | 30 +++++++++++++++++++----- pkg/executor/executor.go | 18 -------------- pkg/executor/release.go | 3 ++- pkg/executor/runbook.go | 5 ++-- 7 files changed, 38 insertions(+), 32 deletions(-) diff --git a/pkg/cmd/release/deploy/deploy.go b/pkg/cmd/release/deploy/deploy.go index 21353869..12c2a25d 100644 --- a/pkg/cmd/release/deploy/deploy.go +++ b/pkg/cmd/release/deploy/deploy.go @@ -208,6 +208,10 @@ func deployRun(cmd *cobra.Command, f factory.Factory, flags *DeployFlags) error outputFormat = constants.OutputFormatTable } + if _, err = executionscommon.ParsePriorityMode(flags.Priority.Value); err != nil { + return err + } + octopus, err := f.GetSpacedClient(apiclient.NewRequester(cmd)) if err != nil { return err diff --git a/pkg/cmd/release/deploy/deploy_test.go b/pkg/cmd/release/deploy/deploy_test.go index 27ed339c..65a91514 100644 --- a/pkg/cmd/release/deploy/deploy_test.go +++ b/pkg/cmd/release/deploy/deploy_test.go @@ -1936,17 +1936,13 @@ func TestDeployCreate_AutomationMode(t *testing.T) { assert.Equal(t, "", stdErr.String()) }}, - {"release deploy rejects an unrecognised --priority value", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { + {"release deploy rejects an unrecognised --priority value before contacting the server", func(t *testing.T, api *testutil.MockHttpServer, rootCmd *cobra.Command, stdOut *bytes.Buffer, stdErr *bytes.Buffer) { cmdReceiver := testutil.GoBegin2(func() (*cobra.Command, error) { defer api.Close() rootCmd.SetArgs([]string{"release", "deploy", "--project", fireProject.Name, "--version", "1.0", "--environment", "dev", "--priority", "urgent"}) return rootCmd.ExecuteC() }) - api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource) - api.ExpectRequest(t, "GET", "/api/Spaces-1").RespondWith(rootResource) - api.ExpectRequest(t, "GET", "/api/Spaces-1/projects/"+fireProject.GetName()).RespondWith(fireProject) - _, err := testutil.ReceivePair(cmdReceiver) assert.EqualError(t, err, "'urgent' is not a valid value for priority, expected true, false or default") diff --git a/pkg/cmd/runbook/run/run.go b/pkg/cmd/runbook/run/run.go index 40420dac..c1ac4afb 100644 --- a/pkg/cmd/runbook/run/run.go +++ b/pkg/cmd/runbook/run/run.go @@ -215,6 +215,10 @@ func runbookRun(cmd *cobra.Command, f factory.Factory, flags *RunFlags) error { outputFormat = constants.OutputFormatTable } + if _, err = executionscommon.ParsePriorityMode(flags.Priority.Value); err != nil { + return err + } + octopus, err := f.GetSpacedClient(apiclient.NewRequester(cmd)) if err != nil { return err diff --git a/pkg/executionscommon/executionscommon.go b/pkg/executionscommon/executionscommon.go index 241ea8bd..91af36fa 100644 --- a/pkg/executionscommon/executionscommon.go +++ b/pkg/executionscommon/executionscommon.go @@ -338,16 +338,34 @@ func LookupGuidedFailureModeString(value string) string { } } +// ParsePriorityMode maps the CLI's tri-state priority value onto the server's PriorityMode. +// An empty result is omitted from the request, leaving the server to apply its own default. +func ParsePriorityMode(value string) (string, error) { + b, err := strconv.ParseBool(value) + if err == nil { + if b { + return "On", nil + } + return "Off", nil + } + if value == "" || strings.EqualFold("default", value) { + return "", nil + } + return "", fmt.Errorf("'%s' is not a valid value for priority, expected true, false or default", value) +} + func LookupPriorityString(value string, defaultDescription string) string { - switch value { - case "", "default": - return defaultDescription - case "true", "True": + priority, err := ParsePriorityMode(value) + if err != nil { + return fmt.Sprintf("Unknown %s", value) + } + switch priority { + case "On": return "Jump the task queue" - case "false", "False": + case "Off": return "Do not jump the task queue" default: - return fmt.Sprintf("Unknown %s", value) + return defaultDescription } } diff --git a/pkg/executor/executor.go b/pkg/executor/executor.go index b717fb45..80fed5c0 100644 --- a/pkg/executor/executor.go +++ b/pkg/executor/executor.go @@ -2,8 +2,6 @@ package executor import ( "fmt" - "strconv" - "strings" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/spaces" @@ -36,22 +34,6 @@ func NewTask(taskType TaskType, options any) *Task { } } -// parsePriorityMode maps the CLI's tri-state priority value onto the server's PriorityMode. -// An empty result is omitted from the request, leaving the server to apply its own default. -func parsePriorityMode(value string) (string, error) { - b, err := strconv.ParseBool(value) - if err == nil { - if b { - return "On", nil - } - return "Off", nil - } - if value == "" || strings.EqualFold("default", value) { - return "", nil - } - return "", fmt.Errorf("'%s' is not a valid value for priority, expected true, false or default", value) -} - // ProcessTasks iterates over the list of tasks and attempts to run them all. // If everything goes well, a nil error will be returned. // On the first failure, the error will be returned and the process will halt. diff --git a/pkg/executor/release.go b/pkg/executor/release.go index ee42e35f..b3390ad7 100644 --- a/pkg/executor/release.go +++ b/pkg/executor/release.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/OctopusDeploy/cli/pkg/executionscommon" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deployments" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/releases" @@ -172,7 +173,7 @@ func releaseDeploy(octopus *client.Client, space *spaces.Space, input any) error } } - abstractCmd.Priority, err = parsePriorityMode(params.Priority) + abstractCmd.Priority, err = executionscommon.ParsePriorityMode(params.Priority) if err != nil { return err } diff --git a/pkg/executor/runbook.go b/pkg/executor/runbook.go index 886f1a25..35244f1a 100644 --- a/pkg/executor/runbook.go +++ b/pkg/executor/runbook.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/OctopusDeploy/cli/pkg/executionscommon" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/deployments" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/runbooks" @@ -97,7 +98,7 @@ func runbookRun(octopus *client.Client, space *spaces.Space, input any) error { } } - abstractCmd.Priority, err = parsePriorityMode(params.Priority) + abstractCmd.Priority, err = executionscommon.ParsePriorityMode(params.Priority) if err != nil { return err } @@ -177,7 +178,7 @@ func gitRunbookRun(octopus *client.Client, space *spaces.Space, input any) error } } - abstractCmd.Priority, err = parsePriorityMode(params.Priority) + abstractCmd.Priority, err = executionscommon.ParsePriorityMode(params.Priority) if err != nil { return err } From ab92a8916ce9edbb2dc9f582cf48a56b40779773 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 25 Aug 2026 15:43:57 +1000 Subject: [PATCH 8/8] Swap the pseudo-version pin for go-octopusdeploy v2.116.0 go-octopusdeploy#460 merged and shipped in v2.116.0, so the temporary pin from 752af58 can go. Co-Authored-By: Claude Opus 5 (1M context) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index de8ddcf1..e11b84b0 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/OctopusDeploy/go-octodiff v1.0.0 - github.com/OctopusDeploy/go-octopusdeploy/v2 v2.115.1-0.20260819065856-b6f0f290712d + github.com/OctopusDeploy/go-octopusdeploy/v2 v2.116.0 github.com/bmatcuk/doublestar/v4 v4.10.0 github.com/briandowns/spinner v1.23.2 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 0f9615d4..275e45a7 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OctopusDeploy/go-octodiff v1.0.0 h1:U+ORg6azniwwYo+O44giOw6TiD5USk8S4VDhOQ0Ven0= github.com/OctopusDeploy/go-octodiff v1.0.0/go.mod h1:Mze0+EkOWTgTmi8++fyUc6r0aLZT7qD9gX+31t8MmIU= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.115.1-0.20260819065856-b6f0f290712d h1:iCMRfyFo2hKQ0zu95s5T9FUwQQ3ucvQxrjZ8So6uM5c= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.115.1-0.20260819065856-b6f0f290712d/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.116.0 h1:kW1H9qngKgI34OkfYN6/PFpjBEQRK/tZm70MCElHOME= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.116.0/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=