From 0f08519cfa1c32ac4850077b889a1b581b713b13 Mon Sep 17 00:00:00 2001 From: Alastair Pitts Date: Mon, 19 Oct 2026 17:03:10 +1100 Subject: [PATCH 1/5] fix: Try get environment by partial name when promoting --- pkg/executor/release.go | 83 ++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/pkg/executor/release.go b/pkg/executor/release.go index 5ce0c6fc..95078bf6 100644 --- a/pkg/executor/release.go +++ b/pkg/executor/release.go @@ -276,30 +276,13 @@ func releasePromote(octopus *client.Client, space *spaces.Space, input any) erro return fmt.Errorf("project not found. %v", err) } - envs, err := environments.Get(octopus, space.ID, environments.EnvironmentQuery{ - IDs: []string{params.FromEnvironment}, - Skip: 0, - Take: math.MaxInt32, - }) + fromEnv, err := getEnvironment(octopus, space, err, params.FromEnvironment) if err != nil { - return fmt.Errorf("environment not found. %v", err) - } - // - if len(envs.Items) == 0 { - envs, err = environments.Get(octopus, space.ID, environments.EnvironmentQuery{ - Name: params.FromEnvironment, - Skip: 0, - Take: math.MaxInt32, - }) - if err != nil || len(envs.Items) == 0 { - return fmt.Errorf("environment not found. %v", err) - } + return err } - // just grab the first environment - sourceEnv := envs.Items[0] dashboardQuery := dashboard.DashboardDynamicQuery{ - Environments: []string{sourceEnv.ID}, + Environments: []string{fromEnv.ID}, Projects: []string{proj.ID}, // If using latest successful, we need previous dashboard items as well // so that we can ensure we get a previous successful deployment @@ -315,7 +298,7 @@ func releasePromote(octopus *client.Client, space *spaces.Space, input any) erro // this is probably unnecessary, but it mimics the existing logic var dashboardItems []*dashboard.DashboardItem for _, item := range dynamicDashboard.Items { - if item.EnvironmentID == sourceEnv.ID && item.ProjectID == proj.ID { + if item.EnvironmentID == fromEnv.ID && item.ProjectID == proj.ID { dashboardItems = append(dashboardItems, item) } } @@ -376,3 +359,61 @@ func compareDashboardItemsByRelease(a, b *dashboard.DashboardItem) int { // we multiply by -1 to order by descending return versioning.Compare(aVersion, bVersion, (*versioning.OctopusVersion).Compare) * -1 } + +func getEnvironment(octopus *client.Client, space *spaces.Space, err error, nameOrID string) (*environments.Environment, error) { + envResponse, err := environments.Get(octopus, space.ID, environments.EnvironmentQuery{ + IDs: []string{nameOrID}, + Skip: 0, + Take: math.MaxInt32, + }) + if err != nil { + return nil, fmt.Errorf("environment not found. %v", err) + } + + // fallback on checking by name + if len(envResponse.Items) == 0 { + envResponse, err = environments.Get(octopus, space.ID, environments.EnvironmentQuery{ + Name: nameOrID, + Skip: 0, + Take: math.MaxInt32, + }) + if err != nil { + return nil, fmt.Errorf("environment not found. %v", err) + } + } + + envItems := envResponse.Items + + // older versions of Server didn't support name, so try partial name + if len(envItems) == 0 { + envResponse, err = environments.Get(octopus, space.ID, environments.EnvironmentQuery{ + Skip: 0, + Take: math.MaxInt32, + PartialName: nameOrID, + }) + if err != nil { + return nil, fmt.Errorf("environment not found. %v", err) + } + + var matchingEnv *environments.Environment + for _, item := range envResponse.Items { + // find an exact match to the partial match responses + if item.Name == nameOrID { + matchingEnv = item + break + } + } + + // if we didn't exact match on name, we must not have found anything + if matchingEnv == nil { + return nil, fmt.Errorf("environment not found") + } + + // update the envItems with the single item + envItems = []*environments.Environment{matchingEnv} + + } + + // just grab the first environment + return envItems[0], nil +} From 4b99c27b9ad2280ebf616bc48607789012450943 Mon Sep 17 00:00:00 2001 From: Alastair Pitts Date: Mon, 19 Oct 2026 17:09:31 +1100 Subject: [PATCH 2/5] Tweak logic --- pkg/executor/release.go | 43 +++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/pkg/executor/release.go b/pkg/executor/release.go index 95078bf6..3297afa9 100644 --- a/pkg/executor/release.go +++ b/pkg/executor/release.go @@ -370,6 +370,11 @@ func getEnvironment(octopus *client.Client, space *spaces.Space, err error, name return nil, fmt.Errorf("environment not found. %v", err) } + // return the matching item (as it's by ID) + if len(envResponse.Items) == 1 { + return envResponse.Items[0], nil + } + // fallback on checking by name if len(envResponse.Items) == 0 { envResponse, err = environments.Get(octopus, space.ID, environments.EnvironmentQuery{ @@ -382,38 +387,22 @@ func getEnvironment(octopus *client.Client, space *spaces.Space, err error, name } } - envItems := envResponse.Items - - // older versions of Server didn't support name, so try partial name - if len(envItems) == 0 { - envResponse, err = environments.Get(octopus, space.ID, environments.EnvironmentQuery{ - Skip: 0, - Take: math.MaxInt32, - PartialName: nameOrID, - }) - if err != nil { - return nil, fmt.Errorf("environment not found. %v", err) - } - - var matchingEnv *environments.Environment + // if we have multiple responses, this means that the `Name` filter didn't work + // so we do the filtering ourselves + switch { + case len(envResponse.Items) == 0: + return nil, fmt.Errorf("environment not found") + case len(envResponse.Items) == 1: + return envResponse.Items[0], nil + case len(envResponse.Items) > 1: for _, item := range envResponse.Items { // find an exact match to the partial match responses if item.Name == nameOrID { - matchingEnv = item - break + return item, nil } } - - // if we didn't exact match on name, we must not have found anything - if matchingEnv == nil { - return nil, fmt.Errorf("environment not found") - } - - // update the envItems with the single item - envItems = []*environments.Environment{matchingEnv} - } - // just grab the first environment - return envItems[0], nil + // final fallback + return nil, fmt.Errorf("environment not found") } From 5db3059d36a4f5a91fbb755b8ac919b2b40c8ece Mon Sep 17 00:00:00 2001 From: Alastair Pitts Date: Tue, 20 Oct 2026 09:07:05 +1100 Subject: [PATCH 3/5] Add some extra checks --- pkg/executor/release.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/executor/release.go b/pkg/executor/release.go index 3297afa9..38f42cb8 100644 --- a/pkg/executor/release.go +++ b/pkg/executor/release.go @@ -364,14 +364,14 @@ func getEnvironment(octopus *client.Client, space *spaces.Space, err error, name envResponse, err := environments.Get(octopus, space.ID, environments.EnvironmentQuery{ IDs: []string{nameOrID}, Skip: 0, - Take: math.MaxInt32, + Take: 1, // we are matching on ID, so there should only be one }) if err != nil { return nil, fmt.Errorf("environment not found. %v", err) } - // return the matching item (as it's by ID) - if len(envResponse.Items) == 1 { + // double check the ID and return if true + if len(envResponse.Items) == 1 && envResponse.Items[0].ID == nameOrID { return envResponse.Items[0], nil } @@ -392,7 +392,7 @@ func getEnvironment(octopus *client.Client, space *spaces.Space, err error, name switch { case len(envResponse.Items) == 0: return nil, fmt.Errorf("environment not found") - case len(envResponse.Items) == 1: + case len(envResponse.Items) == 1 && envResponse.Items[0].Name == nameOrID: return envResponse.Items[0], nil case len(envResponse.Items) > 1: for _, item := range envResponse.Items { From f54b4111ba91e71232659373db2e5cff1c570f5b Mon Sep 17 00:00:00 2001 From: Alastair Pitts Date: Tue, 20 Oct 2026 09:11:02 +1100 Subject: [PATCH 4/5] Fix tests --- pkg/cmd/release/promote/promote_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/cmd/release/promote/promote_test.go b/pkg/cmd/release/promote/promote_test.go index 03dabfdb..af29c229 100644 --- a/pkg/cmd/release/promote/promote_test.go +++ b/pkg/cmd/release/promote/promote_test.go @@ -90,7 +90,7 @@ func TestPromoteRelease_Success(t *testing.T) { }) // Lookup from environment by ID - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -151,7 +151,7 @@ func TestPromoteRelease_Success(t *testing.T) { }) // Lookup from environment by ID - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -212,7 +212,7 @@ func TestPromoteRelease_Success(t *testing.T) { }) // Lookup from environment by ID - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -261,7 +261,7 @@ func TestPromoteRelease_Success(t *testing.T) { }) // Lookup from environment by ID - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -310,7 +310,7 @@ func TestPromoteRelease_Success(t *testing.T) { }) // Lookup from environment by ID - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -371,7 +371,7 @@ func TestPromoteRelease_Success(t *testing.T) { }) // Lookup from environment by ID - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -420,7 +420,7 @@ func TestPromoteRelease_Success(t *testing.T) { }) // Lookup from environment by ID - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -469,7 +469,7 @@ func TestPromoteRelease_Success(t *testing.T) { }) // Lookup from environment by ID - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -518,7 +518,7 @@ func TestPromoteRelease_Success(t *testing.T) { }) // Lookup from environment by ID - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -606,7 +606,7 @@ func TestPromoteRelease_Errors(t *testing.T) { RespondWith(resources.Resources[*projects.Project]{ Items: []*projects.Project{fireProject}, }) - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=NonExistent&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=NonExistent&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -628,7 +628,7 @@ func TestPromoteRelease_Errors(t *testing.T) { RespondWith(resources.Resources[*projects.Project]{ Items: []*projects.Project{fireProject}, }) - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) @@ -653,7 +653,7 @@ func TestPromoteRelease_Errors(t *testing.T) { RespondWith(resources.Resources[*projects.Project]{ Items: []*projects.Project{fireProject}, }) - api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=2147483647"). + api.ExpectRequest(t, "GET", "/api/"+spaceID+"/environments/v2?name=&skip=0&ids=Development&take=1"). RespondWith(resources.Resources[*environments.Environment]{ Items: []*environments.Environment{}, }) From 5fa8d16d2154b49156f5905a46c0b2144e37e60d Mon Sep 17 00:00:00 2001 From: Alastair Pitts Date: Tue, 20 Oct 2026 09:14:48 +1100 Subject: [PATCH 5/5] Change to case-insensitive name checks --- pkg/executor/release.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/executor/release.go b/pkg/executor/release.go index 38f42cb8..313d3069 100644 --- a/pkg/executor/release.go +++ b/pkg/executor/release.go @@ -392,12 +392,12 @@ func getEnvironment(octopus *client.Client, space *spaces.Space, err error, name switch { case len(envResponse.Items) == 0: return nil, fmt.Errorf("environment not found") - case len(envResponse.Items) == 1 && envResponse.Items[0].Name == nameOrID: + case len(envResponse.Items) == 1 && strings.EqualFold(envResponse.Items[0].Name, nameOrID): return envResponse.Items[0], nil case len(envResponse.Items) > 1: for _, item := range envResponse.Items { // find an exact match to the partial match responses - if item.Name == nameOrID { + if strings.EqualFold(item.Name, nameOrID) { return item, nil } }