From 0051257ca8a6e68778676def39aeb207af829eb5 Mon Sep 17 00:00:00 2001 From: Nick Josevski Date: Thu, 28 May 2026 19:49:30 +1000 Subject: [PATCH 1/3] Handle MostRecentlyPublished channel rule strategy in BuildPackageVersionBaselineForChannel When a channel version rule has VersioningStrategy "MostRecentlyPublished", send versioningStrategy and versionTagRegex query parameters to the package feed instead of preReleaseTag and versionRange. This ensures the server uses publish-date ordering to suggest the latest package version, supporting non-SemVer versioning schemes. Depends on go-octopusdeploy PR #421 (VersioningStrategy/VersionTagRegex fields on ChannelRule and SearchPackageVersionsQuery). go.mod will be updated to the new go-octopusdeploy release once that PR is merged and tagged. --- pkg/cmd/release/create/create.go | 9 +++- pkg/cmd/release/create/create_test.go | 62 +++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index 9c79f4dc..8e4c6f46 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -394,8 +394,13 @@ func BuildPackageVersionBaselineForChannel(octopus *octopusApiClient.Client, dep for _, ap := range rule.ActionPackages { if ap.PackageReference == packageRef.PackageReferenceName && ap.DeploymentAction == packageRef.ActionName { // this rule applies to our step/packageref combo - query.PreReleaseTag = rule.Tag - query.VersionRange = rule.VersionRange + if rule.VersioningStrategy == "MostRecentlyPublished" { + query.VersioningStrategy = rule.VersioningStrategy + query.VersionTagRegex = rule.VersionTagRegex + } else { + query.PreReleaseTag = rule.Tag + query.VersionRange = rule.VersionRange + } // the octopus server won't let the same package be targeted by more than one rule, so // once we've found the first matching rule for our step+package, we can stop looping break rulesLoop diff --git a/pkg/cmd/release/create/create_test.go b/pkg/cmd/release/create/create_test.go index 864ae37e..8d2cf1b5 100644 --- a/pkg/cmd/release/create/create_test.go +++ b/pkg/cmd/release/create/create_test.go @@ -2284,6 +2284,68 @@ func TestReleaseCreate_BuildPackageVersionBaseline(t *testing.T) { }, packageVersions) }) + t.Run("uses versioningStrategy and versionTagRegex query params when channel rule has MostRecentlyPublished strategy", func(t *testing.T) { + api := testutil.NewMockHttpServer() + processTemplate := &deployments.DeploymentProcessTemplate{ + Packages: []releases.ReleaseTemplatePackage{ + { + ActionName: "Deploy", + FeedID: externalFeedID, + PackageID: "my-app", + PackageReferenceName: "my-app-on-deploy", + IsResolvable: true, + }, + }, + Resource: resources.Resource{}, + } + + channel := fixtures.NewChannel(spaceID, "Channels-1", "Default", "Projects-1") + channel.Rules = []channels.ChannelRule{ + { + VersioningStrategy: "MostRecentlyPublished", + VersionTagRegex: "^feature-", + ActionPackages: []octopusPackages.DeploymentActionPackage{ + {DeploymentAction: "Deploy", PackageReference: "my-app-on-deploy"}, + }, + }, + } + + receiver := testutil.GoBegin2(func() ([]*packages.StepPackageVersion, error) { + defer api.Close() + octopus, _ := octopusApiClient.NewClient(testutil.NewMockHttpClientWithTransport(api), serverUrl, placeholderApiKey, "") + return create.BuildPackageVersionBaselineForChannel(octopus, processTemplate, channel) + }) + + api.ExpectRequest(t, "GET", "/api/").RespondWith(rootResource) + api.ExpectRequest(t, "GET", "/api/spaces").RespondWith(rootResource) + + api.ExpectRequest(t, "GET", "/api/Spaces-1/feeds?ids=Feeds-1001&take=1").RespondWith(&feeds.Feeds{Items: []feeds.IFeed{ + &feeds.FeedResource{Name: "External Nuget", FeedType: feeds.FeedTypeNuGet, Resource: resources.Resource{ + ID: externalFeedID, + Links: map[string]string{ + constants.LinkSearchPackageVersionsTemplate: "/api/Spaces-1/feeds/Feeds-1001/packages/versions{?packageId,take,skip,includePreRelease,versionRange,preReleaseTag,versioningStrategy,versionTagRegex,filter,includeReleaseNotes}", + }}}, + }}) + + // must send versioningStrategy and versionTagRegex — must NOT send preReleaseTag or versionRange + api.ExpectRequest(t, "GET", "/api/Spaces-1/feeds/Feeds-1001/packages/versions?packageId=my-app&take=1&versionTagRegex=%5Efeature-&versioningStrategy=MostRecentlyPublished").RespondWith(&resources.Resources[*octopusPackages.PackageVersion]{ + Items: []*octopusPackages.PackageVersion{ + {PackageID: "my-app", Version: "feature-login-42"}, + }, + }) + + packageVersions, err := testutil.ReceivePair(receiver) + assert.Nil(t, err) + assert.Equal(t, []*packages.StepPackageVersion{ + { + PackageID: "my-app", + ActionName: "Deploy", + Version: "feature-login-42", + PackageReferenceName: "my-app-on-deploy", + }, + }, packageVersions) + }) + t.Run("still returns a value if the server returns zero available packages", func(t *testing.T) { // note: channel rules can affect which packages the server returns so there might be zero, // but either way the server returns zero items, which is the code path we are testing, so channel rules aren't relevant here From f22ac9994b732a7f3767f91ea43326fff9a814ef Mon Sep 17 00:00:00 2001 From: Nick Josevski Date: Mon, 15 Jun 2026 18:01:46 +1000 Subject: [PATCH 2/3] Apply all channel version-rule filters together regardless of strategy Follow-on to PR #608 to match OctopusDeploy #44073: VersionRange, Tag and VersionTagRegex are now always applied together; VersioningStrategy only changes ordering (publish-date vs SemVer), not which versions satisfy the rule. Replace the earlier either/or branching that dropped Tag/VersionRange under MostRecentlyPublished and never sent VersionTagRegex under SemVer. Update the test to assert all four params are sent together, and correct the expected query-param order to match uritemplates expansion (template order). go.mod still pins the client that lacks these fields, so this does not yet compile; bump pending the go-octopusdeploy release (see TODO). Co-Authored-By: Claude Opus 4.8 --- go.mod | 3 +++ pkg/cmd/release/create/create.go | 17 +++++++++-------- pkg/cmd/release/create/create_test.go | 11 ++++++++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index a2f4f3ac..6952947a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,9 @@ 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 + // TODO: bump to the go-octopusdeploy release that adds VersioningStrategy and VersionTagRegex + // to channels.ChannelRule and feeds.SearchPackageVersionsQuery (non-semver-ordering feature). + // The current pinned version lacks these fields, so create.go does not yet compile. github.com/OctopusDeploy/go-octopusdeploy/v2 v2.108.1 github.com/bmatcuk/doublestar/v4 v4.4.0 github.com/briandowns/spinner v1.19.0 diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index 8e4c6f46..72d08abb 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -393,14 +393,15 @@ func BuildPackageVersionBaselineForChannel(octopus *octopusApiClient.Client, dep for _, rule := range channel.Rules { for _, ap := range rule.ActionPackages { if ap.PackageReference == packageRef.PackageReferenceName && ap.DeploymentAction == packageRef.ActionName { - // this rule applies to our step/packageref combo - if rule.VersioningStrategy == "MostRecentlyPublished" { - query.VersioningStrategy = rule.VersioningStrategy - query.VersionTagRegex = rule.VersionTagRegex - } else { - query.PreReleaseTag = rule.Tag - query.VersionRange = rule.VersionRange - } + // this rule applies to our step/packageref combo. + // VersionRange, Tag/pre-release and VersionTagRegex are always applied together, + // regardless of strategy; VersioningStrategy only changes ordering (publish-date + // vs SemVer), not which versions satisfy the rule. (empty fields are dropped by + // the query's omitempty uri tags) + query.PreReleaseTag = rule.Tag + query.VersionRange = rule.VersionRange + query.VersionTagRegex = rule.VersionTagRegex + query.VersioningStrategy = rule.VersioningStrategy // the octopus server won't let the same package be targeted by more than one rule, so // once we've found the first matching rule for our step+package, we can stop looping break rulesLoop diff --git a/pkg/cmd/release/create/create_test.go b/pkg/cmd/release/create/create_test.go index 8d2cf1b5..c69bbe05 100644 --- a/pkg/cmd/release/create/create_test.go +++ b/pkg/cmd/release/create/create_test.go @@ -2284,7 +2284,10 @@ func TestReleaseCreate_BuildPackageVersionBaseline(t *testing.T) { }, packageVersions) }) - t.Run("uses versioningStrategy and versionTagRegex query params when channel rule has MostRecentlyPublished strategy", func(t *testing.T) { + t.Run("sends all version-rule filters together along with versioningStrategy, regardless of strategy", func(t *testing.T) { + // VersionRange, Tag/pre-release and VersionTagRegex are always applied together; VersioningStrategy + // only changes ordering (publish-date vs SemVer), not which versions satisfy the rule. So even with + // MostRecentlyPublished set, an accompanying VersionRange and Tag must still be sent. api := testutil.NewMockHttpServer() processTemplate := &deployments.DeploymentProcessTemplate{ Packages: []releases.ReleaseTemplatePackage{ @@ -2302,6 +2305,8 @@ func TestReleaseCreate_BuildPackageVersionBaseline(t *testing.T) { channel := fixtures.NewChannel(spaceID, "Channels-1", "Default", "Projects-1") channel.Rules = []channels.ChannelRule{ { + VersionRange: "1.0", + Tag: "beta", VersioningStrategy: "MostRecentlyPublished", VersionTagRegex: "^feature-", ActionPackages: []octopusPackages.DeploymentActionPackage{ @@ -2327,8 +2332,8 @@ func TestReleaseCreate_BuildPackageVersionBaseline(t *testing.T) { }}}, }}) - // must send versioningStrategy and versionTagRegex — must NOT send preReleaseTag or versionRange - api.ExpectRequest(t, "GET", "/api/Spaces-1/feeds/Feeds-1001/packages/versions?packageId=my-app&take=1&versionTagRegex=%5Efeature-&versioningStrategy=MostRecentlyPublished").RespondWith(&resources.Resources[*octopusPackages.PackageVersion]{ + // all four filters must be sent; query param order follows the uri template variable order + api.ExpectRequest(t, "GET", "/api/Spaces-1/feeds/Feeds-1001/packages/versions?packageId=my-app&take=1&versionRange=1.0&preReleaseTag=beta&versioningStrategy=MostRecentlyPublished&versionTagRegex=%5Efeature-").RespondWith(&resources.Resources[*octopusPackages.PackageVersion]{ Items: []*octopusPackages.PackageVersion{ {PackageID: "my-app", Version: "feature-login-42"}, }, From 5d13c5a2ea657e1157e1a7f87d9067ee4020106a Mon Sep 17 00:00:00 2001 From: Nick Josevski Date: Wed, 15 Jul 2026 16:49:36 +1000 Subject: [PATCH 3/3] Bump go-octopusdeploy to v2.112.1 for non-SemVer ordering fields v2.112.1 adds VersioningStrategy and VersionTagRegex to channels.ChannelRule and feeds.SearchPackageVersionsQuery (go-octopusdeploy#421), which BuildPackageVersionBaselineForChannel already consumes. Removes the go.mod TODO now that the release exists. Corrects the expected query string in create_test.go: the client emits query params alphabetically, confirmed against the real client build. Co-Authored-By: Claude Opus 4.8 --- go.mod | 5 +---- go.sum | 4 ++-- pkg/cmd/release/create/create_test.go | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 6952947a..5f1f67dd 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +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 - // TODO: bump to the go-octopusdeploy release that adds VersioningStrategy and VersionTagRegex - // to channels.ChannelRule and feeds.SearchPackageVersionsQuery (non-semver-ordering feature). - // The current pinned version lacks these fields, so create.go does not yet compile. - github.com/OctopusDeploy/go-octopusdeploy/v2 v2.108.1 + github.com/OctopusDeploy/go-octopusdeploy/v2 v2.112.1 github.com/bmatcuk/doublestar/v4 v4.4.0 github.com/briandowns/spinner v1.19.0 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index 56c25587..267c49f3 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,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.108.1 h1:THcIJGQyhhzSx8/b7ibYnyFlwktF8/U+xwXfC373N1U= -github.com/OctopusDeploy/go-octopusdeploy/v2 v2.108.1/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.112.1 h1:a/UEgCu7F8BbY+95W10aQ5VYhlf5jP/LVigZOH0eu/k= +github.com/OctopusDeploy/go-octopusdeploy/v2 v2.112.1/go.mod h1:VkTXDoIPbwGFi5+goo1VSwFNdMVo784cVtJdKIEvfus= github.com/bmatcuk/doublestar/v4 v4.4.0 h1:LmAwNwhjEbYtyVLzjcP/XeVw4nhuScHGkF/XWXnvIic= github.com/bmatcuk/doublestar/v4 v4.4.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E= diff --git a/pkg/cmd/release/create/create_test.go b/pkg/cmd/release/create/create_test.go index c69bbe05..65ee97c7 100644 --- a/pkg/cmd/release/create/create_test.go +++ b/pkg/cmd/release/create/create_test.go @@ -2332,8 +2332,8 @@ func TestReleaseCreate_BuildPackageVersionBaseline(t *testing.T) { }}}, }}) - // all four filters must be sent; query param order follows the uri template variable order - api.ExpectRequest(t, "GET", "/api/Spaces-1/feeds/Feeds-1001/packages/versions?packageId=my-app&take=1&versionRange=1.0&preReleaseTag=beta&versioningStrategy=MostRecentlyPublished&versionTagRegex=%5Efeature-").RespondWith(&resources.Resources[*octopusPackages.PackageVersion]{ + // all four filters must be sent; the client emits query params in alphabetical order + api.ExpectRequest(t, "GET", "/api/Spaces-1/feeds/Feeds-1001/packages/versions?packageId=my-app&preReleaseTag=beta&take=1&versionRange=1.0&versionTagRegex=%5Efeature-&versioningStrategy=MostRecentlyPublished").RespondWith(&resources.Resources[*octopusPackages.PackageVersion]{ Items: []*octopusPackages.PackageVersion{ {PackageID: "my-app", Version: "feature-login-42"}, },