Repository: akuity/kargo

Issue:
controller: try to make step timeout somewhat more accurate
Numerous discussions have taken place over the last few months re: configurability of the retry interval for step retries and timeouts, possibly not being honored.

I'll borrow my own recent explanation for the difficulty behind this from #3515:

> the timeouts are not exact because steps do not continuously retry internally. If the timeout hasn't elapsed, a step that's still running (waiting on something external) is retried on the next reconciliation attempt.
> 
> In general, those attempts are every five minutes, but the next attempt can be sooner if a related resource has a state change that forces the Promotion back onto the queue. It can also be later depending on the depth of the queue.
>
> ...
>
> We're a little bit at the mercy of the controller runtime here since we don't have precise control over the interval before the next reconciliation...
>
> That said, we can probably get closer to the specified timeout by shortening the requeue interval when timeout is sooner than when the next reconciliation attempt would typically be.

If someone wishes to tackle this, it should only be a few lines of code [right around here](https://github.com/akuity/kargo/blob/e2cdf0110122c6aa2740fed01c0cf0aca60cc4e7/internal/controller/promotions/promotions.go#L419-L421).

We can keep the hard coded five minutes, but _shorten it_ by the appropriate amount if the running step's start time + timeout is sooner than now + five minutes.

Note: At this time, I'm still hesitant to apply a similar strategy for making retry intervals configurable because I think our inability to offer much precision there is more problematic than it is with timeouts. Let's, at least, consider it out of scope for this issue.

Hints from issue comments:
@ashish111333: @krancour  can I help with this one ? @krancour: @ashish111333, go for it. And thank you!@krancour: @ashish111333 if you don't have the bandwidth for this, please let me know, as I believe @fuskovic will have the bandwidth, but I don't want to yank the rug out from under you if you're already working on this.

Solution patch:
diff --git a/Dockerfile b/Dockerfile
index 1f67d139..b9f5458a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -61,7 +61,7 @@ WORKDIR /kargo/bin
 ####################################################################################################
 # `tools` stage allows us to take the leverage of the parallel build.
 # For example, this stage can be cached and re-used when we have to rebuild code base.
-FROM curlimages/curl:8.12.1 AS tools
+FROM curlimages/curl:8.13.0 AS tools
 
 ARG TARGETOS
 ARG TARGETARCH
diff --git a/charts/kargo/README.md b/charts/kargo/README.md
index 40211d99..2bdd5502 100644
--- a/charts/kargo/README.md
+++ b/charts/kargo/README.md
@@ -111,6 +111,7 @@ the Kargo controller is running.
 | `api.oidc.clientID`                         | The client ID for the OIDC client. If Dex is enabled, this value will be ignored and the client ID will be automatically configured. If Dex is not enabled, this should be set to the client ID provided to you by your identity provider.                                                                                                                                                                                                                                                                                                                                                                                                                                          | `nil`                    |
 | `api.oidc.cliClientID`                      | The client ID for the OIDC client used by CLI (optional). Needed by some OIDC providers (such as Dex) that require a separate Client ID for web app login vs. CLI login (`http://localhost`). If Dex is enabled, this value will be ignored and cli client ID will be automatically configured. If Dex is not enabled, and a different client app is configured for localhost CLI login, this should be the client ID configured in the IdP.                                                                                                                                                                                                                                        | `nil`                    |
 | `api.oidc.additionalScopes`                 | The additional scopes to send to the OIDC provider. This should be set to the scopes you wish to be provided to your identity provider from clients of Kargo, the scopes openid, profile and email are always requested and don't need to be added, this value is intended for any additional ones you require.                                                                                                                                                                                                                                                                                                                                                                     | `["groups"]`             |
+| `api.oidc.usernameClaim`                    | The claim to use as the username for the user.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `email`                  |
 | `api.oidc.admins.claims`                    | Subjects having any of these claims will automatically be Kargo admins.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{}`                     |
 | `api.oidc.viewers.claims`                   | Subjects having any of these claims will automatically receive read-only access to all Kargo resources.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{}`                     |
 | `api.oidc.globalServiceAccounts.namespaces` | List of namespaces to look for shared service accounts.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `[]`                     |
diff --git a/charts/kargo/templates/api/configmap.yaml b/charts/kargo/templates/api/configmap.yaml
index ece6b8c6..cc369b68 100644
--- a/charts/kargo/templates/api/configmap.yaml
+++ b/charts/kargo/templates/api/configmap.yaml
@@ -49,6 +49,7 @@ data:
   {{- if .Values.api.oidc.cliClientID }}
   OIDC_CLI_CLIENT_ID: {{ quote .Values.api.oidc.cliClientID }}
   {{- end }}
+  OIDC_USERNAME_CLAIM: {{ quote .Values.api.oidc.usernameClaim | default "email" }}
   {{- end }}
   {{- end }}
   {{- if .Values.api.argocd.urls }}
diff --git a/charts/kargo/values.yaml b/charts/kargo/values.yaml
index 295be553..35f44c83 100755
--- a/charts/kargo/values.yaml
+++ b/charts/kargo/values.yaml
@@ -211,6 +211,8 @@ api:
     ## @param api.oidc.additionalScopes The additional scopes to send to the OIDC provider. This should be set to the scopes you wish to be provided to your identity provider from clients of Kargo, the scopes openid, profile and email are always requested and don't need to be added, this value is intended for any additional ones you require.
     additionalScopes:
     - groups
+    ## @param api.oidc.usernameClaim The claim to use as the username for the user.
+    usernameClaim: email
 
     admins:
       ## @param api.oidc.admins.claims Subjects having any of these claims will automatically be Kargo admins.
diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/compose-output.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/compose-output.md
index 9156823f..11c4e2fa 100644
--- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/compose-output.md
+++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/compose-output.md
@@ -66,7 +66,7 @@ steps:
             "type": "section",
             "text": {
               "type": "mrkdwn",
-              "text": "A new PR has been opened: ${{ outputs['pr-link'].url }}"
+              "text": "A new PR has been opened: ${{ task.outputs['pr-link'].url }}"
             }
           }
         ]
diff --git a/go.mod b/go.mod
index 61c61424..b2432183 100644
--- a/go.mod
+++ b/go.mod
@@ -18,10 +18,10 @@ require (
 	github.com/akuity/kargo/api v0.0.0
 	github.com/akuity/kargo/pkg v0.0.0
 	github.com/aws/aws-sdk-go-v2 v1.36.3
-	github.com/aws/aws-sdk-go-v2/config v1.29.13
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.66
-	github.com/aws/aws-sdk-go-v2/service/ecr v1.43.2
-	github.com/aws/aws-sdk-go-v2/service/sts v1.33.18
+	github.com/aws/aws-sdk-go-v2/config v1.29.14
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.67
+	github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.19
 	github.com/bacongobbler/browser v1.1.0
 	github.com/bmatcuk/doublestar/v4 v4.8.1
 	github.com/bombsimon/logrusr/v4 v4.1.0
@@ -31,7 +31,7 @@ require (
 	github.com/expr-lang/expr v1.17.2
 	github.com/fatih/structtag v1.2.0
 	github.com/fluxcd/pkg/kustomize v1.16.0
-	github.com/go-git/go-git/v5 v5.14.0
+	github.com/go-git/go-git/v5 v5.15.0
 	github.com/go-logr/logr v1.4.2
 	github.com/gogo/protobuf v1.3.2
 	github.com/golang-jwt/jwt/v5 v5.2.2
@@ -59,7 +59,7 @@ require (
 	gitlab.com/gitlab-org/api/client-go v0.127.0
 	go.uber.org/ratelimit v0.3.1
 	golang.org/x/crypto v0.37.0
-	golang.org/x/net v0.38.0
+	golang.org/x/net v0.39.0
 	golang.org/x/oauth2 v0.29.0
 	golang.org/x/sync v0.13.0
 	golang.org/x/text v0.24.0
diff --git a/go.sum b/go.sum
index 7d5b7bc9..04bbe752 100644
--- a/go.sum
+++ b/go.sum
@@ -52,10 +52,10 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
 github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
-github.com/aws/aws-sdk-go-v2/config v1.29.13 h1:RgdPqWoE8nPpIekpVpDJsBckbqT4Liiaq9f35pbTh1Y=
-github.com/aws/aws-sdk-go-v2/config v1.29.13/go.mod h1:NI28qs/IOUIRhsR7GQ/JdexoqRN9tDxkIrYZq0SOF44=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.66 h1:aKpEKaTy6n4CEJeYI1MNj97oSDLi4xro3UzQfwf5RWE=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.66/go.mod h1:xQ5SusDmHb/fy55wU0QqTy0yNfLqxzec59YcsRZB+rI=
+github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
+github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
@@ -64,8 +64,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0io
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
-github.com/aws/aws-sdk-go-v2/service/ecr v1.43.2 h1:G6Rj8A8zgukXWSSB8evVE8az2S3Ci/Q3CpSMxJsK4G0=
-github.com/aws/aws-sdk-go-v2/service/ecr v1.43.2/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 h1:YyH8Hk73bYzdbvf6S8NF5z/fb/1stpiMnFSfL6jSfRA=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
@@ -74,8 +74,8 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2
 github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.18 h1:xz7WvTMfSStb9Y8NpCT82FXLNC3QasqBfuAFHY4Pk5g=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.18/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
 github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
 github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/bacongobbler/browser v1.1.0 h1:6YTctUlzcApit1vpWgh+myjh8lQUyQRD2Ltoyvy2EoM=
@@ -186,8 +186,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
 github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
 github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
-github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
-github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
+github.com/go-git/go-git/v5 v5.15.0 h1:f5Qn0W0F7ry1iN0ZwIU5m/n7/BKB4hiZfc+zlZx7ly0=
+github.com/go-git/go-git/v5 v5.15.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
 github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
 github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
 github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
@@ -577,8 +577,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
 golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index 05b8661f..7ed9f5c2 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -3,9 +3,9 @@ module tools
 go 1.24.1
 
 require (
-	github.com/bufbuild/buf v1.51.0
+	github.com/bufbuild/buf v1.52.1
 	github.com/golangci/golangci-lint v1.64.8
-	golang.org/x/tools v0.31.0
+	golang.org/x/tools v0.32.0
 	helm.sh/helm/v3 v3.17.3
 	k8s.io/code-generator v0.32.3
 	sigs.k8s.io/controller-tools v0.17.3
@@ -14,21 +14,21 @@ require (
 require (
 	4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
 	4d63.com/gochecknoglobals v0.2.2 // indirect
-	buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.5-20250121211742-6d880cc6cc8d.1 // indirect
-	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1 // indirect
+	buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 // indirect
+	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1 // indirect
 	buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 // indirect
-	buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.5-20250116203702-1c024d64352b.1 // indirect
-	buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 // indirect
+	buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250116203702-1c024d64352b.1 // indirect
+	buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 // indirect
 	buf.build/go/bufplugin v0.8.0 // indirect
-	buf.build/go/protoyaml v0.3.1 // indirect
+	buf.build/go/protoyaml v0.3.2 // indirect
 	buf.build/go/spdx v0.2.0 // indirect
-	cel.dev/expr v0.21.2 // indirect
+	cel.dev/expr v0.23.1 // indirect
 	connectrpc.com/connect v1.18.1 // indirect
 	connectrpc.com/otelconnect v0.7.2 // indirect
 	dario.cat/mergo v1.0.1 // indirect
 	github.com/4meepo/tagalign v1.4.2 // indirect
 	github.com/Abirdcfly/dupword v0.1.3 // indirect
-	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
+	github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
 	github.com/Antonboom/errname v1.0.0 // indirect
 	github.com/Antonboom/nilnil v1.0.1 // indirect
 	github.com/Antonboom/testifylint v1.5.2 // indirect
@@ -63,8 +63,8 @@ require (
 	github.com/breml/bidichk v0.3.2 // indirect
 	github.com/breml/errchkjson v0.4.0 // indirect
 	github.com/bufbuild/protocompile v0.14.1 // indirect
-	github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9 // indirect
-	github.com/bufbuild/protovalidate-go v0.9.3-0.20250317160558-38a17488914d // indirect
+	github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
+	github.com/bufbuild/protovalidate-go v0.9.3-0.20250403190939-663657418457 // indirect
 	github.com/butuzov/ireturn v0.3.1 // indirect
 	github.com/butuzov/mirror v1.3.0 // indirect
 	github.com/catenacyber/perfsprint v0.8.2 // indirect
@@ -88,10 +88,10 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/denis-tingaikin/go-header v0.5.0 // indirect
 	github.com/distribution/reference v0.6.0 // indirect
-	github.com/docker/cli v27.5.1+incompatible // indirect
+	github.com/docker/cli v28.0.4+incompatible // indirect
 	github.com/docker/distribution v2.8.3+incompatible // indirect
-	github.com/docker/docker v28.0.2+incompatible // indirect
-	github.com/docker/docker-credential-helpers v0.8.2 // indirect
+	github.com/docker/docker v28.0.4+incompatible // indirect
+	github.com/docker/docker-credential-helpers v0.9.3 // indirect
 	github.com/docker/go-connections v0.5.0 // indirect
 	github.com/docker/go-metrics v0.0.1 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
@@ -146,7 +146,7 @@ require (
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/go-containerregistry v0.20.3 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect
+	github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/gordonklaus/ineffassign v0.1.0 // indirect
@@ -216,7 +216,7 @@ require (
 	github.com/moby/sys/mountinfo v0.7.2 // indirect
 	github.com/moby/sys/reexec v0.1.0 // indirect
 	github.com/moby/sys/sequential v0.6.0 // indirect
-	github.com/moby/sys/user v0.3.0 // indirect
+	github.com/moby/sys/user v0.4.0 // indirect
 	github.com/moby/sys/userns v0.1.0 // indirect
 	github.com/moby/term v0.5.2 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -231,9 +231,9 @@ require (
 	github.com/nishanths/predeclared v0.2.2 // indirect
 	github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
-	github.com/onsi/ginkgo/v2 v2.22.2 // indirect
+	github.com/onsi/ginkgo/v2 v2.23.4 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/image-spec v1.1.0 // indirect
+	github.com/opencontainers/image-spec v1.1.1 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
@@ -315,30 +315,30 @@ require (
 	go.lsp.dev/protocol v0.12.0 // indirect
 	go.lsp.dev/uri v0.3.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
-	go.opentelemetry.io/otel v1.34.0 // indirect
-	go.opentelemetry.io/otel/metric v1.34.0 // indirect
-	go.opentelemetry.io/otel/trace v1.34.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.uber.org/mock v0.5.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
 	go.uber.org/zap/exp v0.3.0 // indirect
-	golang.org/x/crypto v0.36.0 // indirect
-	golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect
+	golang.org/x/crypto v0.37.0 // indirect
+	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
 	golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
 	golang.org/x/mod v0.24.0 // indirect
-	golang.org/x/net v0.37.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
 	golang.org/x/oauth2 v0.26.0 // indirect
-	golang.org/x/sync v0.12.0 // indirect
-	golang.org/x/sys v0.31.0 // indirect
-	golang.org/x/term v0.30.0 // indirect
-	golang.org/x/text v0.23.0 // indirect
+	golang.org/x/sync v0.13.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
+	golang.org/x/term v0.31.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
 	golang.org/x/time v0.10.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 // indirect
-	google.golang.org/grpc v1.70.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 // indirect
+	google.golang.org/grpc v1.71.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index 2c81adee..685da4d8 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -2,24 +2,24 @@
 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
-buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.5-20250121211742-6d880cc6cc8d.1 h1:z/NYWpgoeKkKL3+LYF+8QK58Rjz3qkMAshpdzJTaJ7o=
-buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.5-20250121211742-6d880cc6cc8d.1/go.mod h1:LpnZWZGTs6IBCnY9WHAkR9X4/NbpL5nwOXivQdXILTs=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1 h1:j+l4+E1EEo83GVIxuqinfFOTyImSQUH90WfufE86xaI=
-buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1/go.mod h1:eOqrCVUfhh7SLo00urDe/XhJHljj0dWMZirS0aX7cmc=
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 h1:f6miF8tK6H+Ktad24WpnNfpHO75GRGk0rhJ1mxPXqgA=
+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1/go.mod h1:rvbyamNtvJ4o3ExeCmaG5/6iHnu0vy0E+UQ+Ph0om8s=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1 h1:zgJPqo17m28+Lf5BW4xv3PvU20BnrmTcGYrog22lLIU=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
 buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 h1:1SDs5tEGoWWv2vmKLx2B0Bp+yfhlxiU4DaZUII8+Pvs=
 buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1/go.mod h1:o2AgVM1j3MczvxnMqfZTpiqGwK1VD4JbEagseY0QcjE=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.5-20250116203702-1c024d64352b.1 h1:MTNYELBBDEj2ddEwWb/vuAm5PRLyWtZe7CLnc6WZ5qQ=
-buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.5-20250116203702-1c024d64352b.1/go.mod h1:E8bt4dG1/NfuocvVmlDNWIfKoLK0B4AgGq4ubwEGBvo=
-buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 h1:XmYgi9W/9oST2ZrfT3ucGWkzD9+Vd0ls9yhyZ8ae0KQ=
-buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1/go.mod h1:cxFpqWIC80Wm8YNo1038ocBmrF84uQ0IfL0uVdAu9ZY=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250116203702-1c024d64352b.1 h1:O1sbHpYA7yAIZpDWSEw0mNibv1gov2KH8mSzPruCNhk=
+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250116203702-1c024d64352b.1/go.mod h1:ee69ieBAzwc/oY/Vde0K4r6JWvrk093q4Z/FXexPMmA=
+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 h1:trcsXBDm8exui7mvndZnvworCyBq1xuMnod2N0j79K8=
+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1/go.mod h1:OUbhXurY+VHFGn9FBxcRy8UB7HXk9NvJ2qCgifOMypQ=
 buf.build/go/bufplugin v0.8.0 h1:YgR1+CNGmzR69jt85oRWTa5FioZoX/tOrHV+JxfNnnk=
 buf.build/go/bufplugin v0.8.0/go.mod h1:rcm0Esd3P/GM2rtYTvz3+9Gf8w9zdo7rG8dKSxYHHIE=
-buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4=
-buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg=
+buf.build/go/protoyaml v0.3.2 h1:QJF3k7btMameIadLLcK3Rry81OK3gYA5nZMXirV1Bs4=
+buf.build/go/protoyaml v0.3.2/go.mod h1:rUlMqwfZeONS/BAt00wB6jV5ay/eHXUzxgiKSIyrvyc=
 buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
 buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
-cel.dev/expr v0.21.2 h1:o+Wj235dy4gFYlYin3JsMpp3EEfMrPm/6tdoyjT98S0=
-cel.dev/expr v0.21.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
+cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
 connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
 connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
 connectrpc.com/otelconnect v0.7.2 h1:WlnwFzaW64dN06JXU+hREPUGeEzpz3Acz2ACOmN8cMI=
@@ -32,8 +32,8 @@ github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E
 github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
 github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
 github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
 github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA=
 github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI=
 github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs=
@@ -116,14 +116,14 @@ github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAd
 github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
-github.com/bufbuild/buf v1.51.0 h1:k2we7gmuSDeIqxkv16F/8s5Kk0l2ZfvMHpvC1n6o5Rk=
-github.com/bufbuild/buf v1.51.0/go.mod h1:TbX4Df3BfE0Lugd3Y3sFr7QTxqmCfPkuiEexe29KZeE=
+github.com/bufbuild/buf v1.52.1 h1:tAQ9rr9c8j8XRc9xvVvRFv5ztpiIyu32a6AUanWp34Y=
+github.com/bufbuild/buf v1.52.1/go.mod h1:htcJKxeVf7xZTCUmPZlh4OrHenMvTsvEkkxiLrEXf2I=
 github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
 github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
-github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9 h1:kAWER21DzhzU7ys8LL1WkSfbGkwXv+tM30hyEsYrW2k=
-github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
-github.com/bufbuild/protovalidate-go v0.9.3-0.20250317160558-38a17488914d h1:Y6Yp/LwSaRG8gw9GyyQD7jensL9NXqPlkbuulaAvCEE=
-github.com/bufbuild/protovalidate-go v0.9.3-0.20250317160558-38a17488914d/go.mod h1:SZN6Qr3lPWuKMoQtIhKdhESkb+3m2vk0lqN9WMuZDDU=
+github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
+github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
+github.com/bufbuild/protovalidate-go v0.9.3-0.20250403190939-663657418457 h1:Sa5rWJF1c3HdWoF5QcBDyCIoqPdIQf1Jh4HTos7UZsM=
+github.com/bufbuild/protovalidate-go v0.9.3-0.20250403190939-663657418457/go.mod h1:2lUDP6fNd3wxznRNH3Nj64VB07+PySeslamkerwP6tE=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
 github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
@@ -200,14 +200,14 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
 github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
-github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A=
+github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8=
-github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
-github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
+github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
+github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
+github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
 github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
@@ -365,8 +365,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
 github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
-github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro=
-github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
+github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -552,8 +552,8 @@ github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok
 github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8=
 github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
 github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
-github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
-github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
 github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
 github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
 github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -590,16 +590,16 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
-github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
-github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
-github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
-github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
+github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
+github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
+github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
+github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
-github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
-github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
-github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
+github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
 github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
 github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
 github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
@@ -831,24 +831,24 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
-go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
-go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
-go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
+go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -868,10 +868,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
-golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c=
-golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
+golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
@@ -906,8 +906,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
 golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -921,8 +921,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -951,8 +951,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -961,8 +961,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
-golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -973,8 +973,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
 golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -997,20 +997,20 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
+golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
+golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99 h1:ilJhrCga0AptpJZXmUYG4MCrx/zf3l1okuYz7YK9PPw=
-google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 h1:ZSlhAUqC4r8TPzqLXQ0m3upBNZeF+Y8jQ3c4CR3Ujms=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0 h1:Qbb5RVn5xzI4naMJSpJ7lhvmos6UwZkbekd5Uz7rt9E=
+google.golang.org/genproto/googleapis/api v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:6T35kB3IPpdw7Wul09by0G/JuOuIFkXV6OOvt8IZeT8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0 h1:0K7wTWyzxZ7J+L47+LbFogJW1nn/gnnMCN0vGXNYtTI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250404141209-ee84b53bf3d0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
diff --git a/internal/api/event.go b/internal/api/event.go
index 02d183d3..ce8d250a 100644
--- a/internal/api/event.go
+++ b/internal/api/event.go
@@ -1,6 +1,8 @@
 package api
 
 import (
+	"fmt"
+	"os"
 	"time"
 
 	authnv1 "k8s.io/api/authentication/v1"
@@ -21,28 +23,23 @@ func FormatEventControllerActor(name string) string {
 // 3. If the subject is available, it returns subject in "subject:<subject>" format.
 // 4. Otherwise, it returns EventActorUnknown.
 func FormatEventUserActor(u user.Info) string {
-	var email, subject string
+	if u.IsAdmin {
+		return kargoapi.EventActorAdmin
+	}
+	if u.Username != "" {
+		return formatOIDCUsername(u.Username)
+	}
 	if emailClaim, ok := u.Claims["email"]; ok {
-		if emailStr, ok := emailClaim.(string); ok {
-			email = emailStr
+		if email, ok := emailClaim.(string); ok {
+			return kargoapi.EventActorEmailPrefix + email
 		}
 	}
 	if subClaim, ok := u.Claims["sub"]; ok {
-		if subStr, ok := subClaim.(string); ok {
-			subject = subStr
+		if sub, ok := subClaim.(string); ok {
+			return kargoapi.EventActorSubjectPrefix + sub
 		}
 	}
-
-	switch {
-	case u.IsAdmin:
-		return kargoapi.EventActorAdmin
-	case email != "":
-		return kargoapi.EventActorEmailPrefix + email
-	case subject != "":
-		return kargoapi.EventActorSubjectPrefix + subject
-	default:
-		return kargoapi.EventActorUnknown
-	}
+	return kargoapi.EventActorUnknown
 }
 
 func FormatEventKubernetesUserActor(u authnv1.UserInfo) string {
@@ -62,3 +59,7 @@ func NewFreightApprovedEventAnnotations(actor string, f *kargoapi.Freight, stage
 	}
 	return annotations
 }
+
+func formatOIDCUsername(oidcUsername string) string {
+	return fmt.Sprintf("%s:%s", os.Getenv("OIDC_USERNAME_CLAIM"), oidcUsername)
+}
diff --git a/internal/controller/promotions/promotions.go b/internal/controller/promotions/promotions.go
index 97230d45..733e6021 100644
--- a/internal/controller/promotions/promotions.go
+++ b/internal/controller/promotions/promotions.go
@@ -416,10 +416,8 @@ func (r *reconciler) Reconcile(
 
 	// If the promotion is still running, we'll need to periodically check on
 	// it.
-	//
-	// TODO: Make this configurable
 	if newStatus.Phase == kargoapi.PromotionPhaseRunning {
-		return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
+		return ctrl.Result{RequeueAfter: calculateRequeueInterval(promo)}, nil
 	}
 	return ctrl.Result{}, nil
 }
@@ -665,3 +663,20 @@ func parseCreateActorAnnotation(promo *kargoapi.Promotion) string {
 	}
 	return creator
 }
+
+func calculateRequeueInterval(p *kargoapi.Promotion) time.Duration {
+	defaultRequeueInterval := 5 * time.Minute
+	step := p.Spec.Steps[p.Status.CurrentStep]
+	runner := promotion.GetStepRunner(step.Uses)
+
+	timeout := (&promotion.Step{
+		Retry: step.Retry,
+	}).GetTimeout(runner)
+
+	md := p.Status.StepExecutionMetadata[p.Status.CurrentStep]
+	targetTimeout := md.StartedAt.Time.Add(*timeout)
+	if targetTimeout.Before(time.Now().Add(defaultRequeueInterval)) {
+		return time.Until(targetTimeout)
+	}
+	return defaultRequeueInterval
+}
diff --git a/internal/gitprovider/gitlab/gitlab.go b/internal/gitprovider/gitlab/gitlab.go
index 7c88c3de..5a0ea4bb 100644
--- a/internal/gitprovider/gitlab/gitlab.go
+++ b/internal/gitprovider/gitlab/gitlab.go
@@ -120,12 +120,11 @@ func (p *provider) CreatePullRequest(
 		opts = &gitprovider.CreatePullRequestOpts{}
 	}
 	glMR, _, err := p.client.CreateMergeRequest(p.projectName, &gitlab.CreateMergeRequestOptions{
-		Title:              &opts.Title,
-		Description:        &opts.Description,
-		Labels:             (*gitlab.LabelOptions)(&opts.Labels),
-		SourceBranch:       &opts.Head,
-		TargetBranch:       &opts.Base,
-		RemoveSourceBranch: gitlab.Ptr(true),
+		Title:        &opts.Title,
+		Description:  &opts.Description,
+		Labels:       (*gitlab.LabelOptions)(&opts.Labels),
+		SourceBranch: &opts.Head,
+		TargetBranch: &opts.Base,
 	})
 	if err != nil {
 		return nil, err
diff --git a/internal/promotion/registry.go b/internal/promotion/registry.go
index d1217d21..1b22e148 100644
--- a/internal/promotion/registry.go
+++ b/internal/promotion/registry.go
@@ -1,6 +1,8 @@
 package promotion
 
-import "github.com/akuity/kargo/pkg/promotion"
+import (
+	"github.com/akuity/kargo/pkg/promotion"
+)
 
 // stepRunnerRegistry is a registry of StepRunners.
 type stepRunnerRegistry map[string]promotion.StepRunner
@@ -24,3 +26,9 @@ var stepRunnerReg = stepRunnerRegistry{}
 func RegisterStepRunner(runner promotion.StepRunner) {
 	stepRunnerReg.register(runner)
 }
+
+// GetStepRunner returns a StepRunner from the package's internal registry. If
+// no StepRunner is registered with the given name, nil is returned instead.
+func GetStepRunner(name string) promotion.StepRunner {
+	return stepRunnerReg.getStepRunner(name)
+}
diff --git a/internal/server/oidc/config.go b/internal/server/oidc/config.go
index 1a67e88b..76804202 100644
--- a/internal/server/oidc/config.go
+++ b/internal/server/oidc/config.go
@@ -22,6 +22,8 @@ type Config struct {
 	// AdditionalScopes are any more scopes to be requested during the authorization code flow
 	// on top of the default Scopes.
 	AdditionalScopes []string `envconfig:"OIDC_ADDITIONAL_SCOPES"`
+	// UsernameClaim is the claim to use as the username for the user.
+	UsernameClaim string `envconfig:"OIDC_USERNAME_CLAIM" default:"email"`
 
 	// GlobalServiceAccountNamespaces is the list of namespaces to look up
 	// for shared service accounts.
diff --git a/internal/server/option/auth.go b/internal/server/option/auth.go
index 085c09be..d648b756 100644
--- a/internal/server/option/auth.go
+++ b/internal/server/option/auth.go
@@ -398,12 +398,23 @@ func (a *authInterceptor) authenticate(
 		if err != nil {
 			return ctx, fmt.Errorf("list service accounts for user: %w", err)
 		}
+		var username string
+		if un, ok := c[a.cfg.OIDCConfig.UsernameClaim]; ok {
+			if username, ok = un.(string); !ok {
+				return ctx, fmt.Errorf(
+					"claim %q must be a string; got %T",
+					a.cfg.OIDCConfig.UsernameClaim,
+					un,
+				)
+			}
+		}
 		return user.ContextWithInfo(
 			ctx,
 			user.Info{
 				Claims:                     c,
 				ServiceAccountsByNamespace: sa,
 				BearerToken:                rawToken,
+				Username:                   username,
 			},
 		), nil
 
diff --git a/internal/server/user/user.go b/internal/server/user/user.go
index 0f9774eb..bd19a82d 100644
--- a/internal/server/user/user.go
+++ b/internal/server/user/user.go
@@ -27,6 +27,9 @@ type Info struct {
 	// ServiceAccountsByNamespace is the mapping of namespace names to sets of
 	// ServiceAccounts that a user has been mapped to.
 	ServiceAccountsByNamespace map[string]map[types.NamespacedName]struct{}
+	// Username is the username of the user. This is often the email address
+	// of the user, but may be different depending on configuration.
+	Username string
 }
 
 // ContextWithInfo returns a context.Context that has been augmented with
diff --git a/ui/src/app.tsx b/ui/src/app.tsx
index 18ef7353..836ac64d 100644
--- a/ui/src/app.tsx
+++ b/ui/src/app.tsx
@@ -43,15 +43,6 @@ export const App = () => (
                   <Route path={paths.warehouse} element={<Project />} />
                   <Route path={paths.downloads} element={<Downloads />} />
                   <Route path={paths.user} element={<User />} />
-                  <Route path={paths.settings} element={<Settings />} />
-                  <Route
-                    path={paths.settingsAnalysisTemplates}
-                    element={<Settings section='verification' />}
-                  />
-                  <Route
-                    path={paths.settingsClusterPromotionTasks}
-                    element={<Settings section='cluster-promotion-tasks' />}
-                  />
                   <Route
                     path={paths.createStage}
                     element={<Project tab='pipelines' creatingStage={true} />}
@@ -65,6 +56,10 @@ export const App = () => (
                     <Route index element={<ProjectSettings />} />
                     <Route path='*' element={<ProjectSettings />} />
                   </Route>
+                  <Route path={paths.settings}>
+                    <Route index element={<Settings />} />
+                    <Route path='*' element={<Settings />} />
+                  </Route>
                 </Route>
                 <Route path={paths.analysisRunLogs} element={<AnalysisRunLogsPage />} />
               </Route>
diff --git a/ui/src/config/paths.ts b/ui/src/config/paths.ts
index 63524413..3e3262ce 100644
--- a/ui/src/config/paths.ts
+++ b/ui/src/config/paths.ts
@@ -10,9 +10,7 @@ export const paths = {
   createWarehouse: '/project/:name/create-warehouse',
   projectSettings: '/project/:name/settings',
   user: '/user',
-  settings: '/settings/',
-  settingsAnalysisTemplates: '/settings/analysis-templates',
-  settingsClusterPromotionTasks: '/settings/cluster-promotion-tasks',
+  settings: '/settings',
   analysisRunLogs: '/ext/project/:name/stage/:stageName/analysis-run/:analysisRunId/logs',
   promotionTasks: '/project/:name/promotion-tasks',
 
diff --git a/ui/src/features/settings/analysis-templates/analysis-templates.tsx b/ui/src/features/settings/analysis-templates/analysis-templates.tsx
index 6f9cc5b2..d75a48e3 100644
--- a/ui/src/features/settings/analysis-templates/analysis-templates.tsx
+++ b/ui/src/features/settings/analysis-templates/analysis-templates.tsx
@@ -1,7 +1,7 @@
 import { useMutation, useQuery } from '@connectrpc/connect-query';
 import { faPencil, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, Table } from 'antd';
+import { Button, Card, Table } from 'antd';
 import { format } from 'date-fns';
 
 import { useConfirmModal } from '@ui/features/common/confirm-modal/use-confirm-modal';
@@ -34,83 +34,87 @@ export const ClusterAnalysisTemplatesList = () => {
   );
 
   return (
-    <Table<ClusterAnalysisTemplate>
-      dataSource={data?.clusterAnalysisTemplates}
-      pagination={{ hideOnSinglePage: true }}
-      rowKey={(i) => i.metadata?.name || ''}
-      loading={isLoading}
-      expandable={descriptionExpandable()}
-      className='w-full'
+    <Card
+      title='Cluster Analysis Templates'
+      type='inner'
+      className='min-h-full'
+      extra={
+        <Button icon={<FontAwesomeIcon icon={faPlus} />} onClick={() => showCreate()}>
+          Add Template
+        </Button>
+      }
     >
-      <Table.Column<ClusterAnalysisTemplate>
-        title='Creation Date'
-        width={200}
-        render={(_, template) => {
-          const date = timestampDate(template.metadata?.creationTimestamp);
-          return date ? format(date, 'MMM do yyyy HH:mm:ss') : '';
-        }}
-      />
-      <Table.Column<ClusterAnalysisTemplate> title='Name' dataIndex={['metadata', 'name']} />
-      <Table.Column<ClusterAnalysisTemplate>
-        width={260}
-        title={
-          <div className='text-right'>
-            <Button
-              type='primary'
-              className='ml-auto text-xs font-semibold'
-              icon={<FontAwesomeIcon icon={faPlus} />}
-              onClick={() => showCreate()}
-            >
-              ADD TEMPLATE
-            </Button>
-          </div>
-        }
-        render={(_, template) => (
-          <div className='flex gap-2 justify-end'>
-            <Button
-              icon={<FontAwesomeIcon icon={faPencil} />}
-              className='mr-2 ml-auto'
-              onClick={() => {
-                showEdit((p) => (
-                  <EditClusterAnalysisTemplateModal
-                    {...p}
-                    templateName={template.metadata?.name || ''}
-                  />
-                ));
-              }}
-            >
-              Edit
-            </Button>
-            <Button
-              icon={<FontAwesomeIcon icon={faTrash} />}
-              danger
-              loading={isDeleting}
-              onClick={() => {
-                confirm({
-                  title: (
-                    <div className='flex items-center'>
-                      <FontAwesomeIcon icon={faTrash} className='mr-2' />
-                      Delete Cluster Analysis Template
-                    </div>
-                  ),
-                  content: (
-                    <p>
-                      Are you sure you want to delete ClusterAnalysisTemplate{' '}
-                      <b>{template?.metadata?.name}</b>?
-                    </p>
-                  ),
-                  onOk: () => {
-                    deleteTemplate({ name: template?.metadata?.name || '' });
-                  },
-                  hide: () => {}
-                });
-              }}
-            >
-              Delete
-            </Button>
-          </div>
-        )}
-      />
-    </Table>
+      <Table<ClusterAnalysisTemplate>
+        dataSource={data?.clusterAnalysisTemplates}
+        pagination={{ hideOnSinglePage: true }}
+        rowKey={(i) => i.metadata?.name || ''}
+        loading={isLoading}
+        expandable={descriptionExpandable()}
+        className='w-full'
+      >
+        <Table.Column<ClusterAnalysisTemplate>
+          title='Creation Date'
+          width={200}
+          render={(_, template) => {
+            const date = timestampDate(template.metadata?.creationTimestamp);
+            return date ? format(date, 'MMM do yyyy HH:mm:ss') : '';
+          }}
+        />
+        <Table.Column<ClusterAnalysisTemplate> title='Name' dataIndex={['metadata', 'name']} />
+        <Table.Column<ClusterAnalysisTemplate>
+          width={260}
+          render={(_, template) => (
+            <div className='flex gap-2 justify-end'>
+              <Button
+                icon={<FontAwesomeIcon icon={faPencil} />}
+                className='mr-2 ml-auto'
+                onClick={() => {
+                  showEdit((p) => (
+                    <EditClusterAnalysisTemplateModal
+                      {...p}
+                      templateName={template.metadata?.name || ''}
+                    />
+                  ));
+                }}
+                size='small'
+                color='default'
+                variant='filled'
+              >
+                Edit
+              </Button>
+              <Button
+                icon={<FontAwesomeIcon icon={faTrash} />}
+                loading={isDeleting}
+                onClick={() => {
+                  confirm({
+                    title: (
+                      <div className='flex items-center'>
+                        <FontAwesomeIcon icon={faTrash} className='mr-2' />
+                        Delete Cluster Analysis Template
+                      </div>
+                    ),
+                    content: (
+                      <p>
+                        Are you sure you want to delete ClusterAnalysisTemplate{' '}
+                        <b>{template?.metadata?.name}</b>?
+                      </p>
+                    ),
+                    onOk: () => {
+                      deleteTemplate({ name: template?.metadata?.name || '' });
+                    },
+                    hide: () => {}
+                  });
+                }}
+                size='small'
+                color='danger'
+                variant='filled'
+              >
+                Delete
+              </Button>
+            </div>
+          )}
+        />
+      </Table>
+    </Card>
   );
 };
diff --git a/ui/src/features/settings/cluster-promotion-tasks/cluster-promotion-tasks.tsx b/ui/src/features/settings/cluster-promotion-tasks/cluster-promotion-tasks.tsx
index 8acc80da..8f130d2e 100644
--- a/ui/src/features/settings/cluster-promotion-tasks/cluster-promotion-tasks.tsx
+++ b/ui/src/features/settings/cluster-promotion-tasks/cluster-promotion-tasks.tsx
@@ -1,7 +1,7 @@
 import { useMutation, useQuery } from '@connectrpc/connect-query';
 import { faPencil, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, Flex, Table } from 'antd';
+import { Button, Card, Flex, Table } from 'antd';
 import { format } from 'date-fns';
 
 import { useConfirmModal } from '@ui/features/common/confirm-modal/use-confirm-modal';
@@ -61,7 +61,19 @@ export const ClusterPromotionTasks = () => {
     ));
 
   return (
-    <div className='p-4'>
+    <Card
+      title='Cluster Promotion Tasks'
+      type='inner'
+      className='min-h-full'
+      extra={
+        <Button
+          icon={<FontAwesomeIcon icon={faPlus} />}
+          onClick={() => onAddClusterPromotionTaskModalOpen()}
+        >
+          Add Cluster Promotion Task
+        </Button>
+      }
+    >
       <Table<ClusterPromotionTask>
         dataSource={listClusterPromotionTasksQuery.data?.clusterPromotionTasks}
         loading={listClusterPromotionTasksQuery.isFetching}
@@ -94,30 +106,23 @@ export const ClusterPromotionTasks = () => {
             render: (_, r) => r.metadata?.name
           },
           {
-            title: (
-              <div className='text-right'>
-                <Button
-                  type='primary'
-                  className='ml-auto text-xs font-semibold'
-                  icon={<FontAwesomeIcon icon={faPlus} />}
-                  onClick={onAddClusterPromotionTaskModalOpen}
-                >
-                  ADD CLUSTER PROMOTION TASK
-                </Button>
-              </div>
-            ),
             render: (_, template) => (
               <Flex gap={8} justify='end'>
                 <Button
                   icon={<FontAwesomeIcon icon={faPencil} />}
                   onClick={() => onEditClusterPromotionTaskModalOpen(template)}
+                  size='small'
+                  color='default'
+                  variant='filled'
                 >
                   Edit
                 </Button>
                 <Button
                   icon={<FontAwesomeIcon icon={faTrash} />}
-                  danger
                   onClick={() => onDeleteClusterPromotionTaskModalOpen(template)}
+                  size='small'
+                  color='danger'
+                  variant='filled'
                 >
                   Delete
                 </Button>
@@ -126,6 +131,6 @@ export const ClusterPromotionTasks = () => {
           }
         ]}
       />
-    </div>
+    </Card>
   );
 };
diff --git a/ui/src/features/settings/cluster-promotion-tasks/create-cluster-promotion-task.tsx b/ui/src/features/settings/cluster-promotion-tasks/create-cluster-promotion-task.tsx
index 10d70a69..71570c4d 100644
--- a/ui/src/features/settings/cluster-promotion-tasks/create-cluster-promotion-task.tsx
+++ b/ui/src/features/settings/cluster-promotion-tasks/create-cluster-promotion-task.tsx
@@ -1,5 +1,6 @@
 import { createConnectQueryKey, useMutation } from '@connectrpc/connect-query';
 import { useQueryClient } from '@tanstack/react-query';
+import { Button, Flex, Space } from 'antd';
 import Modal from 'antd/es/modal/Modal';
 import Link from 'antd/es/typography/Link';
 import { useForm } from 'react-hook-form';
@@ -52,21 +53,24 @@ export const CreateClusterPromotionTaskModal = (props: CreateClusterPromotionTas
     <Modal
       open={props.visible}
       onCancel={props.hide}
-      title={
-        <>
-          Create Cluster Promotion Task{' '}
+      title='Create Cluster Promotion Task'
+      width={700}
+      footer={
+        <Flex justify='space-between' align='center'>
           <Link
             href='https://docs.kargo.io/user-guide/reference-docs/promotion-tasks/#defining-a-global-promotion-task'
             target='_blank'
-            className='ml-1 text-xs'
           >
-            documentation
+            Documentation
           </Link>
-        </>
+          <Space>
+            <Button onClick={props.hide}>Cancel</Button>
+            <Button onClick={onSubmit} loading={createResourceMutation.isPending} type='primary'>
+              Create
+            </Button>
+          </Space>
+        </Flex>
       }
-      okText='Create'
-      width={700}
-      onOk={onSubmit}
     >
       <FieldContainer control={clusterPromotionTaskForm.control} name='value'>
         {({ field }) => (
diff --git a/ui/src/pages/settings.tsx b/ui/src/pages/settings.tsx
index e2b31769..d058be7c 100644
--- a/ui/src/pages/settings.tsx
+++ b/ui/src/pages/settings.tsx
@@ -1,75 +1,65 @@
 import { faBarChart, faTasks } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Layout, Menu, MenuProps } from 'antd';
-import { useNavigate, generatePath } from 'react-router-dom';
+import { Breadcrumb, Flex, Menu, Typography } from 'antd';
+import { NavLink, Routes, Route, Navigate, useLocation } from 'react-router-dom';
 
-import { paths } from '@ui/config/paths';
-import { PageTitle } from '@ui/features/common';
+import { BaseHeader } from '@ui/features/common/layout/base-header';
 import { ClusterAnalysisTemplatesList } from '@ui/features/settings/analysis-templates/analysis-templates';
 import { ClusterPromotionTasks } from '@ui/features/settings/cluster-promotion-tasks/cluster-promotion-tasks';
 
-const { Sider, Content } = Layout;
-
-type MenuItem = Required<MenuProps>['items'][number];
-
-export const Settings = ({ section = 'verification' }: { section?: string }) => {
-  const navigate = useNavigate();
-
-  const items: MenuItem[] = [
-    {
-      key: 'verification',
-      label: 'Verification',
-      icon: <FontAwesomeIcon icon={faBarChart} />,
-      onClick: () => {
-        navigate(generatePath(paths.settingsAnalysisTemplates));
-      }
-    },
-    {
-      key: 'cluster-promotion-tasks',
-      label: 'ClusterPromotionTasks',
-      icon: <FontAwesomeIcon icon={faTasks} />,
-      onClick: () => {
-        navigate(generatePath(paths.settingsClusterPromotionTasks));
-      }
-    }
-  ];
+const settingsViews = {
+  verification: {
+    label: 'Verificaion',
+    icon: faBarChart,
+    path: 'analysis-templates',
+    component: ClusterAnalysisTemplatesList
+  },
+  clusterPromotionTasks: {
+    label: 'ClusterPromotionTasks',
+    icon: faTasks,
+    path: 'cluster-promotion-tasks',
+    component: ClusterPromotionTasks
+  }
+};
 
-  const renderSection = (section: string) => {
-    switch (section) {
-      case 'verification':
-        return <ClusterAnalysisTemplatesList />;
-      case 'cluster-promotion-tasks':
-        return <ClusterPromotionTasks />;
-    }
-  };
+const defaultView = settingsViews.verification;
 
-  const getSectionTitle = (section: string) => {
-    switch (section) {
-      case 'verification':
-        return 'Cluster Analysis Templates';
-      case 'cluster-promotion-tasks':
-        return 'Cluster Promotion Tasks';
-    }
-  };
+export const Settings = () => {
+  const location = useLocation();
 
   return (
-    <Layout className='min-h-screen'>
-      <Sider
-        width={250}
-        className='border-r border-gray-300 shadow-sm'
-        style={{ background: 'white' }}
-      >
-        <div className='p-4'>
-          <PageTitle title='Settings' />
-        </div>
-        <Menu mode='vertical' defaultSelectedKeys={[section]} items={items} />
-      </Sider>
-      <Layout>
-        <Content className='p-6 bg-gray-100'>
-          <div className='text-2xl font-semibold mb-4'>{getSectionTitle(section)}</div>
-          {renderSection(section)}
-        </Content>
-      </Layout>
-    </Layout>
+    <>
+      <BaseHeader>
+        <Breadcrumb separator='>' items={[{ title: 'Settings' }]} />
+      </BaseHeader>
+      <div className='py-4 px-6'>
+        <Typography.Title level={3}>Settings</Typography.Title>
+        <Flex gap={24} className='mt-2'>
+          <div style={{ width: 240 }}>
+            <Menu
+              className='-ml-2 -mt-1'
+              style={{ border: 0, background: 'transparent' }}
+              selectedKeys={Object.values(settingsViews)
+                .map((i) => i.path)
+                .filter((i) => location.pathname.endsWith(i))}
+              items={Object.values(settingsViews).map((i) => ({
+                label: <NavLink to={`../${i.path}`}>{i.label}</NavLink>,
+                icon: <FontAwesomeIcon icon={i.icon} />,
+                key: i.path
+              }))}
+            />
+          </div>
+          <div className='flex-1 overflow-hidden' style={{ maxWidth: '920px', minHeight: '700px' }}>
+            <Routes>
+              <Route index element={<Navigate to={defaultView.path} replace={true} />} />
+              {Object.values(settingsViews).map((t) => (
+                <Route key={t.path} path={t.path} element={<t.component />} />
+              ))}
+              <Route path='*' element={<Navigate to='../' replace={true} />} />
+            </Routes>
+          </div>
+        </Flex>
+      </div>
+    </>
   );
 };



Test patch:
diff --git a/internal/api/events_test.go b/internal/api/events_test.go
new file mode 100644
index 00000000..68895b64
--- /dev/null
+++ b/internal/api/events_test.go
@@ -0,0 +1,54 @@
+package api
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+	"github.com/akuity/kargo/internal/server/user"
+)
+
+func TestFormatEventUserActor(t *testing.T) {
+	for _, test := range []struct {
+		name     string
+		user     user.Info
+		expected string
+	}{
+		{
+			name:     "admin",
+			user:     user.Info{IsAdmin: true},
+			expected: kargoapi.EventActorAdmin,
+		},
+		{
+			name: "sub",
+			user: user.Info{
+				Claims: map[string]any{
+					"sub": "subject",
+				},
+			},
+			expected: kargoapi.EventActorSubjectPrefix + "subject",
+		},
+		{
+			name: "email",
+			user: user.Info{
+				Claims: map[string]any{
+					"email": "email@inbox.com",
+				},
+			},
+			expected: kargoapi.EventActorEmailPrefix + "email@inbox.com",
+		},
+		{
+			name: "oidc-username",
+			user: user.Info{
+				Username: "oidc-username",
+			},
+			expected: formatOIDCUsername("oidc-username"),
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			result := FormatEventUserActor(test.user)
+			require.Equal(t, test.expected, result)
+		})
+	}
+}
diff --git a/internal/controller/promotions/promotions_test.go b/internal/controller/promotions/promotions_test.go
index 09aded66..b2fc49e7 100644
--- a/internal/controller/promotions/promotions_test.go
+++ b/internal/controller/promotions/promotions_test.go
@@ -531,6 +531,118 @@ func Test_parseCreateActorAnnotation(t *testing.T) {
 	}
 }
 
+func Test_calculateRequeueInterval(t *testing.T) {
+	defaultTimeout := 5 * time.Minute
+	for _, test := range []struct {
+		name       string
+		promo      *kargoapi.Promotion
+		setup      func(*testing.T)
+		assertions func(*testing.T, time.Duration)
+	}{
+		{
+			name: "should return default requeue interval",
+			promo: &kargoapi.Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					CreationTimestamp: now,
+					Name:              "fake-name",
+					Namespace:         "fake-namespace",
+				},
+				Spec: kargoapi.PromotionSpec{
+					Stage: "fake-stage",
+					Steps: []kargoapi.PromotionStep{
+						{
+							Uses: "slow-step",
+							Retry: &kargoapi.PromotionStepRetry{
+								Timeout: &metav1.Duration{
+									// exceeds default threshold
+									Duration: 10 * time.Minute,
+								},
+							},
+						},
+					},
+				},
+				Status: kargoapi.PromotionStatus{
+					Phase:       kargoapi.PromotionPhasePending,
+					CurrentStep: 0,
+					StepExecutionMetadata: []kargoapi.StepExecutionMetadata{
+						{StartedAt: &now},
+					},
+				},
+			},
+			setup: func(tt *testing.T) {
+				tt.Helper()
+				var (
+					// exceeds than the 5 minute default
+					stepRunnerTimeout = 10 * time.Minute
+					numRetries        = uint32(1)
+					runner            = promotion.NewMockRetryableStepRunner(
+						"slow-step",
+						&stepRunnerTimeout,
+						numRetries,
+					)
+				)
+				promotion.RegisterStepRunner(runner)
+			},
+			assertions: func(tt *testing.T, d time.Duration) {
+				require.Equal(tt, d, defaultTimeout)
+			},
+		},
+		{
+			name: "requeue interval should return requeue interval less than the default",
+			promo: &kargoapi.Promotion{
+				ObjectMeta: metav1.ObjectMeta{
+					CreationTimestamp: now,
+					Name:              "fake-name",
+					Namespace:         "fake-namespace",
+				},
+				Spec: kargoapi.PromotionSpec{
+					Stage: "fake-stage",
+					Steps: []kargoapi.PromotionStep{
+						{
+							Uses: "fast-step",
+							Retry: &kargoapi.PromotionStepRetry{
+								Timeout: &metav1.Duration{
+									// faster than default threshold
+									Duration: 3 * time.Minute,
+								},
+							},
+						},
+					},
+				},
+				Status: kargoapi.PromotionStatus{
+					Phase:       kargoapi.PromotionPhasePending,
+					CurrentStep: 0,
+					StepExecutionMetadata: []kargoapi.StepExecutionMetadata{
+						{StartedAt: &now},
+					},
+				},
+			},
+			setup: func(tt *testing.T) {
+				tt.Helper()
+				var (
+					// lower than the 5 minute default
+					stepRunnerTimeout = 3 * time.Minute
+					numRetries        = uint32(1)
+					runner            = promotion.NewMockRetryableStepRunner(
+						"fast-step",
+						&stepRunnerTimeout,
+						numRetries,
+					)
+				)
+				promotion.RegisterStepRunner(runner)
+			},
+			assertions: func(tt *testing.T, d time.Duration) {
+				require.Less(tt, d, defaultTimeout)
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			test.setup(t)
+			test.assertions(t, calculateRequeueInterval(test.promo))
+		})
+	}
+}
+
 // nolint: unparam
 func newPromo(namespace, name, stage string,
 	phase kargoapi.PromotionPhase,
diff --git a/internal/promotion/mock_step_runner_test.go b/internal/promotion/mock_step_runner.go
similarity index 63%
rename from internal/promotion/mock_step_runner_test.go
rename to internal/promotion/mock_step_runner.go
index ae8cd745..6edcfd0f 100644
--- a/internal/promotion/mock_step_runner_test.go
+++ b/internal/promotion/mock_step_runner.go
@@ -2,6 +2,7 @@ package promotion
 
 import (
 	"context"
+	"time"
 
 	"github.com/akuity/kargo/pkg/promotion"
 )
@@ -37,3 +38,29 @@ func (m *mockStepRunner) Run(
 	}
 	return m.runResult, m.runErr
 }
+
+type MockRetryableStepRunner struct {
+	*mockStepRunner
+	defaultTimeout        *time.Duration
+	defaultErrorThreshold uint32
+}
+
+func NewMockRetryableStepRunner(
+	name string,
+	defaultTimeout *time.Duration,
+	defaultErrThreshold uint32,
+) MockRetryableStepRunner {
+	return MockRetryableStepRunner{
+		mockStepRunner:        &mockStepRunner{name: name},
+		defaultTimeout:        defaultTimeout,
+		defaultErrorThreshold: defaultErrThreshold,
+	}
+}
+
+func (m MockRetryableStepRunner) DefaultTimeout() *time.Duration {
+	return m.defaultTimeout
+}
+
+func (m MockRetryableStepRunner) DefaultErrorThreshold() uint32 {
+	return m.defaultErrorThreshold
+}
diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go
index ff823e45..e32c7b65 100644
--- a/internal/promotion/promotion_test.go
+++ b/internal/promotion/promotion_test.go
@@ -17,20 +17,6 @@ import (
 	"github.com/akuity/kargo/pkg/promotion"
 )
 
-type mockRetryableStepRunner struct {
-	*mockStepRunner
-	defaultTimeout        *time.Duration
-	defaultErrorThreshold uint32
-}
-
-func (m mockRetryableStepRunner) DefaultTimeout() *time.Duration {
-	return m.defaultTimeout
-}
-
-func (m mockRetryableStepRunner) DefaultErrorThreshold() uint32 {
-	return m.defaultErrorThreshold
-}
-
 func TestStep_GetTimeout(t *testing.T) {
 	tests := []struct {
 		name       string
@@ -70,7 +56,7 @@ func TestStep_GetTimeout(t *testing.T) {
 					},
 				},
 			},
-			runner: mockRetryableStepRunner{defaultTimeout: ptr.To(time.Duration(3))},
+			runner: MockRetryableStepRunner{defaultTimeout: ptr.To(time.Duration(3))},
 			assertions: func(t *testing.T, result *time.Duration) {
 				assert.Equal(t, ptr.To(time.Duration(5)), result)
 			},
@@ -80,7 +66,7 @@ func TestStep_GetTimeout(t *testing.T) {
 			step: &Step{
 				Retry: &kargoapi.PromotionStepRetry{},
 			},
-			runner: mockRetryableStepRunner{defaultTimeout: ptr.To(time.Duration(3))},
+			runner: MockRetryableStepRunner{defaultTimeout: ptr.To(time.Duration(3))},
 			assertions: func(t *testing.T, result *time.Duration) {
 				assert.Equal(t, ptr.To(time.Duration(3)), result)
 			},
diff --git a/internal/server/option/auth_test.go b/internal/server/option/auth_test.go
index 8958f3ec..130c4898 100644
--- a/internal/server/option/auth_test.go
+++ b/internal/server/option/auth_test.go
@@ -308,7 +308,8 @@ func TestAuthenticate(t *testing.T) {
 			authInterceptor: &authInterceptor{
 				cfg: config.ServerConfig{
 					OIDCConfig: &libOIDC.Config{
-						IssuerURL: testIDPIssuer,
+						IssuerURL:     testIDPIssuer,
+						UsernameClaim: "preferred_username",
 					},
 				},
 				parseUnverifiedJWTFn: func(_ string, claims jwt.Claims) (*jwt.Token, []string, error) {
@@ -322,8 +323,9 @@ func TestAuthenticate(t *testing.T) {
 					string,
 				) (claims, error) {
 					return claims{
-						"sub":   "ironman",
-						"email": "tony@starkindustries.com",
+						"preferred_username": "foo",
+						"sub":                "ironman",
+						"email":              "tony@starkindustries.com",
 						"groups": []string{
 							"avengers",
 							"shield",
@@ -345,6 +347,7 @@ func TestAuthenticate(t *testing.T) {
 				u, ok := user.InfoFromContext(ctx)
 				require.True(t, ok)
 				require.False(t, u.IsAdmin)
+				require.Equal(t, u.Username, "foo")
 				require.Equal(t, "ironman", u.Claims["sub"])
 				require.Equal(t, "tony@starkindustries.com", u.Claims["email"])
 				require.Equal(t, []string{"avengers", "shield"}, u.Claims["groups"])