From 57d127e618fc588ad45c5d21ac5512b2367b4ec4 Mon Sep 17 00:00:00 2001 From: robert Date: Tue, 10 Nov 2026 12:23:00 +1000 Subject: [PATCH 1/3] test: use DeployLatestRelease action in scheduled trigger test Octopus Server now rejects the AutoDeploy action on a OnceDailySchedule trigger. --- test/e2e/project_trigger_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/project_trigger_test.go b/test/e2e/project_trigger_test.go index 7e94e794..ddf902aa 100644 --- a/test/e2e/project_trigger_test.go +++ b/test/e2e/project_trigger_test.go @@ -52,8 +52,8 @@ func CreateTestProjectTrigger(t *testing.T, client *client.Client, project *proj start := time.Date(now.Year(), now.Month(), now.Day(), 9, 0, 0, 0, time.UTC) // days := []filters.Weekday{filters.Sunday, filters.Monday} - action := actions.NewAutoDeployAction(createRandomBoolean()) - // action := actions.NewDeployLatestReleaseAction(internal.GetRandomName(), createRandomBoolean(), []string{internal.GetRandomName()}, "") + // Octopus rejects the AutoDeploy action on a scheduled trigger. + action := actions.NewDeployLatestReleaseAction(environment.GetID(), createRandomBoolean(), []string{environment.GetID()}, "") // action := actions.NewDeployNewReleaseAction(environment.GetID(), "", nil) // filter := filters.NewDeploymentTargetFilter([]string{}, []string{}, []string{"MachineAvailableForDeployment"}, []string{}) From 94517412169d375833f1aec03cf158ef8f7deb49 Mon Sep 17 00:00:00 2001 From: Rob E Date: Tue, 10 Nov 2026 12:33:46 +1000 Subject: [PATCH 2/3] Update project_trigger_test.go --- test/e2e/project_trigger_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/project_trigger_test.go b/test/e2e/project_trigger_test.go index ddf902aa..27e4c72a 100644 --- a/test/e2e/project_trigger_test.go +++ b/test/e2e/project_trigger_test.go @@ -52,7 +52,6 @@ func CreateTestProjectTrigger(t *testing.T, client *client.Client, project *proj start := time.Date(now.Year(), now.Month(), now.Day(), 9, 0, 0, 0, time.UTC) // days := []filters.Weekday{filters.Sunday, filters.Monday} - // Octopus rejects the AutoDeploy action on a scheduled trigger. action := actions.NewDeployLatestReleaseAction(environment.GetID(), createRandomBoolean(), []string{environment.GetID()}, "") // action := actions.NewDeployNewReleaseAction(environment.GetID(), "", nil) // filter := filters.NewDeploymentTargetFilter([]string{}, []string{}, []string{"MachineAvailableForDeployment"}, []string{}) From d09a7fc3d1191f2971e1327dbf1205ee7f0cdbc3 Mon Sep 17 00:00:00 2001 From: robert Date: Tue, 3 Nov 2026 15:46:43 +1000 Subject: [PATCH 3/3] feat: add claim conditions to service account OIDC identities --- pkg/serviceaccounts/oidc_identities.go | 12 ++++++ pkg/serviceaccounts/oidc_identities_test.go | 42 +++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/pkg/serviceaccounts/oidc_identities.go b/pkg/serviceaccounts/oidc_identities.go index cd13234c..29008c54 100644 --- a/pkg/serviceaccounts/oidc_identities.go +++ b/pkg/serviceaccounts/oidc_identities.go @@ -14,12 +14,24 @@ type OIDCIdentityQuery struct { Take int `uri:"take" url:"take"` } +// OIDCIdentityClaimCondition is a condition a token claim must pass before Octopus exchanges the token for an access token. +// The token passes the condition when its claim matches any one of the values. Values support the same wildcards as the subject. +type OIDCIdentityClaimCondition struct { + Claim string `json:"Claim"` + Values []string `json:"Values"` +} + type OIDCIdentity struct { Audience string `json:"Audience,omitempty"` Issuer string `json:"Issuer"` Name string `json:"Name"` ServiceAccountID string `json:"ServiceAccountId"` Subject string `json:"Subject"` + + // ClaimConditions has no omitempty on purpose. + // On update, a nil slice is sent as null and Octopus keeps the existing conditions. + // An empty slice is sent as [] and Octopus removes them. + ClaimConditions []OIDCIdentityClaimCondition `json:"ClaimConditions"` resources.Resource } diff --git a/pkg/serviceaccounts/oidc_identities_test.go b/pkg/serviceaccounts/oidc_identities_test.go index 89dc3610..ea44d79b 100644 --- a/pkg/serviceaccounts/oidc_identities_test.go +++ b/pkg/serviceaccounts/oidc_identities_test.go @@ -34,3 +34,45 @@ func TestOIDCIdentity_MarshalJSON_IncludesAudienceWhenSet(t *testing.T) { assert.Equal(t, "api://custom", result["Audience"]) } + +func TestOIDCIdentity_MarshalJSON_SendsNullClaimConditionsWhenUnset(t *testing.T) { + identity := NewOIDCIdentity("ServiceAccounts-1", "name", "issuer", "subject") + + data, err := json.Marshal(identity) + assert.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + assert.NoError(t, err) + + value, hasClaimConditions := result["ClaimConditions"] + assert.True(t, hasClaimConditions) + assert.Nil(t, value, "A nil slice must be sent as null so Octopus keeps the existing conditions") +} + +func TestOIDCIdentity_MarshalJSON_SendsEmptyClaimConditionsToClearThem(t *testing.T) { + identity := NewOIDCIdentity("ServiceAccounts-1", "name", "issuer", "subject") + identity.ClaimConditions = []OIDCIdentityClaimCondition{} + + data, err := json.Marshal(identity) + assert.NoError(t, err) + + assert.Contains(t, string(data), `"ClaimConditions":[]`) +} + +func TestOIDCIdentity_JSONRoundTrip_ClaimConditions(t *testing.T) { + identity := NewOIDCIdentity("ServiceAccounts-1", "name", "issuer", "subject") + identity.ClaimConditions = []OIDCIdentityClaimCondition{ + {Claim: "repository_owner_id", Values: []string{"123456"}}, + {Claim: "environment", Values: []string{"prod", "staging-*"}}, + } + + data, err := json.Marshal(identity) + assert.NoError(t, err) + + var result OIDCIdentity + err = json.Unmarshal(data, &result) + assert.NoError(t, err) + + assert.Equal(t, identity.ClaimConditions, result.ClaimConditions) +}