Repository: akuity/kargo

Issue:
[Feature] Support Passing Dynamic Key-Value Pairs in helm-template Step
# Checklist

* [x] I've searched the issue queue to verify this is not a duplicate feature request.
* [x] I've pasted the output of `kargo version`, if applicable.
* [x] I've pasted logs, if applicable.

# Proposed Feature
<!-- What new feature would you like to see? -->
Enable passing dynamic variables directly through Kargo's helm-template step, similar to the --set flag in Helm. This would allow users to override or set specific key-value pairs during rendering without modifying the values.yaml files.

# Motivation
<!-- Please give examples of your use case. i.e. When would someone use this? -->
Currently, when using Kargo's helm-template step, values must be provided through valuesFiles. However, in many cases, it's convenient to pass dynamic values or override specific keys without modifying the values.yaml files or maintaining multiple environment-specific files.

Helm's --set flag allows users to set values directly in the command line during deployment, which is useful for quick updates and overriding values for specific environments or configurations. Adding similar functionality to the helm-template step in Kargo would enable more flexible and dynamic templating, especially in CI/CD pipelines or automation scripts where certain values may need to be passed in without altering the existing values.yaml files.

# Suggested Implementation
<!-- Do you have thoughts about how this should be implemented? -->
Add support for passing dynamic key-value pairs using a --set-like flag to Kargo's helm-template step. This could be implemented by:

### Adding a New Field to the helm-template Step:
A new optional field setValues can be added to the configuration.

### Rendering with Dynamic Values:
When the helm-template step is executed, Kargo would append the --set values to the Helm helm template command internally, allowing users to override or set values dynamically.

```
- uses: helm-template
  as: renderingToDeployRepo
  config:
    disableHooks: false
    includeCRDs: false
    outPath: ${{ vars.deployRepoDir }}/mop-carrier-ops/environments/dev/us-east-1
    path: ${{ vars.serviceRepoDir }}/kubernetes/helm/mop-carrier-ops
    skipTests: false
    releaseName: ${{ vars.releaseName }}
    useReleaseName: false
    valuesFiles:
      - ${{ vars.serviceRepoDir }}/values.yaml
      - ${{ vars.serviceRepoDir }}/dev-values.yaml
      - ${{ vars.serviceRepoDir }}/dev-us-east-1.yaml
    setValues:
      - key: foo1
         value: bar1
      - key: foo2
         value: bar2
```

Discussion:
@aarontams: Also see related Enhancement that I filed  https://github.com/akuity/kargo/issues/3752 - Enhancement: Support Setting Key-Value Pairs in yaml-update Step Even if the Key Doesn't Exist in values.yaml @BenHesketh21: I may be wrong but isn't this solved by the yaml-update promotion step? You can just set whatever value you want in any of your value files if you want to override at the time of render? 

```
- uses: yaml-update
  config:
    path: ./src/charts/my-chart/values.yaml
    updates:
    - key: foo1
      value: bar1
    - key: foo2
      value: bar2
```@michaelasper: > I may be wrong but isn't this solved by the yaml-update promotion step? You can just set whatever value you want in any of your value files if you want to override at the time of render?
> 
> ```
> - uses: yaml-update
>   config:
>     path: ./src/charts/my-chart/values.yaml
>     updates:
>     - key: foo1
>       value: bar1
>     - key: foo2
>       value: bar2
> ```

Only if the key exists in the specific values file@krancour: > in many cases, it's convenient to pass dynamic values or override specific keys without modifying the values.yaml files or maintaining multiple environment-specific files.

I just want to call out that that's not "real GitOps," as some aspects of the cluster's desired state are no longer stored in the repository, but elsewhere.

With that being said, I'm not really taking any kind stand against pseudo-GitOps here. It has its place. If I were doing something quick and dirty that I know is never going to production, I might do something like that. To that end, there's actually something in Kargo already that is pseuo-GitOps / there for purely for convenience / shouldn't be used in prod -- various mutations can be performed on Argo CD Application resources, and these _include_ overriding Helm values.

So before going down the road of adding more stuff only to recommend people _not_ use it, I'd be curious to know what kind of mileage you can get out of this:

https://docs.kargo.io/user-guide/patterns/#pseudo-gitops

https://docs.kargo.io/user-guide/reference-docs/promotion-steps/argocd-update#configuration@aarontams: I added the same suggestion to similar feature request - https://github.com/akuity/kargo/issues/3752. What is your thoughts?

### Suggested implementation:
During the Helm template process, add the new key-value pairs in a separate values YAML file and apply it last.

I believe this approach will have minimal impact on the Kargo code.@krancour: @aarontams have you tried this?

https://docs.kargo.io/user-guide/reference-docs/promotion-steps/argocd-update#configuration@aarontams: > [@aarontams](https://github.com/aarontams) have you tried this?
> 
> https://docs.kargo.io/user-guide/reference-docs/promotion-steps/argocd-update#configuration

We render the manifest and save it in a GitHub repository first, then use the ArgoCD app to deploy it. So, argocd-update won't work for us.@krancour: @aarontams understood. Thank you.

Contrasted with something like #3761, this is something `helm template` actually supports (the `--set` flag), so I think there's adequate precedent to support this in the `helm-template` step.

With that said, I still see this as pseudo-gitops and not something people should be encouraged to do. Accordingly, I'm going to set the priority to low, but by all means, if someone wants to step up and do the work, including the corresponding doc update, I will be quite happy to review that.@aarontams: Thanks, @krancour, for the feedback and support on this issue.

I noticed @michaelasper raise a PR today to address this enhancement request.
I reviewed the PR, and it looks exactly like what I was hoping for and very promising! Hopefully, it will be included in the next release.@krancour: This was closed by #3795.

Solution patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 3ba2e38b..abc2ef4f 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -17,7 +17,7 @@ jobs:
   test-unit:
     runs-on: ubuntu-latest
     container:
-      image: golang:1.24.1-bookworm
+      image: golang:1.24.2-bookworm
     steps:
     # Install Git from "trixie" repository to get a more recent version than
     # the one available in "stable". This can be removed once the version in
@@ -80,7 +80,7 @@ jobs:
       checks: write # Used to create checks (linting comments) on PRs
     runs-on: ubuntu-latest
     container:
-      image: golang:1.24.1-bookworm
+      image: golang:1.24.2-bookworm
     steps:
     - name: Checkout code
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -105,7 +105,7 @@ jobs:
   lint-charts:
     runs-on: ubuntu-latest
     container:
-      image: golang:1.24.1-bookworm
+      image: golang:1.24.2-bookworm
     steps:
     - name: Checkout code
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -129,7 +129,7 @@ jobs:
       checks: write # Used to create checks (linting comments) on PRs
     runs-on: ubuntu-latest
     container:
-      image: golang:1.24.1-bookworm
+      image: golang:1.24.2-bookworm
     steps:
     - name: Checkout code
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -152,7 +152,7 @@ jobs:
   check-codegen:
     runs-on: ubuntu-latest
     container:
-      image: golang:1.24.1-bookworm
+      image: golang:1.24.2-bookworm
     steps:
     - name: Checkout code
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -228,7 +228,7 @@ jobs:
     needs: [test-unit, lint-go, lint-charts, lint-proto, lint-and-typecheck-ui, check-codegen]
     runs-on: ubuntu-latest
     container:
-      image: golang:1.24.1-bookworm
+      image: golang:1.24.2-bookworm
     steps:
     - name: Checkout code
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 0e8001c1..bce8a68f 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -177,7 +177,7 @@ jobs:
     if: github.event_name == 'release'
     runs-on: ubuntu-latest
     container:
-      image: golang:1.24.1-bookworm
+      image: golang:1.24.2-bookworm
     strategy:
       matrix:
         os: [linux, darwin, windows]
@@ -245,7 +245,7 @@ jobs:
     if: github.event_name != 'release'
     runs-on: ubuntu-latest
     container:
-      image: golang:1.24.1-bookworm
+      image: golang:1.24.2-bookworm
     strategy:
       matrix:
         os: [linux, darwin, windows]
diff --git a/Dockerfile b/Dockerfile
index ce4fcdcd..1f67d139 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,7 +20,7 @@ RUN NODE_ENV='production' VERSION=${VERSION} pnpm run build
 ####################################################################################################
 # back-end-builder
 ####################################################################################################
-FROM --platform=$BUILDPLATFORM golang:1.24.1-bookworm AS back-end-builder
+FROM --platform=$BUILDPLATFORM golang:1.24.2-bookworm AS back-end-builder
 
 ARG TARGETOS
 ARG TARGETARCH
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 6c791c00..0a889e58 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -1,4 +1,4 @@
-FROM golang:1.24.1-bookworm
+FROM golang:1.24.2-bookworm
 
 ARG TARGETARCH
 
diff --git a/api/v1alpha1/expression_types.go b/api/v1alpha1/expression_types.go
new file mode 100644
index 00000000..02e739a9
--- /dev/null
+++ b/api/v1alpha1/expression_types.go
@@ -0,0 +1,19 @@
+package v1alpha1
+
+// ExpressionVariable describes a single variable that may be referenced by
+// expressions in the context of a ClusterPromotionTask, PromotionTask,
+// Promotion, AnalysisRun arguments, or other objects that support expressions.
+//
+// It is used to pass information to the expression evaluation engine, and to
+// allow for dynamic evaluation of expressions based on the variable values.
+type ExpressionVariable struct {
+	// Name is the name of the variable.
+	//
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
+	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
+	// Value is the value of the variable. It is allowed to utilize expressions
+	// in the value.
+	// See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
+	Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"`
+}
diff --git a/api/v1alpha1/generated.pb.go b/api/v1alpha1/generated.pb.go
index 318a4af1..1e9d6cec 100644
--- a/api/v1alpha1/generated.pb.go
+++ b/api/v1alpha1/generated.pb.go
@@ -1458,15 +1458,15 @@ func (m *PromotionTemplateSpec) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_PromotionTemplateSpec proto.InternalMessageInfo
 
-func (m *PromotionVariable) Reset()      { *m = PromotionVariable{} }
-func (*PromotionVariable) ProtoMessage() {}
-func (*PromotionVariable) Descriptor() ([]byte, []int) {
+func (m *ExpressionVariable) Reset()      { *m = ExpressionVariable{} }
+func (*ExpressionVariable) ProtoMessage() {}
+func (*ExpressionVariable) Descriptor() ([]byte, []int) {
 	return fileDescriptor_e26b7f7bbc391025, []int{51}
 }
-func (m *PromotionVariable) XXX_Unmarshal(b []byte) error {
+func (m *ExpressionVariable) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
 }
-func (m *PromotionVariable) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+func (m *ExpressionVariable) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
 	b = b[:cap(b)]
 	n, err := m.MarshalToSizedBuffer(b)
 	if err != nil {
@@ -1474,17 +1474,17 @@ func (m *PromotionVariable) XXX_Marshal(b []byte, deterministic bool) ([]byte, e
 	}
 	return b[:n], nil
 }
-func (m *PromotionVariable) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_PromotionVariable.Merge(m, src)
+func (m *ExpressionVariable) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ExpressionVariable.Merge(m, src)
 }
-func (m *PromotionVariable) XXX_Size() int {
+func (m *ExpressionVariable) XXX_Size() int {
 	return m.Size()
 }
-func (m *PromotionVariable) XXX_DiscardUnknown() {
-	xxx_messageInfo_PromotionVariable.DiscardUnknown(m)
+func (m *ExpressionVariable) XXX_DiscardUnknown() {
+	xxx_messageInfo_ExpressionVariable.DiscardUnknown(m)
 }
 
-var xxx_messageInfo_PromotionVariable proto.InternalMessageInfo
+var xxx_messageInfo_ExpressionVariable proto.InternalMessageInfo
 
 func (m *RepoSubscription) Reset()      { *m = RepoSubscription{} }
 func (*RepoSubscription) ProtoMessage() {}
@@ -1910,7 +1910,7 @@ func init() {
 	proto.RegisterType((*PromotionTaskSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionTaskSpec")
 	proto.RegisterType((*PromotionTemplate)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionTemplate")
 	proto.RegisterType((*PromotionTemplateSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionTemplateSpec")
-	proto.RegisterType((*PromotionVariable)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionVariable")
+	proto.RegisterType((*ExpressionVariable)(nil), "github.com.akuity.kargo.api.v1alpha1.ExpressionVariable")
 	proto.RegisterType((*RepoSubscription)(nil), "github.com.akuity.kargo.api.v1alpha1.RepoSubscription")
 	proto.RegisterType((*Stage)(nil), "github.com.akuity.kargo.api.v1alpha1.Stage")
 	proto.RegisterType((*StageList)(nil), "github.com.akuity.kargo.api.v1alpha1.StageList")
@@ -4976,7 +4976,7 @@ func (m *PromotionTemplateSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
-func (m *PromotionVariable) Marshal() (dAtA []byte, err error) {
+func (m *ExpressionVariable) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
 	n, err := m.MarshalToSizedBuffer(dAtA[:size])
@@ -4986,12 +4986,12 @@ func (m *PromotionVariable) Marshal() (dAtA []byte, err error) {
 	return dAtA[:n], nil
 }
 
-func (m *PromotionVariable) MarshalTo(dAtA []byte) (int, error) {
+func (m *ExpressionVariable) MarshalTo(dAtA []byte) (int, error) {
 	size := m.Size()
 	return m.MarshalToSizedBuffer(dAtA[:size])
 }
 
-func (m *PromotionVariable) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+func (m *ExpressionVariable) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	i := len(dAtA)
 	_ = i
 	var l int
@@ -6866,7 +6866,7 @@ func (m *PromotionTemplateSpec) Size() (n int) {
 	return n
 }
 
-func (m *PromotionVariable) Size() (n int) {
+func (m *ExpressionVariable) Size() (n int) {
 	if m == nil {
 		return 0
 	}
@@ -7857,9 +7857,9 @@ func (this *PromotionSpec) String() string {
 		repeatedStringForSteps += strings.Replace(strings.Replace(f.String(), "PromotionStep", "PromotionStep", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForSteps += "}"
-	repeatedStringForVars := "[]PromotionVariable{"
+	repeatedStringForVars := "[]ExpressionVariable{"
 	for _, f := range this.Vars {
-		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "PromotionVariable", "PromotionVariable", 1), `&`, ``, 1) + ","
+		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "ExpressionVariable", "ExpressionVariable", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForVars += "}"
 	s := strings.Join([]string{`&PromotionSpec{`,
@@ -7904,9 +7904,9 @@ func (this *PromotionStep) String() string {
 	if this == nil {
 		return "nil"
 	}
-	repeatedStringForVars := "[]PromotionVariable{"
+	repeatedStringForVars := "[]ExpressionVariable{"
 	for _, f := range this.Vars {
-		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "PromotionVariable", "PromotionVariable", 1), `&`, ``, 1) + ","
+		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "ExpressionVariable", "ExpressionVariable", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForVars += "}"
 	s := strings.Join([]string{`&PromotionStep{`,
@@ -7974,9 +7974,9 @@ func (this *PromotionTaskSpec) String() string {
 	if this == nil {
 		return "nil"
 	}
-	repeatedStringForVars := "[]PromotionVariable{"
+	repeatedStringForVars := "[]ExpressionVariable{"
 	for _, f := range this.Vars {
-		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "PromotionVariable", "PromotionVariable", 1), `&`, ``, 1) + ","
+		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "ExpressionVariable", "ExpressionVariable", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForVars += "}"
 	repeatedStringForSteps := "[]PromotionStep{"
@@ -8010,9 +8010,9 @@ func (this *PromotionTemplateSpec) String() string {
 		repeatedStringForSteps += strings.Replace(strings.Replace(f.String(), "PromotionStep", "PromotionStep", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForSteps += "}"
-	repeatedStringForVars := "[]PromotionVariable{"
+	repeatedStringForVars := "[]ExpressionVariable{"
 	for _, f := range this.Vars {
-		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "PromotionVariable", "PromotionVariable", 1), `&`, ``, 1) + ","
+		repeatedStringForVars += strings.Replace(strings.Replace(f.String(), "ExpressionVariable", "ExpressionVariable", 1), `&`, ``, 1) + ","
 	}
 	repeatedStringForVars += "}"
 	s := strings.Join([]string{`&PromotionTemplateSpec{`,
@@ -8022,11 +8022,11 @@ func (this *PromotionTemplateSpec) String() string {
 	}, "")
 	return s
 }
-func (this *PromotionVariable) String() string {
+func (this *ExpressionVariable) String() string {
 	if this == nil {
 		return "nil"
 	}
-	s := strings.Join([]string{`&PromotionVariable{`,
+	s := strings.Join([]string{`&ExpressionVariable{`,
 		`Name:` + fmt.Sprintf("%v", this.Name) + `,`,
 		`Value:` + fmt.Sprintf("%v", this.Value) + `,`,
 		`}`,
@@ -15800,7 +15800,7 @@ func (m *PromotionSpec) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Vars = append(m.Vars, PromotionVariable{})
+			m.Vars = append(m.Vars, ExpressionVariable{})
 			if err := m.Vars[len(m.Vars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
@@ -16433,7 +16433,7 @@ func (m *PromotionStep) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Vars = append(m.Vars, PromotionVariable{})
+			m.Vars = append(m.Vars, ExpressionVariable{})
 			if err := m.Vars[len(m.Vars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
@@ -17001,7 +17001,7 @@ func (m *PromotionTaskSpec) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Vars = append(m.Vars, PromotionVariable{})
+			m.Vars = append(m.Vars, ExpressionVariable{})
 			if err := m.Vars[len(m.Vars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
@@ -17236,7 +17236,7 @@ func (m *PromotionTemplateSpec) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Vars = append(m.Vars, PromotionVariable{})
+			m.Vars = append(m.Vars, ExpressionVariable{})
 			if err := m.Vars[len(m.Vars)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
@@ -17262,7 +17262,7 @@ func (m *PromotionTemplateSpec) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *PromotionVariable) Unmarshal(dAtA []byte) error {
+func (m *ExpressionVariable) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -17285,10 +17285,10 @@ func (m *PromotionVariable) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: PromotionVariable: wiretype end group for non-group")
+			return fmt.Errorf("proto: ExpressionVariable: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: PromotionVariable: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: ExpressionVariable: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
 		case 1:
diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto
index 6ee061a7..b72d98f9 100644
--- a/api/v1alpha1/generated.proto
+++ b/api/v1alpha1/generated.proto
@@ -302,6 +302,25 @@ message DiscoveredImageReference {
   optional .k8s.io.apimachinery.pkg.apis.meta.v1.Time createdAt = 4;
 }
 
+// ExpressionVariable describes a single variable that may be referenced by
+// expressions in the context of a ClusterPromotionTask, PromotionTask,
+// Promotion, AnalysisRun arguments, or other objects that support expressions.
+//
+// It is used to pass information to the expression evaluation engine, and to
+// allow for dynamic evaluation of expressions based on the variable values.
+message ExpressionVariable {
+  // Name is the name of the variable.
+  //
+  // +kubebuilder:validation:MinLength=1
+  // +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
+  optional string name = 1;
+
+  // Value is the value of the variable. It is allowed to utilize expressions
+  // in the value.
+  // See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
+  optional string value = 2;
+}
+
 // Freight represents a collection of versioned artifacts.
 message Freight {
   optional .k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
@@ -921,7 +940,7 @@ message PromotionSpec {
 
   // Vars is a list of variables that can be referenced by expressions in
   // promotion steps.
-  repeated PromotionVariable vars = 4;
+  repeated ExpressionVariable vars = 4;
 
   // Steps specifies the directives to be executed as part of this Promotion.
   // The order in which the directives are executed is the order in which they
@@ -1007,7 +1026,7 @@ message PromotionStep {
   // Vars is a list of variables that can be referenced by expressions in
   // the step's Config. The values override the values specified in the
   // PromotionSpec.
-  repeated PromotionVariable vars = 6;
+  repeated ExpressionVariable vars = 6;
 
   // Config is opaque configuration for the PromotionStep that is understood
   // only by each PromotionStep's implementation. It is legal to utilize
@@ -1095,7 +1114,7 @@ message PromotionTaskSpec {
   // Vars specifies the variables available to the PromotionTask. The
   // values of these variables are the default values that can be
   // overridden by the step referencing the task.
-  repeated PromotionVariable vars = 1;
+  repeated ExpressionVariable vars = 1;
 
   // Steps specifies the directives to be executed as part of this
   // PromotionTask. The steps as defined here are inflated into a
@@ -1119,7 +1138,7 @@ message PromotionTemplate {
 message PromotionTemplateSpec {
   // Vars is a list of variables that can be referenced by expressions in
   // promotion steps.
-  repeated PromotionVariable vars = 2;
+  repeated ExpressionVariable vars = 2;
 
   // Steps specifies the directives to be executed as part of a Promotion.
   // The order in which the directives are executed is the order in which they
@@ -1130,21 +1149,6 @@ message PromotionTemplateSpec {
   repeated PromotionStep steps = 1;
 }
 
-// PromotionVariable describes a single variable that may be referenced by
-// expressions in promotion steps.
-message PromotionVariable {
-  // Name is the name of the variable.
-  //
-  // +kubebuilder:validation:MinLength=1
-  // +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
-  optional string name = 1;
-
-  // Value is the value of the variable. It is allowed to utilize expressions
-  // in the value.
-  // See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
-  optional string value = 2;
-}
-
 // RepoSubscription describes a subscription to ONE OF a Git repository, a
 // container image repository, or a Helm chart repository.
 message RepoSubscription {
@@ -1189,6 +1193,11 @@ message StageSpec {
   // is empty, the webhook will ensure that label is absent.
   optional string shard = 4;
 
+  // Vars is a list of variables that can be referenced anywhere in the
+  // StageSpec that supports expressions. For example, the PromotionTemplate
+  // and arguments of the Verification.
+  repeated ExpressionVariable vars = 7;
+
   // RequestedFreight expresses the Stage's need for certain pieces of Freight,
   // each having originated from a particular Warehouse. This list must be
   // non-empty. In the common case, a Stage will request Freight having
diff --git a/api/v1alpha1/promotion_task_types.go b/api/v1alpha1/promotion_task_types.go
index 5c0fe35e..20270d10 100644
--- a/api/v1alpha1/promotion_task_types.go
+++ b/api/v1alpha1/promotion_task_types.go
@@ -23,7 +23,7 @@ type PromotionTaskSpec struct {
 	// Vars specifies the variables available to the PromotionTask. The
 	// values of these variables are the default values that can be
 	// overridden by the step referencing the task.
-	Vars []PromotionVariable `json:"vars,omitempty" protobuf:"bytes,1,rep,name=vars"`
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,1,rep,name=vars"`
 	// Steps specifies the directives to be executed as part of this
 	// PromotionTask. The steps as defined here are inflated into a
 	// Promotion when it is built from a PromotionTemplate.
diff --git a/api/v1alpha1/promotion_types.go b/api/v1alpha1/promotion_types.go
index 8e0cac73..1f8ba97e 100644
--- a/api/v1alpha1/promotion_types.go
+++ b/api/v1alpha1/promotion_types.go
@@ -96,7 +96,7 @@ type PromotionSpec struct {
 	Freight string `json:"freight" protobuf:"bytes,2,opt,name=freight"`
 	// Vars is a list of variables that can be referenced by expressions in
 	// promotion steps.
-	Vars []PromotionVariable `json:"vars,omitempty" protobuf:"bytes,4,rep,name=vars"`
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,4,rep,name=vars"`
 	// Steps specifies the directives to be executed as part of this Promotion.
 	// The order in which the directives are executed is the order in which they
 	// are listed in this field.
@@ -107,20 +107,6 @@ type PromotionSpec struct {
 	Steps []PromotionStep `json:"steps" protobuf:"bytes,3,rep,name=steps"`
 }
 
-// PromotionVariable describes a single variable that may be referenced by
-// expressions in promotion steps.
-type PromotionVariable struct {
-	// Name is the name of the variable.
-	//
-	// +kubebuilder:validation:MinLength=1
-	// +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
-	Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
-	// Value is the value of the variable. It is allowed to utilize expressions
-	// in the value.
-	// See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
-	Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"`
-}
-
 // PromotionTaskReference describes a reference to a PromotionTask.
 type PromotionTaskReference struct {
 	// Name is the name of the (Cluster)PromotionTask.
@@ -217,7 +203,7 @@ type PromotionStep struct {
 	// Vars is a list of variables that can be referenced by expressions in
 	// the step's Config. The values override the values specified in the
 	// PromotionSpec.
-	Vars []PromotionVariable `json:"vars,omitempty" protobuf:"bytes,6,rep,name=vars"`
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,6,rep,name=vars"`
 	// Config is opaque configuration for the PromotionStep that is understood
 	// only by each PromotionStep's implementation. It is legal to utilize
 	// expressions in defining values at any level of this block.
diff --git a/api/v1alpha1/stage_types.go b/api/v1alpha1/stage_types.go
index 0f7045b2..7fd9f9e5 100644
--- a/api/v1alpha1/stage_types.go
+++ b/api/v1alpha1/stage_types.go
@@ -220,6 +220,10 @@ type StageSpec struct {
 	// kargo.akuity.io/shard label with the value of this field. When this field
 	// is empty, the webhook will ensure that label is absent.
 	Shard string `json:"shard,omitempty" protobuf:"bytes,4,opt,name=shard"`
+	// Vars is a list of variables that can be referenced anywhere in the
+	// StageSpec that supports expressions. For example, the PromotionTemplate
+	// and arguments of the Verification.
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,7,rep,name=vars"`
 	// RequestedFreight expresses the Stage's need for certain pieces of Freight,
 	// each having originated from a particular Warehouse. This list must be
 	// non-empty. In the common case, a Stage will request Freight having
@@ -329,7 +333,7 @@ type PromotionTemplate struct {
 type PromotionTemplateSpec struct {
 	// Vars is a list of variables that can be referenced by expressions in
 	// promotion steps.
-	Vars []PromotionVariable `json:"vars,omitempty" protobuf:"bytes,2,rep,name=vars"`
+	Vars []ExpressionVariable `json:"vars,omitempty" protobuf:"bytes,2,rep,name=vars"`
 	// Steps specifies the directives to be executed as part of a Promotion.
 	// The order in which the directives are executed is the order in which they
 	// are listed in this field.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index f3cab7fc..10f4fc31 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -364,6 +364,21 @@ func (in *DiscoveredImageReference) DeepCopy() *DiscoveredImageReference {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExpressionVariable) DeepCopyInto(out *ExpressionVariable) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpressionVariable.
+func (in *ExpressionVariable) DeepCopy() *ExpressionVariable {
+	if in == nil {
+		return nil
+	}
+	out := new(ExpressionVariable)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Freight) DeepCopyInto(out *Freight) {
 	*out = *in
@@ -1014,7 +1029,7 @@ func (in *PromotionSpec) DeepCopyInto(out *PromotionSpec) {
 	*out = *in
 	if in.Vars != nil {
 		in, out := &in.Vars, &out.Vars
-		*out = make([]PromotionVariable, len(*in))
+		*out = make([]ExpressionVariable, len(*in))
 		copy(*out, *in)
 	}
 	if in.Steps != nil {
@@ -1099,7 +1114,7 @@ func (in *PromotionStep) DeepCopyInto(out *PromotionStep) {
 	}
 	if in.Vars != nil {
 		in, out := &in.Vars, &out.Vars
-		*out = make([]PromotionVariable, len(*in))
+		*out = make([]ExpressionVariable, len(*in))
 		copy(*out, *in)
 	}
 	if in.Config != nil {
@@ -1217,7 +1232,7 @@ func (in *PromotionTaskSpec) DeepCopyInto(out *PromotionTaskSpec) {
 	*out = *in
 	if in.Vars != nil {
 		in, out := &in.Vars, &out.Vars
-		*out = make([]PromotionVariable, len(*in))
+		*out = make([]ExpressionVariable, len(*in))
 		copy(*out, *in)
 	}
 	if in.Steps != nil {
@@ -1260,7 +1275,7 @@ func (in *PromotionTemplateSpec) DeepCopyInto(out *PromotionTemplateSpec) {
 	*out = *in
 	if in.Vars != nil {
 		in, out := &in.Vars, &out.Vars
-		*out = make([]PromotionVariable, len(*in))
+		*out = make([]ExpressionVariable, len(*in))
 		copy(*out, *in)
 	}
 	if in.Steps != nil {
@@ -1282,21 +1297,6 @@ func (in *PromotionTemplateSpec) DeepCopy() *PromotionTemplateSpec {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *PromotionVariable) DeepCopyInto(out *PromotionVariable) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PromotionVariable.
-func (in *PromotionVariable) DeepCopy() *PromotionVariable {
-	if in == nil {
-		return nil
-	}
-	out := new(PromotionVariable)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RepoSubscription) DeepCopyInto(out *RepoSubscription) {
 	*out = *in
@@ -1389,6 +1389,11 @@ func (in *StageList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *StageSpec) DeepCopyInto(out *StageSpec) {
 	*out = *in
+	if in.Vars != nil {
+		in, out := &in.Vars, &out.Vars
+		*out = make([]ExpressionVariable, len(*in))
+		copy(*out, *in)
+	}
 	if in.RequestedFreight != nil {
 		in, out := &in.RequestedFreight, &out.RequestedFreight
 		*out = make([]FreightRequest, len(*in))
diff --git a/charts/kargo/README.md b/charts/kargo/README.md
index 3b3b8532..40211d99 100644
--- a/charts/kargo/README.md
+++ b/charts/kargo/README.md
@@ -67,79 +67,79 @@ the Kargo controller is running.
 
 ### API
 
-| Name                                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | Value                    |
-| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
-| `api.enabled`                               | Whether the API server is enabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `true`                   |
-| `api.replicas`                              | The number of API server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `1`                      |
-| `api.host`                                  | The domain name where Kargo's API server will be accessible. When applicable, this is used for generation of an Ingress resource, certificates, and the OpenID Connect issuer and callback URLs. Note: The value in this field MAY include a port number and MUST NOT specify the protocol (http vs https), which is automatically inferred from other configuration options.                                                                                                                                                                                                                                                                      | `localhost`              |
-| `api.logLevel`                              | The log level for the API server.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `INFO`                   |
-| `api.labels`                                | Labels to add to the api resources. Merges with `global.labels`, allowing you to override or add to the global labels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{}`                     |
-| `api.annotations`                           | Annotations to add to the api resources. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
-| `api.podLabels`                             | Optional labels to add to pods. Merges with `global.podLabels`, allowing you to override or add to the global labels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
-| `api.podAnnotations`                        | Optional annotations to add to pods. Merges with `global.podAnnotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
-| `api.secretManagementEnabled`               | Specifies whether Secret management is enabled. This affects the API server's ability to manage repository credentials and other Project-level Secrets, such as those used by AnalysisRuns for verification purposes. If using GitOps to manage Kargo Projects declaratively, the API's Secret management capabilities are not needed and can be disabled to effectively reduce the API server's attackable surface.                                                                                                                                                                                                                               | `true`                   |
-| `api.resources`                             | Resources limits and requests for the api containers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
-| `api.nodeSelector`                          | Node selector for api pods. Defaults to `global.nodeSelector`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `{}`                     |
-| `api.tolerations`                           | Tolerations for api pods. Defaults to `global.tolerations`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `[]`                     |
-| `api.affinity`                              | Specifies pod affinity for api pods. Defaults to `global.affinity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `{}`                     |
-| `api.securityContext`                       | Security context for api pods. Defaults to `global.securityContext`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
-| `api.cabundle.configMapName`                | Specifies the name of an optional ConfigMap containing CA certs that is managed "out of band." Values in the ConfigMap named here should each contain a single PEM-encoded CA cert. If secretName is also defined, it will take precedence over this field.                                                                                                                                                                                                                                                                                                                                                                                        | `""`                     |
-| `api.cabundle.secretName`                   | Specifies the name of an optional Secret containing CA certs that is managed "out of band." Values in the Secret named here should each contain a single PEM-encoded CA cert. If defined, the value of this field takes precedence over any in configMapName.                                                                                                                                                                                                                                                                                                                                                                                      | `""`                     |
-| `api.env`                                   | Environment variables to add to API server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `[]`                     |
-| `api.envFrom`                               | Environment variables to add to API server pods from ConfigMaps or Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `[]`                     |
-| `api.probes.enabled`                        | Whether liveness and readiness probes should be included in the API server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `true`                   |
-| `api.tls.enabled`                           | Whether to enable TLS directly on the API server. This is helpful if you do not intend to use an ingress controller or if you require TLS end-to-end. All other settings in this section EXCEPT `terminatedUpstream` will be ignored when this is set to `false`.                                                                                                                                                                                                                                                                                                                                                                                  | `true`                   |
-| `api.tls.selfSignedCert`                    | Whether to generate a self-signed certificate for use by the API server. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-api-cert` **must** be provided in the same namespace as Kargo.                                                                                                                                                                                                                                                                                                                                             | `true`                   |
-| `api.tls.terminatedUpstream`                | Whether TLS is terminated upstream, i.e. a load balancer, reverse-proxy, or an Ingress controller using a single wildcard cert is terminating it. Setting this to `true` forces all API server URLs to use HTTPS even if the Ingress (if applicable) or API server itself are listening for plain HTTP requests.                                                                                                                                                                                                                                                                                                                                   | `false`                  |
-| `api.permissiveCORSPolicyEnabled`           | Whether to enable a permissive CORS (Cross Origin Resource Sharing) policy. This is sometimes advantageous during local development, but otherwise, should generally be left disabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `false`                  |
-| `api.ingress.enabled`                       | Whether to enable ingress by creating an Ingress resource. By default, this is disabled. Enabling ingress is advanced usage.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `false`                  |
-| `api.ingress.annotations`                   | Annotations specified by your ingress controller to customize the behavior of the Ingress resource.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `{}`                     |
-| `api.ingress.ingressClassName`              | If implemented by your ingress controller, specifies the ingress class. If your ingress controller does not support this, use the `kubernetes.io/ingress.class` annotation instead.                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `nil`                    |
-| `api.ingress.tls.enabled`                   | Whether to associate a certificate with the Ingress resource.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `true`                   |
-| `api.ingress.tls.selfSignedCert`            | Whether to generate a self-signed certificate for use with the API server's Ingress resource. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-api-ingress-cert` **must** be provided in the same namespace as Kargo. The value in this field has no effect if `api.ingress.tls.enabled` is `false`.                                                                                                                                                                                                                                 | `true`                   |
-| `api.ingress.pathType`                      | You may want to use `Prefix` for some controllers (like AWS LoadBalancer Ingress controller), which don't support `/` as wildcard path when pathType is set to `ImplementationSpecific`                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `ImplementationSpecific` |
-| `api.service.type`                          | If you're not going to use an ingress controller, you may want to change this value to `LoadBalancer` for production deployments. If running locally, you may want to change it to `NodePort` OR leave it as `ClusterIP` and use `kubectl port-forward` to map a port on the local network interface to the service.                                                                                                                                                                                                                                                                                                                               | `ClusterIP`              |
-| `api.service.nodePort`                      | Host port the `Service` will be mapped to when `type` is either `NodePort` or `LoadBalancer`. If not specified, Kubernetes chooses.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `undefined`              |
-| `api.service.annotations`                   | Annotations to add to the API server's service. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `{}`                     |
-| `api.secret.name`                           | Specifies the name of an existing Secret which contains the `ADMIN_ACCOUNT_PASSWORD_HASH` and `ADMIN_ACCOUNT_TOKEN_SIGNING_KEY` values. By setting this, the Secret will **not** be generated by Helm.                                                                                                                                                                                                                                                                                                                                                                                                                                             | `""`                     |
-| `api.adminAccount.enabled`                  | Whether to enable the admin account.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `true`                   |
-| `api.adminAccount.passwordHash`             | Bcrypt password hash for the admin account. A value **must** be provided for this field unless `api.secret.name` is specified.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `""`                     |
-| `api.adminAccount.tokenSigningKey`          | Key used to sign ID tokens (JWTs) for the admin account. It is suggested that you generate this using a password manager or a command like: `openssl rand -base64 29 \| tr -d "=+/" \| cut`. A value **must** be provided for this field, unless `api.secret.name` is specified.                                                                                                                                                                                                                                                                                                                                                                   | `""`                     |
-| `api.adminAccount.tokenTTL`                 | Specifies how long ID tokens for the admin account are valid. (i.e. The expiry will be the time of issue plus this duration.)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `24h`                    |
-| `api.oidc.enabled`                          | Whether to enable authentication using Open ID Connect.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `false`                  |
-| `api.oidc.issuerURL`                        | The issuer URL for the identity provider. If Dex is enabled, this value will be ignored and the issuer URL will be automatically configured. If Dex is not enabled, this should be set to the issuer URL provided to you by your identity provider.                                                                                                                                                                                                                                                                                                                                                                                                | `nil`                    |
-| `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.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.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `[]`                     |
-| `api.oidc.dex.enabled`                      | Whether to enable Dex as the identity provider. When set to true, the Kargo installation will include a Dex server and the Kargo API server will be configured to make the /dex endpoint a reverse proxy for the Dex server.                                                                                                                                                                                                                                                                                                                                                                                                                       | `false`                  |
-| `api.oidc.dex.image.repository`             | Image repository of Dex                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `ghcr.io/dexidp/dex`     |
-| `api.oidc.dex.image.tag`                    | Image tag for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `v2.37.0`                |
-| `api.oidc.dex.image.pullPolicy`             | Image pull policy for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `IfNotPresent`           |
-| `api.oidc.dex.image.pullSecrets`            | List of imagePullSecrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `[]`                     |
-| `api.oidc.dex.env`                          | Environment variables to add to Dex server pods. This is convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                         | `[]`                     |
-| `api.oidc.dex.envFrom`                      | Environment variables to add to Dex server pods from ConfigMaps or Secrets. This is especially convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                   | `[]`                     |
-| `api.oidc.dex.volumes`                      | Add additional volumes to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                               | `[]`                     |
-| `api.oidc.dex.volumeMounts`                 | Add additional volume mounts to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                         | `nil`                    |
-| `api.oidc.dex.probes.enabled`               | Whether liveness and readiness probes should be included in the Dex server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `true`                   |
-| `api.oidc.dex.tls.selfSignedCert`           | Whether to generate a self-signed certificate for use with Dex. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-dex-server-cert` **must** be provided in the same namespace as Kargo. There is no provision for running Dex without TLS.                                                                                                                                                                                                                                                                                            | `true`                   |
-| `api.oidc.dex.skipApprovalScreen`           | Whether to skip Dex's own approval screen. Since upstream identity providers will already request user consent, this second approval screen from Dex can be both superfluous and confusing.                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `true`                   |
-| `api.oidc.dex.connectors`                   | Configure [Dex connectors](https://dexidp.io/docs/connectors/) to one or more upstream identity providers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `[]`                     |
-| `api.oidc.dex.resources`                    | Resources limits and requests for the Dex server containers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `{}`                     |
-| `api.oidc.dex.nodeSelector`                 | Node selector for Dex server pods. Defaults to `global.nodeSelector`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
-| `api.oidc.dex.tolerations`                  | Tolerations for Dex server pods. Defaults to `global.tolerations`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `[]`                     |
-| `api.oidc.dex.affinity`                     | Specifies pod affinity for the Dex server pods. Defaults to `global.affinity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `{}`                     |
-| `api.oidc.dex.annotations`                  | Annotations to add to the Dex server pods. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `{}`                     |
-| `api.oidc.dex.securityContext`              | Security context for Dex server pods. Defaults to `global.securityContext`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `{}`                     |
-| `api.argocd.urls`                           | Mapping of Argo CD shards names to URLs to support deep links to Argo CD URLs. If sharding is not used, map the empty string to the single Argo CD URL.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `nil`                    |
-| `api.rollouts.integrationEnabled`           | Specifies whether Argo Rollouts integration is enabled. When not enabled, the API server will not be capable of creating/updating/applying AnalysesTemplate resources in the Kargo control plane. When enabled, the API server will perform a sanity check at startup. If Argo Rollouts CRDs are not found, the API server will proceed as if this integration had been explicitly disabled. Explicitly disabling is still preferable if this integration is not desired, as it will grant fewer permissions to the API server.                                                                                                                    | `true`                   |
-| `api.rollouts.logs.enabled`                 | Specifies whether support for streaming logs from AnalysisRuns using a JobMetric provider is enabled. This feature requires you to have forwarded and stored the logs yourself in a place where they can be retrieved with an HTTP GET.                                                                                                                                                                                                                                                                                                                                                                                                            | `false`                  |
-| `api.rollouts.logs.urlTemplate`             | Instructs Kargo on how to construct a URL for the retrieval of relevant logs via HTTP GET. Expressions offset by ${{ }} are supported with the following variables pre-defined and injected with values: project, namespace (the Project namespace), stage, analysisRun (its name), metricName (name of the JobMetric), jobNamespace (namespace of the Job; may be different that the Project namespace as the Job may actually execute in a different cluster), jobName, container (since a Pod associated with a Job could have more than one). Example: "https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{job}}/${{container}}". | `""`                     |
-| `api.rollouts.logs.tokenSecret.name`        | specifies the name of a Kubernetes Secret managed "out of band" that contains a token usable for accessing job metric logs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `nil`                    |
-| `api.rollouts.logs.tokenSecret.key`         | specifies the key in a Kubernetes Secret (named by name) that is managed "out of band" and contains a token usable for accessing job metric logs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `nil`                    |
-| `api.rollouts.logs.httpHeaders`             | Specifies HTTP headers to include in the HTTP GET request for log retrieval. These are typically used for authentication. The header values support expressions offset by ${{ }}, with the same variables documented for urlTemplate pre-defined and injected with values.                                                                                                                                                                                                                                                                                                                                                                         | `{}`                     |
+| Name                                        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Value                    |
+| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
+| `api.enabled`                               | Whether the API server is enabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `true`                   |
+| `api.replicas`                              | The number of API server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `1`                      |
+| `api.host`                                  | The domain name where Kargo's API server will be accessible. When applicable, this is used for generation of an Ingress resource, certificates, and the OpenID Connect issuer and callback URLs. Note: The value in this field MAY include a port number and MUST NOT specify the protocol (http vs https), which is automatically inferred from other configuration options.                                                                                                                                                                                                                                                                                                       | `localhost`              |
+| `api.logLevel`                              | The log level for the API server.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `INFO`                   |
+| `api.labels`                                | Labels to add to the api resources. Merges with `global.labels`, allowing you to override or add to the global labels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `{}`                     |
+| `api.annotations`                           | Annotations to add to the api resources. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
+| `api.podLabels`                             | Optional labels to add to pods. Merges with `global.podLabels`, allowing you to override or add to the global labels.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
+| `api.podAnnotations`                        | Optional annotations to add to pods. Merges with `global.podAnnotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `{}`                     |
+| `api.secretManagementEnabled`               | Specifies whether Secret management is enabled. This affects the API server's ability to manage repository credentials and other Project-level Secrets, such as those used by AnalysisRuns for verification purposes. If using GitOps to manage Kargo Projects declaratively, the API's Secret management capabilities are not needed and can be disabled to effectively reduce the API server's attackable surface.                                                                                                                                                                                                                                                                | `true`                   |
+| `api.resources`                             | Resources limits and requests for the api containers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
+| `api.nodeSelector`                          | Node selector for api pods. Defaults to `global.nodeSelector`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `{}`                     |
+| `api.tolerations`                           | Tolerations for api pods. Defaults to `global.tolerations`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `[]`                     |
+| `api.affinity`                              | Specifies pod affinity for api pods. Defaults to `global.affinity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `{}`                     |
+| `api.securityContext`                       | Security context for api pods. Defaults to `global.securityContext`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `{}`                     |
+| `api.cabundle.configMapName`                | Specifies the name of an optional ConfigMap containing CA certs that is managed "out of band." Values in the ConfigMap named here should each contain a single PEM-encoded CA cert. If secretName is also defined, it will take precedence over this field.                                                                                                                                                                                                                                                                                                                                                                                                                         | `""`                     |
+| `api.cabundle.secretName`                   | Specifies the name of an optional Secret containing CA certs that is managed "out of band." Values in the Secret named here should each contain a single PEM-encoded CA cert. If defined, the value of this field takes precedence over any in configMapName.                                                                                                                                                                                                                                                                                                                                                                                                                       | `""`                     |
+| `api.env`                                   | Environment variables to add to API server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | `[]`                     |
+| `api.envFrom`                               | Environment variables to add to API server pods from ConfigMaps or Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `[]`                     |
+| `api.probes.enabled`                        | Whether liveness and readiness probes should be included in the API server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `true`                   |
+| `api.tls.enabled`                           | Whether to enable TLS directly on the API server. This is helpful if you do not intend to use an ingress controller or if you require TLS end-to-end. All other settings in this section EXCEPT `terminatedUpstream` will be ignored when this is set to `false`.                                                                                                                                                                                                                                                                                                                                                                                                                   | `true`                   |
+| `api.tls.selfSignedCert`                    | Whether to generate a self-signed certificate for use by the API server. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-api-cert` **must** be provided in the same namespace as Kargo.                                                                                                                                                                                                                                                                                                                                                                              | `true`                   |
+| `api.tls.terminatedUpstream`                | Whether TLS is terminated upstream, i.e. a load balancer, reverse-proxy, or an Ingress controller using a single wildcard cert is terminating it. Setting this to `true` forces all API server URLs to use HTTPS even if the Ingress (if applicable) or API server itself are listening for plain HTTP requests.                                                                                                                                                                                                                                                                                                                                                                    | `false`                  |
+| `api.permissiveCORSPolicyEnabled`           | Whether to enable a permissive CORS (Cross Origin Resource Sharing) policy. This is sometimes advantageous during local development, but otherwise, should generally be left disabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `false`                  |
+| `api.ingress.enabled`                       | Whether to enable ingress by creating an Ingress resource. By default, this is disabled. Enabling ingress is advanced usage.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `false`                  |
+| `api.ingress.annotations`                   | Annotations specified by your ingress controller to customize the behavior of the Ingress resource.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `{}`                     |
+| `api.ingress.ingressClassName`              | If implemented by your ingress controller, specifies the ingress class. If your ingress controller does not support this, use the `kubernetes.io/ingress.class` annotation instead.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `nil`                    |
+| `api.ingress.tls.enabled`                   | Whether to associate a certificate with the Ingress resource.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `true`                   |
+| `api.ingress.tls.selfSignedCert`            | Whether to generate a self-signed certificate for use with the API server's Ingress resource. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-api-ingress-cert` **must** be provided in the same namespace as Kargo. The value in this field has no effect if `api.ingress.tls.enabled` is `false`.                                                                                                                                                                                                                                                                  | `true`                   |
+| `api.ingress.pathType`                      | You may want to use `Prefix` for some controllers (like AWS LoadBalancer Ingress controller), which don't support `/` as wildcard path when pathType is set to `ImplementationSpecific`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `ImplementationSpecific` |
+| `api.service.type`                          | If you're not going to use an ingress controller, you may want to change this value to `LoadBalancer` for production deployments. If running locally, you may want to change it to `NodePort` OR leave it as `ClusterIP` and use `kubectl port-forward` to map a port on the local network interface to the service.                                                                                                                                                                                                                                                                                                                                                                | `ClusterIP`              |
+| `api.service.nodePort`                      | Host port the `Service` will be mapped to when `type` is either `NodePort` or `LoadBalancer`. If not specified, Kubernetes chooses.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `undefined`              |
+| `api.service.annotations`                   | Annotations to add to the API server's service. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `{}`                     |
+| `api.secret.name`                           | Specifies the name of an existing Secret which contains the `ADMIN_ACCOUNT_PASSWORD_HASH` and `ADMIN_ACCOUNT_TOKEN_SIGNING_KEY` values. By setting this, the Secret will **not** be generated by Helm.                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `""`                     |
+| `api.adminAccount.enabled`                  | Whether to enable the admin account.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `true`                   |
+| `api.adminAccount.passwordHash`             | Bcrypt password hash for the admin account. A value **must** be provided for this field unless `api.secret.name` is specified.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `""`                     |
+| `api.adminAccount.tokenSigningKey`          | Key used to sign ID tokens (JWTs) for the admin account. It is suggested that you generate this using a password manager or a command like: `openssl rand -base64 29 \| tr -d "=+/" \| cut`. A value **must** be provided for this field, unless `api.secret.name` is specified.                                                                                                                                                                                                                                                                                                                                                                                                    | `""`                     |
+| `api.adminAccount.tokenTTL`                 | Specifies how long ID tokens for the admin account are valid. (i.e. The expiry will be the time of issue plus this duration.)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `24h`                    |
+| `api.oidc.enabled`                          | Whether to enable authentication using Open ID Connect.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `false`                  |
+| `api.oidc.issuerURL`                        | The issuer URL for the identity provider. If Dex is enabled, this value will be ignored and the issuer URL will be automatically configured. If Dex is not enabled, this should be set to the issuer URL provided to you by your identity provider.                                                                                                                                                                                                                                                                                                                                                                                                                                 | `nil`                    |
+| `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.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.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `[]`                     |
+| `api.oidc.dex.enabled`                      | Whether to enable Dex as the identity provider. When set to true, the Kargo installation will include a Dex server and the Kargo API server will be configured to make the /dex endpoint a reverse proxy for the Dex server.                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `false`                  |
+| `api.oidc.dex.image.repository`             | Image repository of Dex                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `ghcr.io/dexidp/dex`     |
+| `api.oidc.dex.image.tag`                    | Image tag for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `v2.37.0`                |
+| `api.oidc.dex.image.pullPolicy`             | Image pull policy for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `IfNotPresent`           |
+| `api.oidc.dex.image.pullSecrets`            | List of imagePullSecrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `[]`                     |
+| `api.oidc.dex.env`                          | Environment variables to add to Dex server pods. This is convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                          | `[]`                     |
+| `api.oidc.dex.envFrom`                      | Environment variables to add to Dex server pods from ConfigMaps or Secrets. This is especially convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                    | `[]`                     |
+| `api.oidc.dex.volumes`                      | Add additional volumes to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                | `[]`                     |
+| `api.oidc.dex.volumeMounts`                 | Add additional volume mounts to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                          | `nil`                    |
+| `api.oidc.dex.probes.enabled`               | Whether liveness and readiness probes should be included in the Dex server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `true`                   |
+| `api.oidc.dex.tls.selfSignedCert`           | Whether to generate a self-signed certificate for use with Dex. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-dex-server-cert` **must** be provided in the same namespace as Kargo. There is no provision for running Dex without TLS.                                                                                                                                                                                                                                                                                                                             | `true`                   |
+| `api.oidc.dex.skipApprovalScreen`           | Whether to skip Dex's own approval screen. Since upstream identity providers will already request user consent, this second approval screen from Dex can be both superfluous and confusing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `true`                   |
+| `api.oidc.dex.connectors`                   | Configure [Dex connectors](https://dexidp.io/docs/connectors/) to one or more upstream identity providers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `[]`                     |
+| `api.oidc.dex.resources`                    | Resources limits and requests for the Dex server containers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | `{}`                     |
+| `api.oidc.dex.nodeSelector`                 | Node selector for Dex server pods. Defaults to `global.nodeSelector`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `{}`                     |
+| `api.oidc.dex.tolerations`                  | Tolerations for Dex server pods. Defaults to `global.tolerations`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `[]`                     |
+| `api.oidc.dex.affinity`                     | Specifies pod affinity for the Dex server pods. Defaults to `global.affinity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `{}`                     |
+| `api.oidc.dex.annotations`                  | Annotations to add to the Dex server pods. Merges with `global.annotations`, allowing you to override or add to the global annotations.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{}`                     |
+| `api.oidc.dex.securityContext`              | Security context for Dex server pods. Defaults to `global.securityContext`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `{}`                     |
+| `api.argocd.urls`                           | Mapping of Argo CD shards names to URLs to support deep links to Argo CD URLs. If sharding is not used, map the empty string to the single Argo CD URL.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `nil`                    |
+| `api.rollouts.integrationEnabled`           | Specifies whether Argo Rollouts integration is enabled. When not enabled, the API server will not be capable of creating/updating/applying AnalysesTemplate resources in the Kargo control plane. When enabled, the API server will perform a sanity check at startup. If Argo Rollouts CRDs are not found, the API server will proceed as if this integration had been explicitly disabled. Explicitly disabling is still preferable if this integration is not desired, as it will grant fewer permissions to the API server.                                                                                                                                                     | `true`                   |
+| `api.rollouts.logs.enabled`                 | Specifies whether support for streaming logs from AnalysisRuns using a JobMetric provider is enabled. This feature requires you to have forwarded and stored the logs yourself in a place where they can be retrieved with an HTTP GET.                                                                                                                                                                                                                                                                                                                                                                                                                                             | `false`                  |
+| `api.rollouts.logs.urlTemplate`             | Instructs Kargo on how to construct a URL for the retrieval of relevant logs via HTTP GET. Expressions offset by ${{ }} are supported with the following variables pre-defined and injected with values: project (name), namespace (always equal to the Project's name), stage (name), analysisRun (name), metricName (name of the JobMetric), jobNamespace (namespace of the Job; may be different that the Project namespace as the Job may actually execute in a different cluster), jobName, container (name; since a Pod associated with a Job could have more than one). Example: "https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{jobName}}/${{container}}". | `""`                     |
+| `api.rollouts.logs.tokenSecret.name`        | specifies the name of a Kubernetes Secret managed "out of band" that contains a token usable for accessing job metric logs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `nil`                    |
+| `api.rollouts.logs.tokenSecret.key`         | specifies the key in a Kubernetes Secret (named by name) that is managed "out of band" and contains a token usable for accessing job metric logs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `nil`                    |
+| `api.rollouts.logs.httpHeaders`             | Specifies HTTP headers to include in the HTTP GET request for log retrieval. These are typically used for authentication. The header values support expressions offset by ${{ }}, with the same variables documented for urlTemplate pre-defined and injected with values.                                                                                                                                                                                                                                                                                                                                                                                                          | `{}`                     |
 
 ### Controller
 
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_clusterpromotiontasks.yaml b/charts/kargo/resources/crds/kargo.akuity.io_clusterpromotiontasks.yaml
index 7274d7dc..afbd4ed9 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_clusterpromotiontasks.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_clusterpromotiontasks.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.17.2
+    controller-gen.kubebuilder.io/version: v0.17.3
   name: clusterpromotiontasks.kargo.akuity.io
 spec:
   group: kargo.akuity.io
@@ -151,8 +151,12 @@ spec:
                         PromotionSpec.
                       items:
                         description: |-
-                          PromotionVariable describes a single variable that may be referenced by
-                          expressions in promotion steps.
+                          ExpressionVariable describes a single variable that may be referenced by
+                          expressions in the context of a ClusterPromotionTask, PromotionTask,
+                          Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                          It is used to pass information to the expression evaluation engine, and to
+                          allow for dynamic evaluation of expressions based on the variable values.
                         properties:
                           name:
                             description: Name is the name of the variable.
@@ -183,8 +187,12 @@ spec:
                   overridden by the step referencing the task.
                 items:
                   description: |-
-                    PromotionVariable describes a single variable that may be referenced by
-                    expressions in promotion steps.
+                    ExpressionVariable describes a single variable that may be referenced by
+                    expressions in the context of a ClusterPromotionTask, PromotionTask,
+                    Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                    It is used to pass information to the expression evaluation engine, and to
+                    allow for dynamic evaluation of expressions based on the variable values.
                   properties:
                     name:
                       description: Name is the name of the variable.
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml b/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml
index eabce520..2e7e5cb2 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.17.2
+    controller-gen.kubebuilder.io/version: v0.17.3
   name: freights.kargo.akuity.io
 spec:
   group: kargo.akuity.io
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml b/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml
index 0f4dcd5d..0d60211f 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.17.2
+    controller-gen.kubebuilder.io/version: v0.17.3
   name: projects.kargo.akuity.io
 spec:
   group: kargo.akuity.io
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
index 756da83c..876e1680 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.17.2
+    controller-gen.kubebuilder.io/version: v0.17.3
   name: promotions.kargo.akuity.io
 spec:
   group: kargo.akuity.io
@@ -183,8 +183,12 @@ spec:
                         PromotionSpec.
                       items:
                         description: |-
-                          PromotionVariable describes a single variable that may be referenced by
-                          expressions in promotion steps.
+                          ExpressionVariable describes a single variable that may be referenced by
+                          expressions in the context of a ClusterPromotionTask, PromotionTask,
+                          Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                          It is used to pass information to the expression evaluation engine, and to
+                          allow for dynamic evaluation of expressions based on the variable values.
                         properties:
                           name:
                             description: Name is the name of the variable.
@@ -214,8 +218,12 @@ spec:
                   promotion steps.
                 items:
                   description: |-
-                    PromotionVariable describes a single variable that may be referenced by
-                    expressions in promotion steps.
+                    ExpressionVariable describes a single variable that may be referenced by
+                    expressions in the context of a ClusterPromotionTask, PromotionTask,
+                    Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                    It is used to pass information to the expression evaluation engine, and to
+                    allow for dynamic evaluation of expressions based on the variable values.
                   properties:
                     name:
                       description: Name is the name of the variable.
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotiontasks.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotiontasks.yaml
index 2b68f6d1..cb7d0da8 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_promotiontasks.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_promotiontasks.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.17.2
+    controller-gen.kubebuilder.io/version: v0.17.3
   name: promotiontasks.kargo.akuity.io
 spec:
   group: kargo.akuity.io
@@ -151,8 +151,12 @@ spec:
                         PromotionSpec.
                       items:
                         description: |-
-                          PromotionVariable describes a single variable that may be referenced by
-                          expressions in promotion steps.
+                          ExpressionVariable describes a single variable that may be referenced by
+                          expressions in the context of a ClusterPromotionTask, PromotionTask,
+                          Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                          It is used to pass information to the expression evaluation engine, and to
+                          allow for dynamic evaluation of expressions based on the variable values.
                         properties:
                           name:
                             description: Name is the name of the variable.
@@ -183,8 +187,12 @@ spec:
                   overridden by the step referencing the task.
                 items:
                   description: |-
-                    PromotionVariable describes a single variable that may be referenced by
-                    expressions in promotion steps.
+                    ExpressionVariable describes a single variable that may be referenced by
+                    expressions in the context of a ClusterPromotionTask, PromotionTask,
+                    Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                    It is used to pass information to the expression evaluation engine, and to
+                    allow for dynamic evaluation of expressions based on the variable values.
                   properties:
                     name:
                       description: Name is the name of the variable.
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
index 1cab0d94..6edc1de4 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.17.2
+    controller-gen.kubebuilder.io/version: v0.17.3
   name: stages.kargo.akuity.io
 spec:
   group: kargo.akuity.io
@@ -173,8 +173,12 @@ spec:
                                 PromotionSpec.
                               items:
                                 description: |-
-                                  PromotionVariable describes a single variable that may be referenced by
-                                  expressions in promotion steps.
+                                  ExpressionVariable describes a single variable that may be referenced by
+                                  expressions in the context of a ClusterPromotionTask, PromotionTask,
+                                  Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                                  It is used to pass information to the expression evaluation engine, and to
+                                  allow for dynamic evaluation of expressions based on the variable values.
                                 properties:
                                   name:
                                     description: Name is the name of the variable.
@@ -204,8 +208,12 @@ spec:
                           promotion steps.
                         items:
                           description: |-
-                            PromotionVariable describes a single variable that may be referenced by
-                            expressions in promotion steps.
+                            ExpressionVariable describes a single variable that may be referenced by
+                            expressions in the context of a ClusterPromotionTask, PromotionTask,
+                            Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                            It is used to pass information to the expression evaluation engine, and to
+                            allow for dynamic evaluation of expressions based on the variable values.
                           properties:
                             name:
                               description: Name is the name of the variable.
@@ -320,6 +328,35 @@ spec:
                   kargo.akuity.io/shard label with the value of this field. When this field
                   is empty, the webhook will ensure that label is absent.
                 type: string
+              vars:
+                description: |-
+                  Vars is a list of variables that can be referenced anywhere in the
+                  StageSpec that supports expressions. For example, the PromotionTemplate
+                  and arguments of the Verification.
+                items:
+                  description: |-
+                    ExpressionVariable describes a single variable that may be referenced by
+                    expressions in the context of a ClusterPromotionTask, PromotionTask,
+                    Promotion, AnalysisRun arguments, or other objects that support expressions.
+
+                    It is used to pass information to the expression evaluation engine, and to
+                    allow for dynamic evaluation of expressions based on the variable values.
+                  properties:
+                    name:
+                      description: Name is the name of the variable.
+                      minLength: 1
+                      pattern: ^[a-zA-Z_]\w*$
+                      type: string
+                    value:
+                      description: |-
+                        Value is the value of the variable. It is allowed to utilize expressions
+                        in the value.
+                        See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
+                      type: string
+                  required:
+                  - name
+                  type: object
+                type: array
               verification:
                 description: |-
                   Verification describes how to verify a Stage's current Freight is fit for
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml b/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
index c67889a7..18c8d33a 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.17.2
+    controller-gen.kubebuilder.io/version: v0.17.3
   name: warehouses.kargo.akuity.io
 spec:
   group: kargo.akuity.io
diff --git a/charts/kargo/values.yaml b/charts/kargo/values.yaml
index de9785dc..295be553 100755
--- a/charts/kargo/values.yaml
+++ b/charts/kargo/values.yaml
@@ -355,7 +355,7 @@ api:
     logs:
       ## @param api.rollouts.logs.enabled Specifies whether support for streaming logs from AnalysisRuns using a JobMetric provider is enabled. This feature requires you to have forwarded and stored the logs yourself in a place where they can be retrieved with an HTTP GET.
       enabled: false
-      ## @param api.rollouts.logs.urlTemplate Instructs Kargo on how to construct a URL for the retrieval of relevant logs via HTTP GET. Expressions offset by ${{ }} are supported with the following variables pre-defined and injected with values: project, namespace (the Project namespace), stage, analysisRun (its name), metricName (name of the JobMetric), jobNamespace (namespace of the Job; may be different that the Project namespace as the Job may actually execute in a different cluster), jobName, container (since a Pod associated with a Job could have more than one). Example: "https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{job}}/${{container}}".
+      ## @param api.rollouts.logs.urlTemplate Instructs Kargo on how to construct a URL for the retrieval of relevant logs via HTTP GET. Expressions offset by ${{ }} are supported with the following variables pre-defined and injected with values: project (name), namespace (always equal to the Project's name), stage (name), analysisRun (name), metricName (name of the JobMetric), jobNamespace (namespace of the Job; may be different that the Project namespace as the Job may actually execute in a different cluster), jobName, container (name; since a Pod associated with a Job could have more than one). Example: "https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{jobName}}/${{container}}".
       urlTemplate: ""
       tokenSecret:
         ## @param api.rollouts.logs.tokenSecret.name specifies the name of a Kubernetes Secret managed "out of band" that contains a token usable for accessing job metric logs.
diff --git a/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md b/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
index 2d97df95..afc18a20 100644
--- a/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
+++ b/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
@@ -370,8 +370,9 @@ any logs that it can access via an HTTP GET request.
 
 To facilitate this, operators may, at the time of installation, provide a URL
 template that the API server can use to construct the URL for any job metric logs
-as a function of attributes such as `Project` name, `Stage` name, `AnalysisRun`
-name, and more.
+as a function of `project` (name), `namespace` (always equal to the `Project`'s
+name), `shard`, `stage` (name), `analysisRun` (name), `metricName`,
+`jobNamespace`, `jobName`, and `container` (name).
 
 A token can be specified by referencing a Kubernetes `Secret` that is managed
 "out of band." HTTP headers may also be specified, and may reference the token
@@ -385,7 +386,7 @@ api:
     integrationEnabled: true
     logs:
       enabled: true
-      urlTemplate: https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{job}}/${{container}}
+      urlTemplate: https://logs.kargo.example.com/${{project}}/${{analysisRun}}/${{jobName}}/${{container}}
       tokenSecret:
         name: kargo-logs-token
         key: token
diff --git a/docs/docs/50-user-guide/20-how-to-guides/10-logging-in/index.md b/docs/docs/50-user-guide/20-how-to-guides/10-logging-in/index.md
index ef98398a..f28d0a80 100644
--- a/docs/docs/50-user-guide/20-how-to-guides/10-logging-in/index.md
+++ b/docs/docs/50-user-guide/20-how-to-guides/10-logging-in/index.md
@@ -5,7 +5,7 @@ description: Learn how to log in to Kargo
 # Logging In
 
 Whether you wish to interact with Kargo through its web-based UI or through
-it's [CLI](../../05-installing-the-cli/index.md), you will need to log in first.
+its [CLI](../../05-installing-the-cli/index.md), you will need to log in first.
 
 1. Obtain the address of your Kargo API server.
 
diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/helm-template.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/helm-template.md
index eb34bbee..4325b011 100644
--- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/helm-template.md
+++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/helm-template.md
@@ -26,6 +26,9 @@ commonly preceded by a [`git-clear` step](git-clear.md) and followed by
 | `skipTests` | `boolean` | N | Whether to skip tests when rendering the manifests. This is `false` by default. |
 | `kubeVersion` | `string` | N | Optionally specifies a Kubernetes version to be assumed when rendering manifests. This is useful for charts that may contain logic specific to different Kubernetes versions. |
 | `apiVersions` | `[]string` | N | Allows a manual set of supported API versions to be specified. |
+| `setValues` | `[]object` | N | Allows for amending chart configuration inline as one would with the `helm template` command's `--set` flag. |
+| `setValues[].key` | `string` | N | The key whose value should be set. For nested values, use dots to delimit key parts. e.g. `image.tag`. |
+| `setValues[].value` | `string` | N | The new value for the key. |
 
 ## Examples
 
diff --git a/docs/docs/50-user-guide/60-reference-docs/40-expressions.md b/docs/docs/50-user-guide/60-reference-docs/40-expressions.md
index 507b58f2..49decb79 100644
--- a/docs/docs/50-user-guide/60-reference-docs/40-expressions.md
+++ b/docs/docs/50-user-guide/60-reference-docs/40-expressions.md
@@ -286,7 +286,14 @@ The returned `FreightOrigin` object has the following fields:
 
 The `FreightOrigin` object can be used as an optional argument to the
 `commitFrom()`, `imageFrom()`, or `chartFrom()` functions to disambiguate the
-desired source of an artifact when necessary.
+desired source of an artifact when necessary. These functions return `nil` when
+relevant `Freight` is not found from the `FreightCollection`. 
+
+:::tip
+You can handle `nil` values gracefully in Expr using its
+[nil coalescing](https://expr-lang.org/docs/language-definition#nil-coalescing) and
+[optional chaining](https://expr-lang.org/docs/language-definition#optional-chaining) features.
+:::
 
 ### `commitFrom(repoURL, [freightOrigin])`
 
@@ -315,6 +322,8 @@ The optional `freightOrigin` argument should be used when a `Stage` requests
 `Freight` from multiple origins (`Warehouse`s) and more than one can provide a
 `GitCommit` object from the specified repository.
 
+If a commit is not found from the `FreightCollection`, returns `nil`.
+
 Examples:
 
 ```yaml
@@ -352,6 +361,8 @@ The optional `freightOrigin` argument should be used when a `Stage` requests
 `Freight` from multiple origins (`Warehouse`s) and more than one can provide a
 `Image` object from the specified repository.
 
+If an image is not found from the `FreightCollection`, returns `nil`.
+
 Examples:
 
 ```yaml
@@ -396,6 +407,8 @@ The optional `freightOrigin` argument should be used when a `Stage` requests
 `Freight` from multiple origins (`Warehouse`s) and more than one can provide a
 `Chart` object from the specified repository.
 
+If a chart is not found from the `FreightCollection`, returns `nil`.
+
 Examples:
 
 ```yaml
diff --git a/docs/docs/50-user-guide/60-reference-docs/50-analysis-templates.md b/docs/docs/50-user-guide/60-reference-docs/50-analysis-templates.md
index b2d68695..12b1b5b6 100644
--- a/docs/docs/50-user-guide/60-reference-docs/50-analysis-templates.md
+++ b/docs/docs/50-user-guide/60-reference-docs/50-analysis-templates.md
@@ -150,6 +150,7 @@ spec:
   metrics:
   - name: total-errors
     interval: 5m
+    count: 3
     failureCondition: result[0] >= 10
     failureLimit: 3
     provider:
diff --git a/go.mod b/go.mod
index 62e1c958..61c61424 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ replace (
 
 require (
 	cloud.google.com/go/compute/metadata v0.6.0
-	code.gitea.io/sdk/gitea v0.20.0
+	code.gitea.io/sdk/gitea v0.21.0
 	connectrpc.com/connect v1.18.1
 	connectrpc.com/grpchealth v1.3.0
 	github.com/AlecAivazis/survey/v2 v2.3.7
@@ -18,14 +18,14 @@ 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.12
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.65
-	github.com/aws/aws-sdk-go-v2/service/ecr v1.43.0
-	github.com/aws/aws-sdk-go-v2/service/sts v1.33.17
+	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/bacongobbler/browser v1.1.0
 	github.com/bmatcuk/doublestar/v4 v4.8.1
 	github.com/bombsimon/logrusr/v4 v4.1.0
-	github.com/coreos/go-oidc/v3 v3.13.0
+	github.com/coreos/go-oidc/v3 v3.14.1
 	github.com/cyphar/filepath-securejoin v0.4.1
 	github.com/evanphx/json-patch/v5 v5.9.11
 	github.com/expr-lang/expr v1.17.2
@@ -56,18 +56,18 @@ require (
 	github.com/tidwall/sjson v1.2.5
 	github.com/valyala/fasttemplate v1.2.2
 	github.com/xeipuuv/gojsonschema v1.2.0
-	gitlab.com/gitlab-org/api/client-go v0.126.0
+	gitlab.com/gitlab-org/api/client-go v0.127.0
 	go.uber.org/ratelimit v0.3.1
-	golang.org/x/crypto v0.36.0
+	golang.org/x/crypto v0.37.0
 	golang.org/x/net v0.38.0
-	golang.org/x/oauth2 v0.28.0
-	golang.org/x/sync v0.12.0
-	golang.org/x/text v0.23.0
+	golang.org/x/oauth2 v0.29.0
+	golang.org/x/sync v0.13.0
+	golang.org/x/text v0.24.0
 	google.golang.org/api v0.228.0
-	google.golang.org/grpc v1.71.0
+	google.golang.org/grpc v1.71.1
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/yaml.v3 v3.0.1
-	helm.sh/helm/v3 v3.17.2
+	helm.sh/helm/v3 v3.17.3
 	k8s.io/api v0.32.3
 	k8s.io/apiextensions-apiserver v0.32.3
 	k8s.io/apimachinery v0.32.3
@@ -87,7 +87,7 @@ require (
 	cloud.google.com/go/auth v0.15.0 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	dario.cat/mergo v1.0.1 // indirect
-	github.com/42wim/httpsig v1.2.1 // indirect
+	github.com/42wim/httpsig v1.2.2 // indirect
 	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
 	github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
 	github.com/BurntSushi/toml v1.4.0 // indirect
@@ -102,8 +102,8 @@ require (
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
 	github.com/aws/smithy-go v1.22.2 // indirect
 	github.com/benbjohnson/clock v1.3.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
@@ -162,7 +162,7 @@ require (
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
-	github.com/hashicorp/go-version v1.6.0 // indirect
+	github.com/hashicorp/go-version v1.7.0 // indirect
 	github.com/huandu/xstrings v1.5.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
@@ -220,8 +220,8 @@ require (
 	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
-	golang.org/x/sys v0.31.0 // indirect
-	golang.org/x/term v0.30.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
+	golang.org/x/term v0.31.0 // indirect
 	golang.org/x/time v0.11.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
diff --git a/go.sum b/go.sum
index 53c8d135..7d5b7bc9 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
 cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
-code.gitea.io/sdk/gitea v0.20.0 h1:Zm/QDwwZK1awoM4AxdjeAQbxolzx2rIP8dDfmKu+KoU=
-code.gitea.io/sdk/gitea v0.20.0/go.mod h1:faouBHC/zyx5wLgjmRKR62ydyvMzwWf3QnU0bH7Cw6U=
+code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
+code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
 connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
 connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
 connectrpc.com/grpchealth v1.3.0 h1:FA3OIwAvuMokQIXQrY5LbIy8IenftksTP/lG4PbYN+E=
@@ -14,8 +14,8 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
 dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
-github.com/42wim/httpsig v1.2.1 h1:oLBxptMe9U4ZmSGtkosT8Dlfg31P3VQnAGq6psXv82Y=
-github.com/42wim/httpsig v1.2.1/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY=
+github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA=
+github.com/42wim/httpsig v1.2.2/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY=
 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/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
@@ -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.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=
-github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ=
+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/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,18 +64,18 @@ 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.0 h1:Ak4Ggvvbg8WYxPLoyLOtes1cIMQePvCAi/dUGqm8hOY=
-github.com/aws/aws-sdk-go-v2/service/ecr v1.43.0/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU=
+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/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=
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E=
-github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
+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/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=
@@ -114,8 +114,8 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS
 github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
 github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
 github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
-github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
-github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
+github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
+github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -288,8 +288,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
 github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
-github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
-github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
+github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=
 github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=
@@ -508,8 +508,8 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-gitlab.com/gitlab-org/api/client-go v0.126.0 h1:VV5TdkF6pMbEdFGvbR2CwEgJwg6qdg1u3bj5eD2tiWk=
-gitlab.com/gitlab-org/api/client-go v0.126.0/go.mod h1:bYC6fPORKSmtuPRyD9Z2rtbAjE7UeNatu2VWHRf4/LE=
+gitlab.com/gitlab-org/api/client-go v0.127.0 h1:8xnxcNKGF2gDazEoMs+hOZfOspSSw8D0vAoWhQk9U+U=
+gitlab.com/gitlab-org/api/client-go v0.127.0/go.mod h1:bYC6fPORKSmtuPRyD9Z2rtbAjE7UeNatu2VWHRf4/LE=
 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=
@@ -562,8 +562,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-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/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -579,16 +579,16 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 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/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
-golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+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=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-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=
@@ -604,18 +604,18 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
 golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -638,8 +638,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-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/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
+google.golang.org/grpc v1.71.1/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=
@@ -660,8 +660,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
 gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
-helm.sh/helm/v3 v3.17.2 h1:agYQ5ew2jq5vdx2K7q5W44KyKQrnSubUMCQsjkiv3/o=
-helm.sh/helm/v3 v3.17.2/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
+helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg=
+helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
 k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
 k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
 k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY=
diff --git a/hack/tools/go.mod b/hack/tools/go.mod
index c40d5af6..05b8661f 100644
--- a/hack/tools/go.mod
+++ b/hack/tools/go.mod
@@ -1,13 +1,14 @@
 module tools
 
 go 1.24.1
+
 require (
 	github.com/bufbuild/buf v1.51.0
 	github.com/golangci/golangci-lint v1.64.8
 	golang.org/x/tools v0.31.0
-	helm.sh/helm/v3 v3.17.2
+	helm.sh/helm/v3 v3.17.3
 	k8s.io/code-generator v0.32.3
-	sigs.k8s.io/controller-tools v0.17.2
+	sigs.k8s.io/controller-tools v0.17.3
 )
 
 require (
diff --git a/hack/tools/go.sum b/hack/tools/go.sum
index 1191f5e9..2c81adee 100644
--- a/hack/tools/go.sum
+++ b/hack/tools/go.sum
@@ -1032,8 +1032,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
 gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
-helm.sh/helm/v3 v3.17.2 h1:agYQ5ew2jq5vdx2K7q5W44KyKQrnSubUMCQsjkiv3/o=
-helm.sh/helm/v3 v3.17.2/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
+helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg=
+helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
 honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
 honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
 k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
@@ -1070,8 +1070,8 @@ oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
 oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
 pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo=
 pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o=
-sigs.k8s.io/controller-tools v0.17.2 h1:jNFOKps8WnaRKZU2R+4vRCHnXyJanVmXBWqkuUPFyFg=
-sigs.k8s.io/controller-tools v0.17.2/go.mod h1:4q5tZG2JniS5M5bkiXY2/potOiXyhoZVw/U48vLkXk0=
+sigs.k8s.io/controller-tools v0.17.3 h1:lwFPLicpBKLgIepah+c8ikRBubFW5kOQyT88r3EwfNw=
+sigs.k8s.io/controller-tools v0.17.3/go.mod h1:1ii+oXcYZkxcBXzwv3YZBlzjt1fvkrCGjVF73blosJI=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=
diff --git a/internal/controller/freight/finder.go b/internal/controller/freight/finder.go
index 2a4c0127..49b509b0 100644
--- a/internal/controller/freight/finder.go
+++ b/internal/controller/freight/finder.go
@@ -73,9 +73,7 @@ func FindCommit(
 		}
 	}
 	if desiredOrigin == nil {
-		return nil, NotFoundError{
-			msg: fmt.Sprintf("commit from repo %s not found in referenced Freight", repoURL),
-		}
+		return nil, nil
 	}
 	// We know exactly what we're after, so this should be easy
 	for i := range freight {
@@ -92,9 +90,7 @@ func FindCommit(
 	// If we get to here, we looked at all the FreightReferences and didn't find
 	// any that came from the desired origin. This could be because no Freight
 	// from the desired origin has been promoted yet.
-	return nil, NotFoundError{
-		msg: fmt.Sprintf("commit from repo %s not found in referenced Freight", repoURL),
-	}
+	return nil, nil
 }
 
 func FindImage(
@@ -147,9 +143,7 @@ func FindImage(
 	if desiredOrigin == nil {
 		// There is no chance of finding the commit we're looking for. Just return
 		// nil and let the caller decide what to do.
-		return nil, NotFoundError{
-			msg: fmt.Sprintf("image from repo %s not found in referenced Freight", repoURL),
-		}
+		return nil, nil
 	}
 	// We know exactly what we're after, so this should be easy
 	for _, f := range freight {
@@ -163,10 +157,9 @@ func FindImage(
 	}
 	// If we get to here, we looked at all the FreightReferences and didn't find
 	// any that came from the desired origin. This could be because no Freight
-	// from the desired origin has been promoted yet.
-	return nil, NotFoundError{
-		msg: fmt.Sprintf("image from repo %s not found in referenced Freight", repoURL),
-	}
+	// from the desired origin has been promoted yet. We return nil to indicate
+	// that none was found
+	return nil, nil
 }
 
 func HasAmbiguousImageRequest(
@@ -285,12 +278,5 @@ func FindChart(
 	// If we get to here, we looked at all the FreightReferences and didn't find
 	// any that came from the desired origin. This could be because no Freight
 	// from the desired origin has been promoted yet.
-	if chartName == "" {
-		return nil, NotFoundError{
-			msg: fmt.Sprintf("chart from repo %s not found in referenced Freight", repoURL),
-		}
-	}
-	return nil, NotFoundError{
-		msg: fmt.Sprintf("chart %q from repo %s not found in referenced Freight", chartName, repoURL),
-	}
+	return nil, nil
 }
diff --git a/internal/controller/promotions/promotions.go b/internal/controller/promotions/promotions.go
index d6540e50..97230d45 100644
--- a/internal/controller/promotions/promotions.go
+++ b/internal/controller/promotions/promotions.go
@@ -104,7 +104,7 @@ func SetupReconcilerWithManager(
 		ctx,
 		&kargoapi.Promotion{},
 		indexer.RunningPromotionsByArgoCDApplicationsField,
-		indexer.RunningPromotionsByArgoCDApplications(ctx, cfg.ShardName),
+		indexer.RunningPromotionsByArgoCDApplications(ctx, kargoMgr.GetClient(), cfg.ShardName),
 	); err != nil {
 		return fmt.Errorf("index running Promotions by Argo CD Applications: %w", err)
 	}
diff --git a/internal/expressions/json_templates.go b/internal/expressions/json_templates.go
index cfeebe7d..8e639be4 100644
--- a/internal/expressions/json_templates.go
+++ b/internal/expressions/json_templates.go
@@ -113,6 +113,9 @@ func EvaluateTemplate(template string, env map[string]any, exprOpts ...expr.Opti
 	exprOpts = append(exprOpts, expr.Function(
 		"quote",
 		func(a ...any) (any, error) {
+			if str, ok := a[0].(string); ok {
+				return fmt.Sprintf("%q", str), nil
+			}
 			jsonBytes, err := json.Marshal(a[0])
 			if err != nil {
 				return nil, fmt.Errorf("error applying quote() function: %w", err)
diff --git a/internal/indexer/indexer.go b/internal/indexer/indexer.go
index c2aef870..c33255fd 100644
--- a/internal/indexer/indexer.go
+++ b/internal/indexer/indexer.go
@@ -114,6 +114,7 @@ func PromotionsByStage(obj client.Object) []string {
 // Promotions not labeled with a shardName are indexed.
 func RunningPromotionsByArgoCDApplications(
 	ctx context.Context,
+	cl client.Client,
 	shardName string,
 ) client.IndexerFunc {
 	logger := logging.LoggerFromContext(ctx)
@@ -176,7 +177,7 @@ func RunningPromotionsByArgoCDApplications(
 
 			// As step-level variables are allowed to reference to output, we
 			// need to provide the state.
-			vars, err := dirStep.GetVars(promoCtx, promoCtx.State)
+			vars, err := dirStep.GetVars(ctx, cl, promoCtx, promoCtx.State)
 			if err != nil {
 				logger.Error(
 					err,
diff --git a/internal/kargo/promotion_builder.go b/internal/kargo/promotion_builder.go
index c0dc6927..a368bf66 100644
--- a/internal/kargo/promotion_builder.go
+++ b/internal/kargo/promotion_builder.go
@@ -71,6 +71,11 @@ func (b *PromotionBuilder) Build(
 		annotations[kargoapi.AnnotationKeyCreateActor] = api.FormatEventUserActor(u)
 	}
 
+	// Merge Stage variables with PromotionTemplate variables
+	vars := make([]kargoapi.ExpressionVariable, 0, len(stage.Spec.Vars)+len(stage.Spec.PromotionTemplate.Spec.Vars))
+	vars = append(vars, stage.Spec.Vars...)
+	vars = append(vars, stage.Spec.PromotionTemplate.Spec.Vars...)
+
 	promotion := kargoapi.Promotion{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        generatePromotionName(stage.Name, freight),
@@ -80,7 +85,7 @@ func (b *PromotionBuilder) Build(
 		Spec: kargoapi.PromotionSpec{
 			Stage:   stage.Name,
 			Freight: freight,
-			Vars:    stage.Spec.PromotionTemplate.Spec.Vars,
+			Vars:    vars,
 			Steps:   stage.Spec.PromotionTemplate.Spec.Steps,
 		},
 	}
@@ -121,7 +126,7 @@ func (b *PromotionBuilder) InflateSteps(ctx context.Context, promo *kargoapi.Pro
 func (b *PromotionBuilder) inflateTaskSteps(
 	ctx context.Context,
 	project, taskAlias string,
-	promoVars []kargoapi.PromotionVariable,
+	promoVars []kargoapi.ExpressionVariable,
 	taskStep kargoapi.PromotionStep,
 ) ([]kargoapi.PromotionStep, error) {
 	task, err := b.getTaskSpec(ctx, project, taskStep.Task)
@@ -220,8 +225,8 @@ func generatePromotionTaskStepAlias(taskAlias, stepAlias string) string {
 // variables and maps them to variables which can be used by the inflated
 // PromotionStep.
 func promotionTaskVarsToStepVars(
-	taskVars, promoVars, stepVars []kargoapi.PromotionVariable,
-) ([]kargoapi.PromotionVariable, error) {
+	taskVars, promoVars, stepVars []kargoapi.ExpressionVariable,
+) ([]kargoapi.ExpressionVariable, error) {
 	// Promotion variables can be used to set (or override) the variables
 	// required by the PromotionTask, but they are not inflated into the
 	// variables for the step. This map is used to check if a variable is
@@ -244,7 +249,7 @@ func promotionTaskVarsToStepVars(
 		}
 	}
 
-	var vars []kargoapi.PromotionVariable
+	var vars []kargoapi.ExpressionVariable
 
 	// Set the PromotionTask variable default values, but only if the variable
 	// is not set on the Promotion.
diff --git a/internal/promotion/promotion.go b/internal/promotion/promotion.go
index fe4f34f8..c19163ef 100644
--- a/internal/promotion/promotion.go
+++ b/internal/promotion/promotion.go
@@ -54,7 +54,7 @@ type Context struct {
 	State promotion.State
 	// Vars is a list of variables definitions that can be used by the
 	// Steps.
-	Vars []kargoapi.PromotionVariable
+	Vars []kargoapi.ExpressionVariable
 	// Secrets is a map of secrets that can be used by the Steps.
 	Secrets map[string]map[string]string
 	// Actor is the name of the actor triggering the Promotion.
@@ -81,7 +81,7 @@ type Step struct {
 	Retry *kargoapi.PromotionStepRetry
 	// Vars is a list of variables definitions that can be used by the
 	// Step.
-	Vars []kargoapi.PromotionVariable
+	Vars []kargoapi.ExpressionVariable
 	// Config is an opaque JSON to be passed to the StepRunner executing this
 	// step.
 	Config []byte
@@ -186,12 +186,17 @@ func (s *Step) BuildEnv(promoCtx Context, opts ...StepEnvOption) map[string]any
 
 // Skip returns true if the Step should be skipped based on the If condition.
 // The If condition is evaluated against the provided Context and State.
-func (s *Step) Skip(promoCtx Context, state promotion.State) (bool, error) {
+func (s *Step) Skip(
+	ctx context.Context,
+	cl client.Client,
+	promoCtx Context,
+	state promotion.State,
+) (bool, error) {
 	if s.If == "" {
 		return false, nil
 	}
 
-	vars, err := s.GetVars(promoCtx, state)
+	vars, err := s.GetVars(ctx, cl, promoCtx, state)
 	if err != nil {
 		return false, err
 	}
@@ -203,7 +208,13 @@ func (s *Step) Skip(promoCtx Context, state promotion.State) (bool, error) {
 		StepEnvWithVars(vars),
 	)
 
-	v, err := expressions.EvaluateTemplate(s.If, env)
+	v, err := expressions.EvaluateTemplate(s.If, env, exprfn.FreightOperations(
+		ctx,
+		cl,
+		promoCtx.Project,
+		promoCtx.FreightRequests,
+		promoCtx.Freight.References(),
+	)...)
 	if err != nil {
 		return false, err
 	}
@@ -228,7 +239,7 @@ func (s *Step) GetConfig(
 		return nil, nil
 	}
 
-	vars, err := s.GetVars(promoCtx, state)
+	vars, err := s.GetVars(ctx, cl, promoCtx, state)
 	if err != nil {
 		return nil, err
 	}
@@ -264,7 +275,12 @@ func (s *Step) GetConfig(
 
 // GetVars returns the variables defined in the Step. The variables are
 // evaluated against the provided Context.
-func (s *Step) GetVars(promoCtx Context, state promotion.State) (map[string]any, error) {
+func (s *Step) GetVars(
+	ctx context.Context,
+	cl client.Client,
+	promoCtx Context,
+	state promotion.State,
+) (map[string]any, error) {
 	vars := make(map[string]any)
 
 	// Evaluate the global variables defined in the Promotion itself, these
@@ -273,6 +289,7 @@ func (s *Step) GetVars(promoCtx Context, state promotion.State) (map[string]any,
 		newVar, err := expressions.EvaluateTemplate(
 			v.Value,
 			s.BuildEnv(promoCtx, StepEnvWithVars(vars)),
+			exprfn.FreightOperations(ctx, cl, promoCtx.Project, promoCtx.FreightRequests, promoCtx.Freight.References())...,
 		)
 		if err != nil {
 			return nil, fmt.Errorf("error pre-processing promotion variable %q: %w", v.Name, err)
@@ -291,6 +308,7 @@ func (s *Step) GetVars(promoCtx Context, state promotion.State) (map[string]any,
 				StepEnvWithTaskOutputs(s.Alias, state),
 				StepEnvWithVars(vars),
 			),
+			exprfn.FreightOperations(ctx, cl, promoCtx.Project, promoCtx.FreightRequests, promoCtx.Freight.References())...,
 		)
 		if err != nil {
 			return nil, fmt.Errorf("error pre-processing promotion variable %q: %w", v.Name, err)
diff --git a/internal/promotion/runner/builtin/helm_template_runner.go b/internal/promotion/runner/builtin/helm_template_runner.go
index 727740e4..4b7dc256 100644
--- a/internal/promotion/runner/builtin/helm_template_runner.go
+++ b/internal/promotion/runner/builtin/helm_template_runner.go
@@ -81,7 +81,7 @@ func (h *helmTemplateRunner) run(
 	stepCtx *promotion.StepContext,
 	cfg builtin.HelmTemplateConfig,
 ) (promotion.StepResult, error) {
-	composedValues, err := h.composeValues(stepCtx.WorkDir, cfg.ValuesFiles)
+	composedValues, err := h.composeValues(stepCtx.WorkDir, cfg)
 	if err != nil {
 		return promotion.StepResult{Status: kargoapi.PromotionPhaseErrored},
 			fmt.Errorf("failed to compose values: %w", err)
@@ -123,17 +123,26 @@ func (h *helmTemplateRunner) run(
 	return promotion.StepResult{Status: kargoapi.PromotionPhaseSucceeded}, nil
 }
 
-// composeValues composes the values from the given values files. It merges the
-// values in the order they are provided.
-func (h *helmTemplateRunner) composeValues(workDir string, valuesFiles []string) (map[string]any, error) {
+// composeValues composes the values from the given values files and set values.
+// It merges the value files in the order they are provided. Set values are merged
+// into last as overrides
+func (h *helmTemplateRunner) composeValues(
+	workDir string,
+	cfg builtin.HelmTemplateConfig,
+) (map[string]any, error) {
 	valueOpts := &values.Options{}
-	for _, p := range valuesFiles {
+	for _, p := range cfg.ValuesFiles {
 		absValuesPath, err := securejoin.SecureJoin(workDir, p)
 		if err != nil {
 			return nil, fmt.Errorf("failed to join path %q: %w", p, err)
 		}
 		valueOpts.ValueFiles = append(valueOpts.ValueFiles, absValuesPath)
 	}
+	setValueStrVal := make([]string, len(cfg.SetValues))
+	for i, setValue := range cfg.SetValues {
+		setValueStrVal[i] = fmt.Sprintf("%s=%s", setValue.Key, setValue.Value)
+	}
+	valueOpts.Values = append(valueOpts.Values, setValueStrVal...)
 	return valueOpts.MergeValues(nil)
 }
 
diff --git a/internal/promotion/runner/builtin/schemas/helm-template-config.json b/internal/promotion/runner/builtin/schemas/helm-template-config.json
index 69aecdfe..149d9717 100644
--- a/internal/promotion/runner/builtin/schemas/helm-template-config.json
+++ b/internal/promotion/runner/builtin/schemas/helm-template-config.json
@@ -1,6 +1,26 @@
 {
   "$schema": "https://json-schema.org/draft/2020-12/schema",
   "title": "HelmTemplateConfig",
+
+  "definitions": {
+    "setValues": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "key": {
+          "type": "string",
+          "description": "The key whose value should be set. For nested values, use dots to delimit key parts. e.g. `image.tag`.",
+          "minLength": 1
+        },
+        "value": {
+          "type": "string",
+          "description": "The new value for the key."
+        }
+      },
+      "required": ["key", "value"]
+    }
+  },
+
   "type": "object",
   "additionalProperties": false,
   "required": ["outPath", "path", "releaseName"],
@@ -37,6 +57,13 @@
         "minLength": 1
       }
     },
+    "setValues": {
+      "type": "array",
+      "description": "Allows for amending chart configuration inline as one would with the `helm template` command's `--set` flag.",
+      "items": {
+        "$ref": "#/definitions/setValues"
+      }
+    },
     "includeCRDs": {
       "type": "boolean",
       "description": "Whether to include CRDs in the rendered manifests.",
diff --git a/internal/promotion/simple_engine.go b/internal/promotion/simple_engine.go
index a5377acc..2b7491e2 100644
--- a/internal/promotion/simple_engine.go
+++ b/internal/promotion/simple_engine.go
@@ -143,7 +143,7 @@ func (e *simpleEngine) executeSteps(
 
 		// Check if the step should be skipped.
 		var skip bool
-		if skip, err = step.Skip(promoCtx, state); err != nil {
+		if skip, err = step.Skip(ctx, e.kargoClient, promoCtx, state); err != nil {
 			return Result{
 				Status:                kargoapi.PromotionPhaseErrored,
 				CurrentStep:           i,
diff --git a/internal/rollouts/analysis_run_builder.go b/internal/rollouts/analysis_run_builder.go
index b08a135a..2d0b3526 100644
--- a/internal/rollouts/analysis_run_builder.go
+++ b/internal/rollouts/analysis_run_builder.go
@@ -24,6 +24,9 @@ import (
 // controller instance.
 const controllerInstanceIDLabelKey = "argo-rollouts.argoproj.io/controller-instance-id"
 
+// varsEnvKey is the key for variables in the ArgumentEvaluationConfig.Env map.
+const varsEnvKey = "vars"
+
 // Config holds the configuration for the AnalysisRunBuilder.
 type Config struct {
 	// ControllerInstanceID is the unique identifier for the Argo Rollouts
@@ -192,6 +195,28 @@ func (b *AnalysisRunBuilder) buildArgs(
 		exprCfg = &ArgumentEvaluationConfig{}
 	}
 
+	if len(exprCfg.Vars) > 0 {
+		if exprCfg.Env == nil {
+			exprCfg.Env = make(map[string]any)
+		}
+		if exprCfg.Env[varsEnvKey] == nil {
+			exprCfg.Env[varsEnvKey] = make(map[string]any)
+		}
+		// NB: This will cause direct modifications to the map in the config.
+		vars, ok := exprCfg.Env[varsEnvKey].(map[string]any)
+		if !ok {
+			return nil, fmt.Errorf("vars in argument evaluation config env is of unexpected type %T", exprCfg.Env["vars"])
+		}
+
+		for _, v := range exprCfg.Vars {
+			value, err := expressions.EvaluateTemplate(v.Value, exprCfg.Env, exprCfg.Options...)
+			if err != nil {
+				return nil, fmt.Errorf("evaluate variable %q: %w", v.Name, err)
+			}
+			vars[v.Name] = value
+		}
+	}
+
 	rolloutsArgs := make([]rolloutsapi.Argument, len(args))
 	for i, arg := range args {
 		rolloutsArgs[i] = rolloutsapi.Argument{
diff --git a/internal/rollouts/options.go b/internal/rollouts/options.go
index a26d6aa3..80b52b42 100644
--- a/internal/rollouts/options.go
+++ b/internal/rollouts/options.go
@@ -45,19 +45,41 @@ type Owner struct {
 }
 
 // ArgumentEvaluationConfig holds the configuration for the evaluation of
-// expressions in the AnalysisRun arguments.
+// expressions in the AnalysisRun arguments. It contains the environment
+// variables, the variables, and the options for the expression evaluation.
+//
+// Note: The builder may modify the Env map to add additional information.
+// For example, to add any evaluated Vars. Because of this, it is recommended
+// not to reuse the same ArgumentEvaluationConfig or Env map for multiple
+// builds without making use of a deep copy.
 type ArgumentEvaluationConfig struct {
 	// Env is a (nested) map of variables that can be used in the expressions.
 	// The keys are the variable names, and the values are the variable values.
 	// When the value itself is a map, it is considered a nested variable and
 	// can be accessed using dot notation. e.g. `${{ foo.bar }}`.
+	//
+	// Note: The Env map can be modified by the builder to add additional
+	// information. For example, to add any evaluated Vars.
 	Env map[string]any
+	// Vars are the variables that can be used in the expressions. They are
+	// evaluated in the order they are defined. After the evaluation of the
+	// variables, they are available in the Env map as `${{ vars.<name>}}`.
+	Vars []ArgumentVariable
 	// Options are the options for the expression evaluation. It can be used to
 	// configure the behavior of the expression evaluation and the functions
 	// available.
 	Options []expr.Option
 }
 
+// ArgumentVariable represents a variable that can be used in the AnalysisRun
+// arguments.
+type ArgumentVariable struct {
+	// Name is the name of the variable.
+	Name string
+	// Value is the value of the variable.
+	Value string
+}
+
 // Apply applies the given options to the AnalysisRunOptions.
 func (o *AnalysisRunOptions) Apply(opts ...AnalysisRunOption) {
 	for _, opt := range opts {
diff --git a/pkg/x/promotion/runner/builtin/zz_config_types.go b/pkg/x/promotion/runner/builtin/zz_config_types.go
index c95e938c..ba4f7071 100644
--- a/pkg/x/promotion/runner/builtin/zz_config_types.go
+++ b/pkg/x/promotion/runner/builtin/zz_config_types.go
@@ -231,6 +231,9 @@ type HelmTemplateConfig struct {
 	Path string `json:"path"`
 	// ReleaseName to use for the rendered manifests.
 	ReleaseName string `json:"releaseName"`
+	// Allows for amending chart configuration inline as one would with the `helm template`
+	// command's `--set` flag.
+	SetValues []SetValues `json:"setValues,omitempty"`
 	// Whether to skip tests when rendering the manifests.
 	SkipTests bool `json:"skipTests,omitempty"`
 	// Whether to use the release name in the output path (instead of the chart name). This only
@@ -240,6 +243,14 @@ type HelmTemplateConfig struct {
 	ValuesFiles []string `json:"valuesFiles,omitempty"`
 }
 
+type SetValues struct {
+	// The key whose value should be set. For nested values, use dots to delimit key parts. e.g.
+	// `image.tag`.
+	Key string `json:"key"`
+	// The new value for the key.
+	Value string `json:"value"`
+}
+
 type HelmUpdateChartConfig struct {
 	// A list of chart dependencies which should receive updates.
 	Charts []Chart `json:"charts"`
diff --git a/ui/package.json b/ui/package.json
index 61372929..aa0bbb3b 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -16,6 +16,8 @@
   "devDependencies": {
     "@apidevtools/json-schema-ref-parser": "^11.9.3",
     "@eslint/compat": "^1.2.7",
+    "@eslint/eslintrc": "^3.3.0",
+    "@eslint/js": "^9.22.0",
     "@openapi-contrib/openapi-schema-to-json-schema": "^5.1.0",
     "@tanstack/react-query-devtools": "^5.71.0",
     "@types/git-url-parse": "^9.0.3",
@@ -43,7 +45,7 @@
     "prettier": "^3.5.3",
     "tailwindcss": "^3.4.17",
     "typescript": "^5.8.2",
-    "vite": "^6.2.4",
+    "vite": "^6.2.5",
     "vite-plugin-compression": "^0.5.1",
     "vite-tsconfig-paths": "^5.1.4",
     "vitest": "^3.0.9",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index f0e9affa..4c774820 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -123,6 +123,12 @@ importers:
       '@eslint/compat':
         specifier: ^1.2.7
         version: 1.2.7(eslint@9.23.0(jiti@1.21.7))
+      '@eslint/eslintrc':
+        specifier: ^3.3.0
+        version: 3.3.1
+      '@eslint/js':
+        specifier: ^9.22.0
+        version: 9.23.0
       '@openapi-contrib/openapi-schema-to-json-schema':
         specifier: ^5.1.0
         version: 5.1.0
@@ -158,7 +164,7 @@ importers:
         version: 8.28.0(eslint@9.23.0(jiti@1.21.7))(typescript@5.8.2)
       '@vitejs/plugin-react':
         specifier: ^4.3.4
-        version: 4.3.4(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
+        version: 4.3.4(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
       autoprefixer:
         specifier: ^10.4.21
         version: 10.4.21(postcss@8.5.3)
@@ -205,14 +211,14 @@ importers:
         specifier: ^5.8.2
         version: 5.8.2
       vite:
-        specifier: ^6.2.4
-        version: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+        specifier: ^6.2.5
+        version: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
       vite-plugin-compression:
         specifier: ^0.5.1
-        version: 0.5.1(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
+        version: 0.5.1(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
       vite-tsconfig-paths:
         specifier: ^5.1.4
-        version: 5.1.4(typescript@5.8.2)(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
+        version: 5.1.4(typescript@5.8.2)(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
       vitest:
         specifier: ^3.0.9
         version: 3.0.9(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
@@ -788,103 +794,103 @@ packages:
     peerDependencies:
       '@rjsf/utils': ^5.24.x
 
-  '@rollup/rollup-android-arm-eabi@4.38.0':
-    resolution: {integrity: sha512-ldomqc4/jDZu/xpYU+aRxo3V4mGCV9HeTgUBANI3oIQMOL+SsxB+S2lxMpkFp5UamSS3XuTMQVbsS24R4J4Qjg==}
+  '@rollup/rollup-android-arm-eabi@4.39.0':
+    resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==}
     cpu: [arm]
     os: [android]
 
-  '@rollup/rollup-android-arm64@4.38.0':
-    resolution: {integrity: sha512-VUsgcy4GhhT7rokwzYQP+aV9XnSLkkhlEJ0St8pbasuWO/vwphhZQxYEKUP3ayeCYLhk6gEtacRpYP/cj3GjyQ==}
+  '@rollup/rollup-android-arm64@4.39.0':
+    resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==}
     cpu: [arm64]
     os: [android]
 
-  '@rollup/rollup-darwin-arm64@4.38.0':
-    resolution: {integrity: sha512-buA17AYXlW9Rn091sWMq1xGUvWQFOH4N1rqUxGJtEQzhChxWjldGCCup7r/wUnaI6Au8sKXpoh0xg58a7cgcpg==}
+  '@rollup/rollup-darwin-arm64@4.39.0':
+    resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==}
     cpu: [arm64]
     os: [darwin]
 
-  '@rollup/rollup-darwin-x64@4.38.0':
-    resolution: {integrity: sha512-Mgcmc78AjunP1SKXl624vVBOF2bzwNWFPMP4fpOu05vS0amnLcX8gHIge7q/lDAHy3T2HeR0TqrriZDQS2Woeg==}
+  '@rollup/rollup-darwin-x64@4.39.0':
+    resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==}
     cpu: [x64]
     os: [darwin]
 
-  '@rollup/rollup-freebsd-arm64@4.38.0':
-    resolution: {integrity: sha512-zzJACgjLbQTsscxWqvrEQAEh28hqhebpRz5q/uUd1T7VTwUNZ4VIXQt5hE7ncs0GrF+s7d3S4on4TiXUY8KoQA==}
+  '@rollup/rollup-freebsd-arm64@4.39.0':
+    resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==}
     cpu: [arm64]
     os: [freebsd]
 
-  '@rollup/rollup-freebsd-x64@4.38.0':
-    resolution: {integrity: sha512-hCY/KAeYMCyDpEE4pTETam0XZS4/5GXzlLgpi5f0IaPExw9kuB+PDTOTLuPtM10TlRG0U9OSmXJ+Wq9J39LvAg==}
+  '@rollup/rollup-freebsd-x64@4.39.0':
+    resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==}
     cpu: [x64]
     os: [freebsd]
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.38.0':
-    resolution: {integrity: sha512-mimPH43mHl4JdOTD7bUMFhBdrg6f9HzMTOEnzRmXbOZqjijCw8LA5z8uL6LCjxSa67H2xiLFvvO67PT05PRKGg==}
+  '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
+    resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm-musleabihf@4.38.0':
-    resolution: {integrity: sha512-tPiJtiOoNuIH8XGG8sWoMMkAMm98PUwlriOFCCbZGc9WCax+GLeVRhmaxjJtz6WxrPKACgrwoZ5ia/uapq3ZVg==}
+  '@rollup/rollup-linux-arm-musleabihf@4.39.0':
+    resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==}
     cpu: [arm]
     os: [linux]
 
-  '@rollup/rollup-linux-arm64-gnu@4.38.0':
-    resolution: {integrity: sha512-wZco59rIVuB0tjQS0CSHTTUcEde+pXQWugZVxWaQFdQQ1VYub/sTrNdY76D1MKdN2NB48JDuGABP6o6fqos8mA==}
+  '@rollup/rollup-linux-arm64-gnu@4.39.0':
+    resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==}
     cpu: [arm64]
     os: [linux]
 
-  '@rollup/rollup-linux-arm64-musl@4.38.0':
-    resolution: {integrity: sha512-fQgqwKmW0REM4LomQ+87PP8w8xvU9LZfeLBKybeli+0yHT7VKILINzFEuggvnV9M3x1Ed4gUBmGUzCo/ikmFbQ==}
+  '@rollup/rollup-linux-arm64-musl@4.39.0':
+    resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==}
     cpu: [arm64]
     os: [linux]
 
-  '@rollup/rollup-linux-loongarch64-gnu@4.38.0':
-    resolution: {integrity: sha512-hz5oqQLXTB3SbXpfkKHKXLdIp02/w3M+ajp8p4yWOWwQRtHWiEOCKtc9U+YXahrwdk+3qHdFMDWR5k+4dIlddg==}
+  '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
+    resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==}
     cpu: [loong64]
     os: [linux]
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.38.0':
-    resolution: {integrity: sha512-NXqygK/dTSibQ+0pzxsL3r4Xl8oPqVoWbZV9niqOnIHV/J92fe65pOir0xjkUZDRSPyFRvu+4YOpJF9BZHQImw==}
+  '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
+    resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==}
     cpu: [ppc64]
     os: [linux]
 
-  '@rollup/rollup-linux-riscv64-gnu@4.38.0':
-    resolution: {integrity: sha512-GEAIabR1uFyvf/jW/5jfu8gjM06/4kZ1W+j1nWTSSB3w6moZEBm7iBtzwQ3a1Pxos2F7Gz+58aVEnZHU295QTg==}
+  '@rollup/rollup-linux-riscv64-gnu@4.39.0':
+    resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==}
     cpu: [riscv64]
     os: [linux]
 
-  '@rollup/rollup-linux-riscv64-musl@4.38.0':
-    resolution: {integrity: sha512-9EYTX+Gus2EGPbfs+fh7l95wVADtSQyYw4DfSBcYdUEAmP2lqSZY0Y17yX/3m5VKGGJ4UmIH5LHLkMJft3bYoA==}
+  '@rollup/rollup-linux-riscv64-musl@4.39.0':
+    resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==}
     cpu: [riscv64]
     os: [linux]
 
-  '@rollup/rollup-linux-s390x-gnu@4.38.0':
-    resolution: {integrity: sha512-Mpp6+Z5VhB9VDk7RwZXoG2qMdERm3Jw07RNlXHE0bOnEeX+l7Fy4bg+NxfyN15ruuY3/7Vrbpm75J9QHFqj5+Q==}
+  '@rollup/rollup-linux-s390x-gnu@4.39.0':
+    resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==}
     cpu: [s390x]
     os: [linux]
 
-  '@rollup/rollup-linux-x64-gnu@4.38.0':
-    resolution: {integrity: sha512-vPvNgFlZRAgO7rwncMeE0+8c4Hmc+qixnp00/Uv3ht2x7KYrJ6ERVd3/R0nUtlE6/hu7/HiiNHJ/rP6knRFt1w==}
+  '@rollup/rollup-linux-x64-gnu@4.39.0':
+    resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==}
     cpu: [x64]
     os: [linux]
 
-  '@rollup/rollup-linux-x64-musl@4.38.0':
-    resolution: {integrity: sha512-q5Zv+goWvQUGCaL7fU8NuTw8aydIL/C9abAVGCzRReuj5h30TPx4LumBtAidrVOtXnlB+RZkBtExMsfqkMfb8g==}
+  '@rollup/rollup-linux-x64-musl@4.39.0':
+    resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==}
     cpu: [x64]
     os: [linux]
 
-  '@rollup/rollup-win32-arm64-msvc@4.38.0':
-    resolution: {integrity: sha512-u/Jbm1BU89Vftqyqbmxdq14nBaQjQX1HhmsdBWqSdGClNaKwhjsg5TpW+5Ibs1mb8Es9wJiMdl86BcmtUVXNZg==}
+  '@rollup/rollup-win32-arm64-msvc@4.39.0':
+    resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==}
     cpu: [arm64]
     os: [win32]
 
-  '@rollup/rollup-win32-ia32-msvc@4.38.0':
-    resolution: {integrity: sha512-mqu4PzTrlpNHHbu5qleGvXJoGgHpChBlrBx/mEhTPpnAL1ZAYFlvHD7rLK839LLKQzqEQMFJfGrrOHItN4ZQqA==}
+  '@rollup/rollup-win32-ia32-msvc@4.39.0':
+    resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==}
     cpu: [ia32]
     os: [win32]
 
-  '@rollup/rollup-win32-x64-msvc@4.38.0':
-    resolution: {integrity: sha512-jjqy3uWlecfB98Psxb5cD6Fny9Fupv9LrDSPTQZUROqjvZmcCqNu4UMl7qqhlUUGpwiAkotj6GYu4SZdcr/nLw==}
+  '@rollup/rollup-win32-x64-msvc@4.39.0':
+    resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==}
     cpu: [x64]
     os: [win32]
 
@@ -3029,8 +3035,8 @@ packages:
     resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
 
-  rollup@4.38.0:
-    resolution: {integrity: sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==}
+  rollup@4.39.0:
+    resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
@@ -3393,8 +3399,8 @@ packages:
       vite:
         optional: true
 
-  vite@6.2.4:
-    resolution: {integrity: sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==}
+  vite@6.2.5:
+    resolution: {integrity: sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
     hasBin: true
     peerDependencies:
@@ -4118,64 +4124,64 @@ snapshots:
       lodash: 4.17.21
       lodash-es: 4.17.21
 
-  '@rollup/rollup-android-arm-eabi@4.38.0':
+  '@rollup/rollup-android-arm-eabi@4.39.0':
     optional: true
 
-  '@rollup/rollup-android-arm64@4.38.0':
+  '@rollup/rollup-android-arm64@4.39.0':
     optional: true
 
-  '@rollup/rollup-darwin-arm64@4.38.0':
+  '@rollup/rollup-darwin-arm64@4.39.0':
     optional: true
 
-  '@rollup/rollup-darwin-x64@4.38.0':
+  '@rollup/rollup-darwin-x64@4.39.0':
     optional: true
 
-  '@rollup/rollup-freebsd-arm64@4.38.0':
+  '@rollup/rollup-freebsd-arm64@4.39.0':
     optional: true
 
-  '@rollup/rollup-freebsd-x64@4.38.0':
+  '@rollup/rollup-freebsd-x64@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.38.0':
+  '@rollup/rollup-linux-arm-gnueabihf@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-arm-musleabihf@4.38.0':
+  '@rollup/rollup-linux-arm-musleabihf@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-arm64-gnu@4.38.0':
+  '@rollup/rollup-linux-arm64-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-arm64-musl@4.38.0':
+  '@rollup/rollup-linux-arm64-musl@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-loongarch64-gnu@4.38.0':
+  '@rollup/rollup-linux-loongarch64-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.38.0':
+  '@rollup/rollup-linux-powerpc64le-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-gnu@4.38.0':
+  '@rollup/rollup-linux-riscv64-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-musl@4.38.0':
+  '@rollup/rollup-linux-riscv64-musl@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-s390x-gnu@4.38.0':
+  '@rollup/rollup-linux-s390x-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-x64-gnu@4.38.0':
+  '@rollup/rollup-linux-x64-gnu@4.39.0':
     optional: true
 
-  '@rollup/rollup-linux-x64-musl@4.38.0':
+  '@rollup/rollup-linux-x64-musl@4.39.0':
     optional: true
 
-  '@rollup/rollup-win32-arm64-msvc@4.38.0':
+  '@rollup/rollup-win32-arm64-msvc@4.39.0':
     optional: true
 
-  '@rollup/rollup-win32-ia32-msvc@4.38.0':
+  '@rollup/rollup-win32-ia32-msvc@4.39.0':
     optional: true
 
-  '@rollup/rollup-win32-x64-msvc@4.38.0':
+  '@rollup/rollup-win32-x64-msvc@4.39.0':
     optional: true
 
   '@rtsao/scc@1.1.0': {}
@@ -4374,14 +4380,14 @@ snapshots:
       '@typescript-eslint/types': 8.28.0
       eslint-visitor-keys: 4.2.0
 
-  '@vitejs/plugin-react@4.3.4(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))':
+  '@vitejs/plugin-react@4.3.4(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))':
     dependencies:
       '@babel/core': 7.26.0
       '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0)
       '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0)
       '@types/babel__core': 7.20.5
       react-refresh: 0.14.2
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
     transitivePeerDependencies:
       - supports-color
 
@@ -4392,13 +4398,13 @@ snapshots:
       chai: 5.2.0
       tinyrainbow: 2.0.0
 
-  '@vitest/mocker@3.0.9(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))':
+  '@vitest/mocker@3.0.9(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))':
     dependencies:
       '@vitest/spy': 3.0.9
       estree-walker: 3.0.3
       magic-string: 0.30.17
     optionalDependencies:
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
 
   '@vitest/pretty-format@3.0.9':
     dependencies:
@@ -6706,30 +6712,30 @@ snapshots:
 
   reusify@1.0.4: {}
 
-  rollup@4.38.0:
+  rollup@4.39.0:
     dependencies:
       '@types/estree': 1.0.7
     optionalDependencies:
-      '@rollup/rollup-android-arm-eabi': 4.38.0
-      '@rollup/rollup-android-arm64': 4.38.0
-      '@rollup/rollup-darwin-arm64': 4.38.0
-      '@rollup/rollup-darwin-x64': 4.38.0
-      '@rollup/rollup-freebsd-arm64': 4.38.0
-      '@rollup/rollup-freebsd-x64': 4.38.0
-      '@rollup/rollup-linux-arm-gnueabihf': 4.38.0
-      '@rollup/rollup-linux-arm-musleabihf': 4.38.0
-      '@rollup/rollup-linux-arm64-gnu': 4.38.0
-      '@rollup/rollup-linux-arm64-musl': 4.38.0
-      '@rollup/rollup-linux-loongarch64-gnu': 4.38.0
-      '@rollup/rollup-linux-powerpc64le-gnu': 4.38.0
-      '@rollup/rollup-linux-riscv64-gnu': 4.38.0
-      '@rollup/rollup-linux-riscv64-musl': 4.38.0
-      '@rollup/rollup-linux-s390x-gnu': 4.38.0
-      '@rollup/rollup-linux-x64-gnu': 4.38.0
-      '@rollup/rollup-linux-x64-musl': 4.38.0
-      '@rollup/rollup-win32-arm64-msvc': 4.38.0
-      '@rollup/rollup-win32-ia32-msvc': 4.38.0
-      '@rollup/rollup-win32-x64-msvc': 4.38.0
+      '@rollup/rollup-android-arm-eabi': 4.39.0
+      '@rollup/rollup-android-arm64': 4.39.0
+      '@rollup/rollup-darwin-arm64': 4.39.0
+      '@rollup/rollup-darwin-x64': 4.39.0
+      '@rollup/rollup-freebsd-arm64': 4.39.0
+      '@rollup/rollup-freebsd-x64': 4.39.0
+      '@rollup/rollup-linux-arm-gnueabihf': 4.39.0
+      '@rollup/rollup-linux-arm-musleabihf': 4.39.0
+      '@rollup/rollup-linux-arm64-gnu': 4.39.0
+      '@rollup/rollup-linux-arm64-musl': 4.39.0
+      '@rollup/rollup-linux-loongarch64-gnu': 4.39.0
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0
+      '@rollup/rollup-linux-riscv64-gnu': 4.39.0
+      '@rollup/rollup-linux-riscv64-musl': 4.39.0
+      '@rollup/rollup-linux-s390x-gnu': 4.39.0
+      '@rollup/rollup-linux-x64-gnu': 4.39.0
+      '@rollup/rollup-linux-x64-musl': 4.39.0
+      '@rollup/rollup-win32-arm64-msvc': 4.39.0
+      '@rollup/rollup-win32-ia32-msvc': 4.39.0
+      '@rollup/rollup-win32-x64-msvc': 4.39.0
       fsevents: 2.3.3
 
   run-parallel@1.2.0:
@@ -7149,7 +7155,7 @@ snapshots:
       debug: 4.4.0
       es-module-lexer: 1.6.0
       pathe: 2.0.3
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
     transitivePeerDependencies:
       - '@types/node'
       - jiti
@@ -7164,12 +7170,12 @@ snapshots:
       - tsx
       - yaml
 
-  vite-plugin-compression@0.5.1(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)):
+  vite-plugin-compression@0.5.1(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)):
     dependencies:
       chalk: 4.1.2
       debug: 4.4.0
       fs-extra: 10.1.0
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
     transitivePeerDependencies:
       - supports-color
 
@@ -7177,22 +7183,22 @@ snapshots:
     dependencies:
       monaco-editor: 0.52.2
 
-  vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)):
+  vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)):
     dependencies:
       debug: 4.4.0
       globrex: 0.1.2
       tsconfck: 3.1.4(typescript@5.8.2)
     optionalDependencies:
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1):
+  vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1):
     dependencies:
       esbuild: 0.25.2
       postcss: 8.5.3
-      rollup: 4.38.0
+      rollup: 4.39.0
     optionalDependencies:
       '@types/node': 22.13.14
       fsevents: 2.3.3
@@ -7203,7 +7209,7 @@ snapshots:
   vitest@3.0.9(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1):
     dependencies:
       '@vitest/expect': 3.0.9
-      '@vitest/mocker': 3.0.9(vite@6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
+      '@vitest/mocker': 3.0.9(vite@6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1))
       '@vitest/pretty-format': 3.0.9
       '@vitest/runner': 3.0.9
       '@vitest/snapshot': 3.0.9
@@ -7219,7 +7225,7 @@ snapshots:
       tinyexec: 0.3.2
       tinypool: 1.0.2
       tinyrainbow: 2.0.0
-      vite: 6.2.4(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
+      vite: 6.2.5(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
       vite-node: 3.0.9(@types/node@22.13.14)(jiti@1.21.7)(less@4.2.2)(yaml@2.7.1)
       why-is-node-running: 2.3.0
     optionalDependencies:
diff --git a/ui/src/extensions/extensions-context.ts b/ui/src/extensions/extensions-context.ts
new file mode 100644
index 00000000..c408ebef
--- /dev/null
+++ b/ui/src/extensions/extensions-context.ts
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { Extension } from './types';
+
+type ExtensionsContextType = {
+  extensions: Extension[];
+};
+
+export const ExtensionsContext = React.createContext<ExtensionsContextType | null>(null);
+
+export const useExtensionsContext = () => {
+  const ctx = React.useContext(ExtensionsContext);
+
+  return {
+    stageTabs: ctx?.extensions.filter((extension) => extension.type === 'stageTab') || []
+  };
+};
diff --git a/ui/src/extensions/types.ts b/ui/src/extensions/types.ts
new file mode 100644
index 00000000..3a5e9d1a
--- /dev/null
+++ b/ui/src/extensions/types.ts
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { Stage } from '@ui/gen/api/v1alpha1/generated_pb';
+
+export type ExtensionStageTabComponentProps = {
+  stage: Stage;
+};
+
+export type ExtensionStageTab = {
+  type: 'stageTab';
+  component: ({ stage }: ExtensionStageTabComponentProps) => React.ReactNode;
+  label: string;
+  icon?: React.ReactNode;
+};
+
+export type Extension = ExtensionStageTab;
diff --git a/ui/src/features/stage/stage-details.tsx b/ui/src/features/stage/stage-details.tsx
index 893fbea5..f59e60d7 100644
--- a/ui/src/features/stage/stage-details.tsx
+++ b/ui/src/features/stage/stage-details.tsx
@@ -13,6 +13,7 @@ import { useEffect, useMemo, useState } from 'react';
 import { generatePath, useNavigate, useParams } from 'react-router-dom';
 
 import { paths } from '@ui/config/paths';
+import { useExtensionsContext } from '@ui/extensions/extensions-context';
 import { Description } from '@ui/features/common/description';
 import { HealthStatusIcon } from '@ui/features/common/health-status/health-status-icon';
 import { StagePhaseIcon } from '@ui/features/common/stage-phase/stage-phase-icon';
@@ -93,6 +94,8 @@ export const StageDetails = ({ stage }: { stage: Stage }) => {
   const shardKey = stage?.metadata?.labels['kargo.akuity.io/shard'] || '';
   const argocdShard = config?.argocdShards?.[shardKey];
 
+  const { stageTabs } = useExtensionsContext();
+
   return (
     <Drawer
       open={!!stageName}
@@ -180,6 +183,12 @@ export const StageDetails = ({ stage }: { stage: Stage }) => {
                     />
                   )
                 },
+                ...stageTabs.map((data, index) => ({
+                  children: <data.component stage={stage} />,
+                  key: String(data.label + index),
+                  label: data.label,
+                  icon: data.icon
+                })),
                 {
                   key: TabsTypes.SETTINGS,
                   label: 'Settings',
diff --git a/ui/src/gen/api/v1alpha1/generated_pb.ts b/ui/src/gen/api/v1alpha1/generated_pb.ts
index 5a86e428..adebccb7 100644
--- a/ui/src/gen/api/v1alpha1/generated_pb.ts
+++ b/ui/src/gen/api/v1alpha1/generated_pb.ts
@@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf";
  * Describes the file api/v1alpha1/generated.proto.
  */
 export const file_api_v1alpha1_generated: GenFile = /*@__PURE__*/
-  fileDesc("ChxhcGkvdjFhbHBoYTEvZ2VuZXJhdGVkLnByb3RvEiRnaXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEiMgoTQW5hbHlzaXNSdW5Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIrACChNBbmFseXNpc1J1bk1ldGFkYXRhElUKBmxhYmVscxgBIAMoCzJFLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhLkxhYmVsc0VudHJ5El8KC2Fubm90YXRpb25zGAIgAygLMkouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGEuQW5ub3RhdGlvbnNFbnRyeRotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJGChRBbmFseXNpc1J1blJlZmVyZW5jZRIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRINCgVwaGFzZRgDIAEoCSI3ChlBbmFseXNpc1RlbXBsYXRlUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSJPCg1BcHByb3ZlZFN0YWdlEj4KCmFwcHJvdmVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSI4ChVBcmdvQ0RBcHBIZWFsdGhTdGF0dXMSDgoGc3RhdHVzGAEgASgJEg8KB21lc3NhZ2UYAiABKAki1AEKD0FyZ29DREFwcFN0YXR1cxIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRJRCgxoZWFsdGhTdGF0dXMYAyABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwSGVhbHRoU3RhdHVzEk0KCnN5bmNTdGF0dXMYBCABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwU3luY1N0YXR1cyJKChNBcmdvQ0RBcHBTeW5jU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIQCghyZXZpc2lvbhgCIAEoCRIRCglyZXZpc2lvbnMYAyADKAkiNwoFQ2hhcnQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiYQoUQ2hhcnREaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSEAoIdmVyc2lvbnMYBCADKAkiZAoRQ2hhcnRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSFgoOZGlzY292ZXJ5TGltaXQYBCABKAUioQEKFENsdXN0ZXJQcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKnAQoYQ2x1c3RlclByb21vdGlvblRhc2tMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEkkKBWl0ZW1zGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNsdXN0ZXJQcm9tb3Rpb25UYXNrIkkKDEN1cnJlbnRTdGFnZRI5CgVzaW5jZRgBIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIrYCChNEaXNjb3ZlcmVkQXJ0aWZhY3RzEkAKDGRpc2NvdmVyZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEkUKA2dpdBgBIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXREaXNjb3ZlcnlSZXN1bHQSSgoGaW1hZ2VzGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlRGlzY292ZXJ5UmVzdWx0EkoKBmNoYXJ0cxgDIAMoCzI6LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydERpc2NvdmVyeVJlc3VsdCKwAQoQRGlzY292ZXJlZENvbW1pdBIKCgJpZBgBIAEoCRIOCgZicmFuY2gYAiABKAkSCwoDdGFnGAMgASgJEg8KB3N1YmplY3QYBCABKAkSDgoGYXV0aG9yGAUgASgJEhEKCWNvbW1pdHRlchgGIAEoCRI/CgtjcmVhdG9yRGF0ZRgHIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIqQCChhEaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2USCwoDdGFnGAEgASgJEg4KBmRpZ2VzdBgCIAEoCRJkCgthbm5vdGF0aW9ucxgFIAMoCzJPLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2UuQW5ub3RhdGlvbnNFbnRyeRISCgpnaXRSZXBvVVJMGAMgASgJEj0KCWNyZWF0ZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKiAwoHRnJlaWdodBJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEg0KBWFsaWFzGAcgASgJEkMKBm9yaWdpbhgJIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkAKB2NvbW1pdHMYAyADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0Q29tbWl0EjsKBmltYWdlcxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZRI7CgZjaGFydHMYBSADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQ2hhcnQSQwoGc3RhdHVzGAYgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMirQIKEUZyZWlnaHRDb2xsZWN0aW9uEgoKAmlkGAMgASgJElEKBWl0ZW1zGAEgAygLMkIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uLkl0ZW1zRW50cnkSUwoTdmVyaWZpY2F0aW9uSGlzdG9yeRgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5WZXJpZmljYXRpb25JbmZvGmQKCkl0ZW1zRW50cnkSCwoDa2V5GAEgASgJEkUKBXZhbHVlGAIgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2U6AjgBIo0BCgtGcmVpZ2h0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0IisKDUZyZWlnaHRPcmlnaW4SDAoEa2luZBgBIAEoCRIMCgRuYW1lGAIgASgJIqECChBGcmVpZ2h0UmVmZXJlbmNlEgwKBG5hbWUYASABKAkSQwoGb3JpZ2luGAggASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SQAoHY29tbWl0cxgCIAMoCzIvLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXRDb21taXQSOwoGaW1hZ2VzGAMgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlEjsKBmNoYXJ0cxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydCKcAQoORnJlaWdodFJlcXVlc3QSQwoGb3JpZ2luGAEgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SRQoHc291cmNlcxgCIAEoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0U291cmNlcyKYAQoORnJlaWdodFNvdXJjZXMSDgoGZGlyZWN0GAEgASgIEg4KBnN0YWdlcxgCIAMoCRJIChByZXF1aXJlZFNvYWtUaW1lGAMgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhwKFGF2YWlsYWJpbGl0eVN0cmF0ZWd5GAQgASgJItcECg1GcmVpZ2h0U3RhdHVzElkKC2N1cnJlbnRseUluGAMgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQ3VycmVudGx5SW5FbnRyeRJXCgp2ZXJpZmllZEluGAEgAygLMkMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuVmVyaWZpZWRJbkVudHJ5ElkKC2FwcHJvdmVkRm9yGAIgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQXBwcm92ZWRGb3JFbnRyeRpmChBDdXJyZW50bHlJbkVudHJ5EgsKA2tleRgBIAEoCRJBCgV2YWx1ZRgCIAEoCzIyLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DdXJyZW50U3RhZ2U6AjgBGmYKD1ZlcmlmaWVkSW5FbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpZWRTdGFnZToCOAEaZwoQQXBwcm92ZWRGb3JFbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXBwcm92ZWRTdGFnZToCOAEieQoJR2l0Q29tbWl0Eg8KB3JlcG9VUkwYASABKAkSCgoCaWQYAiABKAkSDgoGYnJhbmNoGAMgASgJEgsKA3RhZxgEIAEoCRIPCgdtZXNzYWdlGAYgASgJEg4KBmF1dGhvchgHIAEoCRIRCgljb21taXR0ZXIYCCABKAkibgoSR2l0RGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSRwoHY29tbWl0cxgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQ29tbWl0Io4CCg9HaXRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIfChdjb21taXRTZWxlY3Rpb25TdHJhdGVneRgCIAEoCRIOCgZicmFuY2gYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgLIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgHIAEoCBIUCgxpbmNsdWRlUGF0aHMYCCADKAkSFAoMZXhjbHVkZVBhdGhzGAkgAygJEhYKDmRpc2NvdmVyeUxpbWl0GAogASgFIsgBCgZIZWFsdGgSDgoGc3RhdHVzGAEgASgJEg4KBmlzc3VlcxgCIAMoCRJOCgZjb25maWcYBCABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OEk4KBm91dHB1dBgFIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibwoPSGVhbHRoQ2hlY2tTdGVwEgwKBHVzZXMYASABKAkSTgoGY29uZmlnGAIgASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiLQAQoFSW1hZ2USDwoHcmVwb1VSTBgBIAEoCRISCgpnaXRSZXBvVVJMGAIgASgJEgsKA3RhZxgDIAEoCRIOCgZkaWdlc3QYBCABKAkSUQoLYW5ub3RhdGlvbnMYBSADKAsyPC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2UuQW5ub3RhdGlvbnNFbnRyeRoyChBBbm5vdGF0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijQEKFEltYWdlRGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSEAoIcGxhdGZvcm0YAiABKAkSUgoKcmVmZXJlbmNlcxgDIAMoCzI+LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2Ui+QEKEUltYWdlU3Vic2NyaXB0aW9uEg8KB3JlcG9VUkwYASABKAkSEgoKZ2l0UmVwb1VSTBgCIAEoCRIeChZpbWFnZVNlbGVjdGlvblN0cmF0ZWd5GAMgASgJEhUKDXN0cmljdFNlbXZlcnMYCiABKAgSGAoQc2VtdmVyQ29uc3RyYWludBgEIAEoCRIRCglhbGxvd1RhZ3MYBSABKAkSEgoKaWdub3JlVGFncxgGIAMoCRIQCghwbGF0Zm9ybRgHIAEoCRIdChVpbnNlY3VyZVNraXBUTFNWZXJpZnkYCCABKAgSFgoOZGlzY292ZXJ5TGltaXQYCSABKAUi0wEKB1Byb2plY3QSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI/CgRzcGVjGAIgASgLMjEuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RTcGVjEkMKBnN0YXR1cxgDIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0U3RhdHVzIo0BCgtQcm9qZWN0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0Il8KC1Byb2plY3RTcGVjElAKEXByb21vdGlvblBvbGljaWVzGAEgAygLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblBvbGljeSJ0Cg1Qcm9qZWN0U3RhdHVzEkMKCmNvbmRpdGlvbnMYAyADKAsyLy5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuQ29uZGl0aW9uEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAki2QEKCVByb21vdGlvbhJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkEKBHNwZWMYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3BlYxJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzIpEBCg1Qcm9tb3Rpb25MaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEj4KBWl0ZW1zGAIgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvbiI+Cg9Qcm9tb3Rpb25Qb2xpY3kSDQoFc3RhZ2UYASABKAkSHAoUYXV0b1Byb21vdGlvbkVuYWJsZWQYAiABKAgi8gEKElByb21vdGlvblJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEkcKB2ZyZWlnaHQYAiABKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlZmVyZW5jZRJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzEj4KCmZpbmlzaGVkQXQYBCABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSK6AQoNUHJvbW90aW9uU3BlYxINCgVzdGFnZRgBIAEoCRIPCgdmcmVpZ2h0GAIgASgJEkUKBHZhcnMYBCADKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVmFyaWFibGUSQgoFc3RlcHMYAyADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcCK3BAoPUHJvbW90aW9uU3RhdHVzEhoKEmxhc3RIYW5kbGVkUmVmcmVzaBgEIAEoCRINCgVwaGFzZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEkcKB2ZyZWlnaHQYBSABKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlZmVyZW5jZRJSChFmcmVpZ2h0Q29sbGVjdGlvbhgHIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0Q29sbGVjdGlvbhJLCgxoZWFsdGhDaGVja3MYCCADKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSGVhbHRoQ2hlY2tTdGVwEj4KCmZpbmlzaGVkQXQYBiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRITCgtjdXJyZW50U3RlcBgJIAEoAxJaChVzdGVwRXhlY3V0aW9uTWV0YWRhdGEYCyADKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RlcEV4ZWN1dGlvbk1ldGFkYXRhEk0KBXN0YXRlGAogASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiLhAgoNUHJvbW90aW9uU3RlcBIMCgR1c2VzGAEgASgJEkoKBHRhc2sYBSABKAsyPC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGFza1JlZmVyZW5jZRIKCgJhcxgCIAEoCRIKCgJpZhgHIAEoCRJHCgVyZXRyeRgEIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwUmV0cnkSRQoEdmFycxgGIAMoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25WYXJpYWJsZRJOCgZjb25maWcYAyABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OIm0KElByb21vdGlvblN0ZXBSZXRyeRI/Cgd0aW1lb3V0GAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhYKDmVycm9yVGhyZXNob2xkGAIgASgNIpoBCg1Qcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKZAQoRUHJvbW90aW9uVGFza0xpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESQgoFaXRlbXMYAiADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGFzayI0ChZQcm9tb3Rpb25UYXNrUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSKeAQoRUHJvbW90aW9uVGFza1NwZWMSRQoEdmFycxgBIAMoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25WYXJpYWJsZRJCCgVzdGVwcxgCIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIl4KEVByb21vdGlvblRlbXBsYXRlEkkKBHNwZWMYASABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGVTcGVjIqIBChVQcm9tb3Rpb25UZW1wbGF0ZVNwZWMSRQoEdmFycxgCIAMoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25WYXJpYWJsZRJCCgVzdGVwcxgBIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIjAKEVByb21vdGlvblZhcmlhYmxlEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAki5gEKEFJlcG9TdWJzY3JpcHRpb24SQgoDZ2l0GAEgASgLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkdpdFN1YnNjcmlwdGlvbhJGCgVpbWFnZRgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZVN1YnNjcmlwdGlvbhJGCgVjaGFydBgDIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydFN1YnNjcmlwdGlvbiLNAQoFU3RhZ2USQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI9CgRzcGVjGAIgASgLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlU3BlYxJBCgZzdGF0dXMYAyABKAsyMS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RhZ2VTdGF0dXMiiQEKCVN0YWdlTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI6CgVpdGVtcxgCIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGFnZSKIAgoJU3RhZ2VTcGVjEg0KBXNoYXJkGAQgASgJEk4KEHJlcXVlc3RlZEZyZWlnaHQYBSADKAsyNC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlcXVlc3QSUgoRcHJvbW90aW9uVGVtcGxhdGUYBiABKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGUSSAoMdmVyaWZpY2F0aW9uGAMgASgLMjIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlZlcmlmaWNhdGlvbiL2AwoLU3RhZ2VTdGF0dXMSQwoKY29uZGl0aW9ucxgNIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAsgASgJEg0KBXBoYXNlGAEgASgJEk8KDmZyZWlnaHRIaXN0b3J5GAQgAygLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uEhYKDmZyZWlnaHRTdW1tYXJ5GAwgASgJEjwKBmhlYWx0aBgIIAEoCzIsLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5IZWFsdGgSDwoHbWVzc2FnZRgJIAEoCRIaChJvYnNlcnZlZEdlbmVyYXRpb24YBiABKAMSUgoQY3VycmVudFByb21vdGlvbhgHIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2USTwoNbGFzdFByb21vdGlvbhgKIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2Ui2gEKFVN0ZXBFeGVjdXRpb25NZXRhZGF0YRINCgVhbGlhcxgBIAEoCRI9CglzdGFydGVkQXQYAiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRI+CgpmaW5pc2hlZEF0GAMgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSEgoKZXJyb3JDb3VudBgEIAEoDRIOCgZzdGF0dXMYBSABKAkSDwoHbWVzc2FnZRgGIAEoCSKLAgoMVmVyaWZpY2F0aW9uEloKEWFuYWx5c2lzVGVtcGxhdGVzGAEgAygLMj8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzVGVtcGxhdGVSZWZlcmVuY2USVgoTYW5hbHlzaXNSdW5NZXRhZGF0YRgCIAEoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhEkcKBGFyZ3MYAyADKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5Bcmd1bWVudCKdAgoQVmVyaWZpY2F0aW9uSW5mbxIKCgJpZBgEIAEoCRINCgVhY3RvchgHIAEoCRI9CglzdGFydFRpbWUYBSABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRINCgVwaGFzZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEk8KC2FuYWx5c2lzUnVuGAMgASgLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuUmVmZXJlbmNlEj4KCmZpbmlzaFRpbWUYBiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSKUAQoNVmVyaWZpZWRTdGFnZRI+Cgp2ZXJpZmllZEF0GAEgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSQwoLbG9uZ2VzdFNvYWsYAiABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuRHVyYXRpb24i2QEKCVdhcmVob3VzZRJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkEKBHNwZWMYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlU3BlYxJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlU3RhdHVzIpEBCg1XYXJlaG91c2VMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEj4KBWl0ZW1zGAIgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLldhcmVob3VzZSLOAQoNV2FyZWhvdXNlU3BlYxINCgVzaGFyZBgCIAEoCRJACghpbnRlcnZhbBgEIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5EdXJhdGlvbhIdChVmcmVpZ2h0Q3JlYXRpb25Qb2xpY3kYAyABKAkSTQoNc3Vic2NyaXB0aW9ucxgBIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5SZXBvU3Vic2NyaXB0aW9uIv0BCg9XYXJlaG91c2VTdGF0dXMSQwoKY29uZGl0aW9ucxgJIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAYgASgJEhoKEm9ic2VydmVkR2VuZXJhdGlvbhgEIAEoAxIVCg1sYXN0RnJlaWdodElEGAggASgJElYKE2Rpc2NvdmVyZWRBcnRpZmFjdHMYByABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRGlzY292ZXJlZEFydGlmYWN0c0KXAgooY29tLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMUIOR2VuZXJhdGVkUHJvdG9QAVokZ2l0aHViLmNvbS9ha3VpdHkva2FyZ28vYXBpL3YxYWxwaGExogIFR0NBS0GqAiRHaXRodWIuQ29tLkFrdWl0eS5LYXJnby5BcGkuVjFhbHBoYTHKAiRHaXRodWJcQ29tXEFrdWl0eVxLYXJnb1xBcGlcVjFhbHBoYTHiAjBHaXRodWJcQ29tXEFrdWl0eVxLYXJnb1xBcGlcVjFhbHBoYTFcR1BCTWV0YWRhdGHqAilHaXRodWI6OkNvbTo6QWt1aXR5OjpLYXJnbzo6QXBpOjpWMWFscGhhMQ", [file_k8s_io_apiextensions_apiserver_pkg_apis_apiextensions_v1_generated, file_k8s_io_apimachinery_pkg_apis_meta_v1_generated, file_k8s_io_apimachinery_pkg_runtime_generated, file_k8s_io_apimachinery_pkg_runtime_schema_generated]);
+  fileDesc("ChxhcGkvdjFhbHBoYTEvZ2VuZXJhdGVkLnByb3RvEiRnaXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEiMgoTQW5hbHlzaXNSdW5Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIrACChNBbmFseXNpc1J1bk1ldGFkYXRhElUKBmxhYmVscxgBIAMoCzJFLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhLkxhYmVsc0VudHJ5El8KC2Fubm90YXRpb25zGAIgAygLMkouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGEuQW5ub3RhdGlvbnNFbnRyeRotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJGChRBbmFseXNpc1J1blJlZmVyZW5jZRIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRINCgVwaGFzZRgDIAEoCSI3ChlBbmFseXNpc1RlbXBsYXRlUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSJPCg1BcHByb3ZlZFN0YWdlEj4KCmFwcHJvdmVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSI4ChVBcmdvQ0RBcHBIZWFsdGhTdGF0dXMSDgoGc3RhdHVzGAEgASgJEg8KB21lc3NhZ2UYAiABKAki1AEKD0FyZ29DREFwcFN0YXR1cxIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRJRCgxoZWFsdGhTdGF0dXMYAyABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwSGVhbHRoU3RhdHVzEk0KCnN5bmNTdGF0dXMYBCABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwU3luY1N0YXR1cyJKChNBcmdvQ0RBcHBTeW5jU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIQCghyZXZpc2lvbhgCIAEoCRIRCglyZXZpc2lvbnMYAyADKAkiNwoFQ2hhcnQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiYQoUQ2hhcnREaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSEAoIdmVyc2lvbnMYBCADKAkiZAoRQ2hhcnRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSFgoOZGlzY292ZXJ5TGltaXQYBCABKAUioQEKFENsdXN0ZXJQcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKnAQoYQ2x1c3RlclByb21vdGlvblRhc2tMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEkkKBWl0ZW1zGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNsdXN0ZXJQcm9tb3Rpb25UYXNrIkkKDEN1cnJlbnRTdGFnZRI5CgVzaW5jZRgBIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIrYCChNEaXNjb3ZlcmVkQXJ0aWZhY3RzEkAKDGRpc2NvdmVyZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEkUKA2dpdBgBIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXREaXNjb3ZlcnlSZXN1bHQSSgoGaW1hZ2VzGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlRGlzY292ZXJ5UmVzdWx0EkoKBmNoYXJ0cxgDIAMoCzI6LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydERpc2NvdmVyeVJlc3VsdCKwAQoQRGlzY292ZXJlZENvbW1pdBIKCgJpZBgBIAEoCRIOCgZicmFuY2gYAiABKAkSCwoDdGFnGAMgASgJEg8KB3N1YmplY3QYBCABKAkSDgoGYXV0aG9yGAUgASgJEhEKCWNvbW1pdHRlchgGIAEoCRI/CgtjcmVhdG9yRGF0ZRgHIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIqQCChhEaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2USCwoDdGFnGAEgASgJEg4KBmRpZ2VzdBgCIAEoCRJkCgthbm5vdGF0aW9ucxgFIAMoCzJPLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2UuQW5ub3RhdGlvbnNFbnRyeRISCgpnaXRSZXBvVVJMGAMgASgJEj0KCWNyZWF0ZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChJFeHByZXNzaW9uVmFyaWFibGUSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSKiAwoHRnJlaWdodBJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEg0KBWFsaWFzGAcgASgJEkMKBm9yaWdpbhgJIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkAKB2NvbW1pdHMYAyADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0Q29tbWl0EjsKBmltYWdlcxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZRI7CgZjaGFydHMYBSADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQ2hhcnQSQwoGc3RhdHVzGAYgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMirQIKEUZyZWlnaHRDb2xsZWN0aW9uEgoKAmlkGAMgASgJElEKBWl0ZW1zGAEgAygLMkIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uLkl0ZW1zRW50cnkSUwoTdmVyaWZpY2F0aW9uSGlzdG9yeRgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5WZXJpZmljYXRpb25JbmZvGmQKCkl0ZW1zRW50cnkSCwoDa2V5GAEgASgJEkUKBXZhbHVlGAIgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2U6AjgBIo0BCgtGcmVpZ2h0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0IisKDUZyZWlnaHRPcmlnaW4SDAoEa2luZBgBIAEoCRIMCgRuYW1lGAIgASgJIqECChBGcmVpZ2h0UmVmZXJlbmNlEgwKBG5hbWUYASABKAkSQwoGb3JpZ2luGAggASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SQAoHY29tbWl0cxgCIAMoCzIvLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXRDb21taXQSOwoGaW1hZ2VzGAMgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlEjsKBmNoYXJ0cxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydCKcAQoORnJlaWdodFJlcXVlc3QSQwoGb3JpZ2luGAEgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SRQoHc291cmNlcxgCIAEoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0U291cmNlcyKYAQoORnJlaWdodFNvdXJjZXMSDgoGZGlyZWN0GAEgASgIEg4KBnN0YWdlcxgCIAMoCRJIChByZXF1aXJlZFNvYWtUaW1lGAMgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhwKFGF2YWlsYWJpbGl0eVN0cmF0ZWd5GAQgASgJItcECg1GcmVpZ2h0U3RhdHVzElkKC2N1cnJlbnRseUluGAMgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQ3VycmVudGx5SW5FbnRyeRJXCgp2ZXJpZmllZEluGAEgAygLMkMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuVmVyaWZpZWRJbkVudHJ5ElkKC2FwcHJvdmVkRm9yGAIgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQXBwcm92ZWRGb3JFbnRyeRpmChBDdXJyZW50bHlJbkVudHJ5EgsKA2tleRgBIAEoCRJBCgV2YWx1ZRgCIAEoCzIyLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DdXJyZW50U3RhZ2U6AjgBGmYKD1ZlcmlmaWVkSW5FbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpZWRTdGFnZToCOAEaZwoQQXBwcm92ZWRGb3JFbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXBwcm92ZWRTdGFnZToCOAEieQoJR2l0Q29tbWl0Eg8KB3JlcG9VUkwYASABKAkSCgoCaWQYAiABKAkSDgoGYnJhbmNoGAMgASgJEgsKA3RhZxgEIAEoCRIPCgdtZXNzYWdlGAYgASgJEg4KBmF1dGhvchgHIAEoCRIRCgljb21taXR0ZXIYCCABKAkibgoSR2l0RGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSRwoHY29tbWl0cxgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQ29tbWl0Io4CCg9HaXRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIfChdjb21taXRTZWxlY3Rpb25TdHJhdGVneRgCIAEoCRIOCgZicmFuY2gYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgLIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgHIAEoCBIUCgxpbmNsdWRlUGF0aHMYCCADKAkSFAoMZXhjbHVkZVBhdGhzGAkgAygJEhYKDmRpc2NvdmVyeUxpbWl0GAogASgFIsgBCgZIZWFsdGgSDgoGc3RhdHVzGAEgASgJEg4KBmlzc3VlcxgCIAMoCRJOCgZjb25maWcYBCABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OEk4KBm91dHB1dBgFIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibwoPSGVhbHRoQ2hlY2tTdGVwEgwKBHVzZXMYASABKAkSTgoGY29uZmlnGAIgASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiLQAQoFSW1hZ2USDwoHcmVwb1VSTBgBIAEoCRISCgpnaXRSZXBvVVJMGAIgASgJEgsKA3RhZxgDIAEoCRIOCgZkaWdlc3QYBCABKAkSUQoLYW5ub3RhdGlvbnMYBSADKAsyPC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2UuQW5ub3RhdGlvbnNFbnRyeRoyChBBbm5vdGF0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijQEKFEltYWdlRGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSEAoIcGxhdGZvcm0YAiABKAkSUgoKcmVmZXJlbmNlcxgDIAMoCzI+LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2Ui+QEKEUltYWdlU3Vic2NyaXB0aW9uEg8KB3JlcG9VUkwYASABKAkSEgoKZ2l0UmVwb1VSTBgCIAEoCRIeChZpbWFnZVNlbGVjdGlvblN0cmF0ZWd5GAMgASgJEhUKDXN0cmljdFNlbXZlcnMYCiABKAgSGAoQc2VtdmVyQ29uc3RyYWludBgEIAEoCRIRCglhbGxvd1RhZ3MYBSABKAkSEgoKaWdub3JlVGFncxgGIAMoCRIQCghwbGF0Zm9ybRgHIAEoCRIdChVpbnNlY3VyZVNraXBUTFNWZXJpZnkYCCABKAgSFgoOZGlzY292ZXJ5TGltaXQYCSABKAUi0wEKB1Byb2plY3QSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI/CgRzcGVjGAIgASgLMjEuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RTcGVjEkMKBnN0YXR1cxgDIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0U3RhdHVzIo0BCgtQcm9qZWN0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0Il8KC1Byb2plY3RTcGVjElAKEXByb21vdGlvblBvbGljaWVzGAEgAygLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblBvbGljeSJ0Cg1Qcm9qZWN0U3RhdHVzEkMKCmNvbmRpdGlvbnMYAyADKAsyLy5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuQ29uZGl0aW9uEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAki2QEKCVByb21vdGlvbhJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkEKBHNwZWMYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3BlYxJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzIpEBCg1Qcm9tb3Rpb25MaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEj4KBWl0ZW1zGAIgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvbiI+Cg9Qcm9tb3Rpb25Qb2xpY3kSDQoFc3RhZ2UYASABKAkSHAoUYXV0b1Byb21vdGlvbkVuYWJsZWQYAiABKAgi8gEKElByb21vdGlvblJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEkcKB2ZyZWlnaHQYAiABKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlZmVyZW5jZRJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzEj4KCmZpbmlzaGVkQXQYBCABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSK7AQoNUHJvbW90aW9uU3BlYxINCgVzdGFnZRgBIAEoCRIPCgdmcmVpZ2h0GAIgASgJEkYKBHZhcnMYBCADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEkIKBXN0ZXBzGAMgAygLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0ZXAitwQKD1Byb21vdGlvblN0YXR1cxIaChJsYXN0SGFuZGxlZFJlZnJlc2gYBCABKAkSDQoFcGhhc2UYASABKAkSDwoHbWVzc2FnZRgCIAEoCRJHCgdmcmVpZ2h0GAUgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2USUgoRZnJlaWdodENvbGxlY3Rpb24YByABKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodENvbGxlY3Rpb24SSwoMaGVhbHRoQ2hlY2tzGAggAygLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkhlYWx0aENoZWNrU3RlcBI+CgpmaW5pc2hlZEF0GAYgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSEwoLY3VycmVudFN0ZXAYCSABKAMSWgoVc3RlcEV4ZWN1dGlvbk1ldGFkYXRhGAsgAygLMjsuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0ZXBFeGVjdXRpb25NZXRhZGF0YRJNCgVzdGF0ZRgKIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04i4gIKDVByb21vdGlvblN0ZXASDAoEdXNlcxgBIAEoCRJKCgR0YXNrGAUgASgLMjwuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblRhc2tSZWZlcmVuY2USCgoCYXMYAiABKAkSCgoCaWYYByABKAkSRwoFcmV0cnkYBCABKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcFJldHJ5EkYKBHZhcnMYBiADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEk4KBmNvbmZpZxgDIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibQoSUHJvbW90aW9uU3RlcFJldHJ5Ej8KB3RpbWVvdXQYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuRHVyYXRpb24SFgoOZXJyb3JUaHJlc2hvbGQYAiABKA0imgEKDVByb21vdGlvblRhc2sSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRJFCgRzcGVjGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblRhc2tTcGVjIpkBChFQcm9tb3Rpb25UYXNrTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRJCCgVpdGVtcxgCIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrIjQKFlByb21vdGlvblRhc2tSZWZlcmVuY2USDAoEbmFtZRgBIAEoCRIMCgRraW5kGAIgASgJIp8BChFQcm9tb3Rpb25UYXNrU3BlYxJGCgR2YXJzGAEgAygLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkV4cHJlc3Npb25WYXJpYWJsZRJCCgVzdGVwcxgCIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIl4KEVByb21vdGlvblRlbXBsYXRlEkkKBHNwZWMYASABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGVTcGVjIqMBChVQcm9tb3Rpb25UZW1wbGF0ZVNwZWMSRgoEdmFycxgCIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5FeHByZXNzaW9uVmFyaWFibGUSQgoFc3RlcHMYASADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcCLmAQoQUmVwb1N1YnNjcmlwdGlvbhJCCgNnaXQYASABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0U3Vic2NyaXB0aW9uEkYKBWltYWdlGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlU3Vic2NyaXB0aW9uEkYKBWNoYXJ0GAMgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNoYXJ0U3Vic2NyaXB0aW9uIs0BCgVTdGFnZRJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEj0KBHNwZWMYAiABKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RhZ2VTcGVjEkEKBnN0YXR1cxgDIAEoCzIxLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGFnZVN0YXR1cyKJAQoJU3RhZ2VMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEjoKBWl0ZW1zGAIgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlItACCglTdGFnZVNwZWMSDQoFc2hhcmQYBCABKAkSRgoEdmFycxgHIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5FeHByZXNzaW9uVmFyaWFibGUSTgoQcmVxdWVzdGVkRnJlaWdodBgFIAMoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVxdWVzdBJSChFwcm9tb3Rpb25UZW1wbGF0ZRgGIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UZW1wbGF0ZRJICgx2ZXJpZmljYXRpb24YAyABKAsyMi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpY2F0aW9uIvYDCgtTdGFnZVN0YXR1cxJDCgpjb25kaXRpb25zGA0gAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhIaChJsYXN0SGFuZGxlZFJlZnJlc2gYCyABKAkSDQoFcGhhc2UYASABKAkSTwoOZnJlaWdodEhpc3RvcnkYBCADKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodENvbGxlY3Rpb24SFgoOZnJlaWdodFN1bW1hcnkYDCABKAkSPAoGaGVhbHRoGAggASgLMiwuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkhlYWx0aBIPCgdtZXNzYWdlGAkgASgJEhoKEm9ic2VydmVkR2VuZXJhdGlvbhgGIAEoAxJSChBjdXJyZW50UHJvbW90aW9uGAcgASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblJlZmVyZW5jZRJPCg1sYXN0UHJvbW90aW9uGAogASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblJlZmVyZW5jZSLaAQoVU3RlcEV4ZWN1dGlvbk1ldGFkYXRhEg0KBWFsaWFzGAEgASgJEj0KCXN0YXJ0ZWRBdBgCIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEj4KCmZpbmlzaGVkQXQYAyABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRISCgplcnJvckNvdW50GAQgASgNEg4KBnN0YXR1cxgFIAEoCRIPCgdtZXNzYWdlGAYgASgJIosCCgxWZXJpZmljYXRpb24SWgoRYW5hbHlzaXNUZW1wbGF0ZXMYASADKAsyPy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNUZW1wbGF0ZVJlZmVyZW5jZRJWChNhbmFseXNpc1J1bk1ldGFkYXRhGAIgASgLMjkuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGESRwoEYXJncxgDIAMoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bkFyZ3VtZW50Ip0CChBWZXJpZmljYXRpb25JbmZvEgoKAmlkGAQgASgJEg0KBWFjdG9yGAcgASgJEj0KCXN0YXJ0VGltZRgFIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSTwoLYW5hbHlzaXNSdW4YAyABKAsyOi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5SZWZlcmVuY2USPgoKZmluaXNoVGltZRgGIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIpQBCg1WZXJpZmllZFN0YWdlEj4KCnZlcmlmaWVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRJDCgtsb25nZXN0U29haxgCIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5EdXJhdGlvbiLZAQoJV2FyZWhvdXNlEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESQQoEc3BlYxgCIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTcGVjEkUKBnN0YXR1cxgDIAEoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTdGF0dXMikQEKDVdhcmVob3VzZUxpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESPgoFaXRlbXMYAiADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlIs4BCg1XYXJlaG91c2VTcGVjEg0KBXNoYXJkGAIgASgJEkAKCGludGVydmFsGAQgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEh0KFWZyZWlnaHRDcmVhdGlvblBvbGljeRgDIAEoCRJNCg1zdWJzY3JpcHRpb25zGAEgAygLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlJlcG9TdWJzY3JpcHRpb24i/QEKD1dhcmVob3VzZVN0YXR1cxJDCgpjb25kaXRpb25zGAkgAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhIaChJsYXN0SGFuZGxlZFJlZnJlc2gYBiABKAkSGgoSb2JzZXJ2ZWRHZW5lcmF0aW9uGAQgASgDEhUKDWxhc3RGcmVpZ2h0SUQYCCABKAkSVgoTZGlzY292ZXJlZEFydGlmYWN0cxgHIAEoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQXJ0aWZhY3RzQpcCCihjb20uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExQg5HZW5lcmF0ZWRQcm90b1ABWiRnaXRodWIuY29tL2FrdWl0eS9rYXJnby9hcGkvdjFhbHBoYTGiAgVHQ0FLQaoCJEdpdGh1Yi5Db20uQWt1aXR5LkthcmdvLkFwaS5WMWFscGhhMcoCJEdpdGh1YlxDb21cQWt1aXR5XEthcmdvXEFwaVxWMWFscGhhMeICMEdpdGh1YlxDb21cQWt1aXR5XEthcmdvXEFwaVxWMWFscGhhMVxHUEJNZXRhZGF0YeoCKUdpdGh1Yjo6Q29tOjpBa3VpdHk6OkthcmdvOjpBcGk6OlYxYWxwaGEx", [file_k8s_io_apiextensions_apiserver_pkg_apis_apiextensions_v1_generated, file_k8s_io_apimachinery_pkg_apis_meta_v1_generated, file_k8s_io_apimachinery_pkg_runtime_generated, file_k8s_io_apimachinery_pkg_runtime_schema_generated]);
 
 /**
  * AnalysisRunArgument represents an argument to be added to an AnalysisRun.
@@ -691,6 +691,44 @@ export type DiscoveredImageReference = Message<"github.com.akuity.kargo.api.v1al
 export const DiscoveredImageReferenceSchema: GenMessage<DiscoveredImageReference> = /*@__PURE__*/
   messageDesc(file_api_v1alpha1_generated, 16);
 
+/**
+ * ExpressionVariable describes a single variable that may be referenced by
+ * expressions in the context of a ClusterPromotionTask, PromotionTask,
+ * Promotion, AnalysisRun arguments, or other objects that support expressions.
+ *
+ * It is used to pass information to the expression evaluation engine, and to
+ * allow for dynamic evaluation of expressions based on the variable values.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.ExpressionVariable
+ */
+export type ExpressionVariable = Message<"github.com.akuity.kargo.api.v1alpha1.ExpressionVariable"> & {
+  /**
+   * Name is the name of the variable.
+   *
+   * +kubebuilder:validation:MinLength=1
+   * +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
+   *
+   * @generated from field: optional string name = 1;
+   */
+  name: string;
+
+  /**
+   * Value is the value of the variable. It is allowed to utilize expressions
+   * in the value.
+   * See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
+   *
+   * @generated from field: optional string value = 2;
+   */
+  value: string;
+};
+
+/**
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.ExpressionVariable.
+ * Use `create(ExpressionVariableSchema)` to create a new message.
+ */
+export const ExpressionVariableSchema: GenMessage<ExpressionVariable> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 17);
+
 /**
  * Freight represents a collection of versioned artifacts.
  *
@@ -760,7 +798,7 @@ export type Freight = Message<"github.com.akuity.kargo.api.v1alpha1.Freight"> &
  * Use `create(FreightSchema)` to create a new message.
  */
 export const FreightSchema: GenMessage<Freight> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 17);
+  messageDesc(file_api_v1alpha1_generated, 18);
 
 /**
  * FreightCollection is a collection of FreightReferences, each of which
@@ -800,7 +838,7 @@ export type FreightCollection = Message<"github.com.akuity.kargo.api.v1alpha1.Fr
  * Use `create(FreightCollectionSchema)` to create a new message.
  */
 export const FreightCollectionSchema: GenMessage<FreightCollection> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 18);
+  messageDesc(file_api_v1alpha1_generated, 19);
 
 /**
  * FreightList is a list of Freight resources.
@@ -824,7 +862,7 @@ export type FreightList = Message<"github.com.akuity.kargo.api.v1alpha1.FreightL
  * Use `create(FreightListSchema)` to create a new message.
  */
 export const FreightListSchema: GenMessage<FreightList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 19);
+  messageDesc(file_api_v1alpha1_generated, 20);
 
 /**
  * FreightOrigin describes a kind of Freight in terms of where it may have
@@ -861,7 +899,7 @@ export type FreightOrigin = Message<"github.com.akuity.kargo.api.v1alpha1.Freigh
  * Use `create(FreightOriginSchema)` to create a new message.
  */
 export const FreightOriginSchema: GenMessage<FreightOrigin> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 20);
+  messageDesc(file_api_v1alpha1_generated, 21);
 
 /**
  * FreightReference is a simplified representation of a piece of Freight -- not
@@ -913,7 +951,7 @@ export type FreightReference = Message<"github.com.akuity.kargo.api.v1alpha1.Fre
  * Use `create(FreightReferenceSchema)` to create a new message.
  */
 export const FreightReferenceSchema: GenMessage<FreightReference> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 21);
+  messageDesc(file_api_v1alpha1_generated, 22);
 
 /**
  * FreightRequest expresses a Stage's need for Freight having originated from a
@@ -946,7 +984,7 @@ export type FreightRequest = Message<"github.com.akuity.kargo.api.v1alpha1.Freig
  * Use `create(FreightRequestSchema)` to create a new message.
  */
 export const FreightRequestSchema: GenMessage<FreightRequest> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 22);
+  messageDesc(file_api_v1alpha1_generated, 23);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.FreightSources
@@ -1006,7 +1044,7 @@ export type FreightSources = Message<"github.com.akuity.kargo.api.v1alpha1.Freig
  * Use `create(FreightSourcesSchema)` to create a new message.
  */
 export const FreightSourcesSchema: GenMessage<FreightSources> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 23);
+  messageDesc(file_api_v1alpha1_generated, 24);
 
 /**
  * FreightStatus describes a piece of Freight's most recently observed state.
@@ -1045,7 +1083,7 @@ export type FreightStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Freigh
  * Use `create(FreightStatusSchema)` to create a new message.
  */
 export const FreightStatusSchema: GenMessage<FreightStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 24);
+  messageDesc(file_api_v1alpha1_generated, 25);
 
 /**
  * GitCommit describes a specific commit from a specific Git repository.
@@ -1111,7 +1149,7 @@ export type GitCommit = Message<"github.com.akuity.kargo.api.v1alpha1.GitCommit"
  * Use `create(GitCommitSchema)` to create a new message.
  */
 export const GitCommitSchema: GenMessage<GitCommit> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 25);
+  messageDesc(file_api_v1alpha1_generated, 26);
 
 /**
  * GitDiscoveryResult represents the result of a Git discovery operation for a
@@ -1147,7 +1185,7 @@ export type GitDiscoveryResult = Message<"github.com.akuity.kargo.api.v1alpha1.G
  * Use `create(GitDiscoveryResultSchema)` to create a new message.
  */
 export const GitDiscoveryResultSchema: GenMessage<GitDiscoveryResult> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 26);
+  messageDesc(file_api_v1alpha1_generated, 27);
 
 /**
  * GitSubscription defines a subscription to a Git repository.
@@ -1318,7 +1356,7 @@ export type GitSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.GitS
  * Use `create(GitSubscriptionSchema)` to create a new message.
  */
 export const GitSubscriptionSchema: GenMessage<GitSubscription> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 27);
+  messageDesc(file_api_v1alpha1_generated, 28);
 
 /**
  * Health describes the health of a Stage.
@@ -1362,7 +1400,7 @@ export type Health = Message<"github.com.akuity.kargo.api.v1alpha1.Health"> & {
  * Use `create(HealthSchema)` to create a new message.
  */
 export const HealthSchema: GenMessage<Health> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 28);
+  messageDesc(file_api_v1alpha1_generated, 29);
 
 /**
  * HealthCheckStep describes a health check directive which can be executed by
@@ -1393,7 +1431,7 @@ export type HealthCheckStep = Message<"github.com.akuity.kargo.api.v1alpha1.Heal
  * Use `create(HealthCheckStepSchema)` to create a new message.
  */
 export const HealthCheckStepSchema: GenMessage<HealthCheckStep> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 29);
+  messageDesc(file_api_v1alpha1_generated, 30);
 
 /**
  * Image describes a specific version of a container image.
@@ -1446,7 +1484,7 @@ export type Image = Message<"github.com.akuity.kargo.api.v1alpha1.Image"> & {
  * Use `create(ImageSchema)` to create a new message.
  */
 export const ImageSchema: GenMessage<Image> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 30);
+  messageDesc(file_api_v1alpha1_generated, 31);
 
 /**
  * ImageDiscoveryResult represents the result of an image discovery operation
@@ -1492,7 +1530,7 @@ export type ImageDiscoveryResult = Message<"github.com.akuity.kargo.api.v1alpha1
  * Use `create(ImageDiscoveryResultSchema)` to create a new message.
  */
 export const ImageDiscoveryResultSchema: GenMessage<ImageDiscoveryResult> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 31);
+  messageDesc(file_api_v1alpha1_generated, 32);
 
 /**
  * ImageSubscription defines a subscription to an image repository.
@@ -1639,7 +1677,7 @@ export type ImageSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.Im
  * Use `create(ImageSubscriptionSchema)` to create a new message.
  */
 export const ImageSubscriptionSchema: GenMessage<ImageSubscription> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 32);
+  messageDesc(file_api_v1alpha1_generated, 33);
 
 /**
  * Project is a resource type that reconciles to a specially labeled namespace
@@ -1673,7 +1711,7 @@ export type Project = Message<"github.com.akuity.kargo.api.v1alpha1.Project"> &
  * Use `create(ProjectSchema)` to create a new message.
  */
 export const ProjectSchema: GenMessage<Project> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 33);
+  messageDesc(file_api_v1alpha1_generated, 34);
 
 /**
  * ProjectList is a list of Project resources.
@@ -1697,7 +1735,7 @@ export type ProjectList = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectL
  * Use `create(ProjectListSchema)` to create a new message.
  */
 export const ProjectListSchema: GenMessage<ProjectList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 34);
+  messageDesc(file_api_v1alpha1_generated, 35);
 
 /**
  * ProjectSpec describes a Project.
@@ -1719,7 +1757,7 @@ export type ProjectSpec = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectS
  * Use `create(ProjectSpecSchema)` to create a new message.
  */
 export const ProjectSpecSchema: GenMessage<ProjectSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 35);
+  messageDesc(file_api_v1alpha1_generated, 36);
 
 /**
  * ProjectStatus describes a Project's current status.
@@ -1765,7 +1803,7 @@ export type ProjectStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Projec
  * Use `create(ProjectStatusSchema)` to create a new message.
  */
 export const ProjectStatusSchema: GenMessage<ProjectStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 36);
+  messageDesc(file_api_v1alpha1_generated, 37);
 
 /**
  * Promotion represents a request to transition a particular Stage into a
@@ -1803,7 +1841,7 @@ export type Promotion = Message<"github.com.akuity.kargo.api.v1alpha1.Promotion"
  * Use `create(PromotionSchema)` to create a new message.
  */
 export const PromotionSchema: GenMessage<Promotion> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 37);
+  messageDesc(file_api_v1alpha1_generated, 38);
 
 /**
  * PromotionList contains a list of Promotion
@@ -1827,7 +1865,7 @@ export type PromotionList = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionListSchema)` to create a new message.
  */
 export const PromotionListSchema: GenMessage<PromotionList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 38);
+  messageDesc(file_api_v1alpha1_generated, 39);
 
 /**
  * PromotionPolicy defines policies governing the promotion of Freight to a
@@ -1863,7 +1901,7 @@ export type PromotionPolicy = Message<"github.com.akuity.kargo.api.v1alpha1.Prom
  * Use `create(PromotionPolicySchema)` to create a new message.
  */
 export const PromotionPolicySchema: GenMessage<PromotionPolicy> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 39);
+  messageDesc(file_api_v1alpha1_generated, 40);
 
 /**
  * PromotionReference contains the relevant information about a Promotion
@@ -1906,7 +1944,7 @@ export type PromotionReference = Message<"github.com.akuity.kargo.api.v1alpha1.P
  * Use `create(PromotionReferenceSchema)` to create a new message.
  */
 export const PromotionReferenceSchema: GenMessage<PromotionReference> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 40);
+  messageDesc(file_api_v1alpha1_generated, 41);
 
 /**
  * PromotionSpec describes the desired transition of a specific Stage into a
@@ -1946,9 +1984,9 @@ export type PromotionSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
    * Vars is a list of variables that can be referenced by expressions in
    * promotion steps.
    *
-   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.PromotionVariable vars = 4;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 4;
    */
-  vars: PromotionVariable[];
+  vars: ExpressionVariable[];
 
   /**
    * Steps specifies the directives to be executed as part of this Promotion.
@@ -1969,7 +2007,7 @@ export type PromotionSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionSpecSchema)` to create a new message.
  */
 export const PromotionSpecSchema: GenMessage<PromotionSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 41);
+  messageDesc(file_api_v1alpha1_generated, 42);
 
 /**
  * PromotionStatus describes the current state of the transition represented by
@@ -2067,7 +2105,7 @@ export type PromotionStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Prom
  * Use `create(PromotionStatusSchema)` to create a new message.
  */
 export const PromotionStatusSchema: GenMessage<PromotionStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 42);
+  messageDesc(file_api_v1alpha1_generated, 43);
 
 /**
  * PromotionStep describes a directive to be executed as part of a Promotion.
@@ -2122,9 +2160,9 @@ export type PromotionStep = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
    * the step's Config. The values override the values specified in the
    * PromotionSpec.
    *
-   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.PromotionVariable vars = 6;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 6;
    */
-  vars: PromotionVariable[];
+  vars: ExpressionVariable[];
 
   /**
    * Config is opaque configuration for the PromotionStep that is understood
@@ -2142,7 +2180,7 @@ export type PromotionStep = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionStepSchema)` to create a new message.
  */
 export const PromotionStepSchema: GenMessage<PromotionStep> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 43);
+  messageDesc(file_api_v1alpha1_generated, 44);
 
 /**
  * PromotionStepRetry describes the retry policy for a PromotionStep.
@@ -2201,7 +2239,7 @@ export type PromotionStepRetry = Message<"github.com.akuity.kargo.api.v1alpha1.P
  * Use `create(PromotionStepRetrySchema)` to create a new message.
  */
 export const PromotionStepRetrySchema: GenMessage<PromotionStepRetry> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 44);
+  messageDesc(file_api_v1alpha1_generated, 45);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionTask
@@ -2228,7 +2266,7 @@ export type PromotionTask = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionTaskSchema)` to create a new message.
  */
 export const PromotionTaskSchema: GenMessage<PromotionTask> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 45);
+  messageDesc(file_api_v1alpha1_generated, 46);
 
 /**
  * PromotionTaskList contains a list of PromotionTasks.
@@ -2252,7 +2290,7 @@ export type PromotionTaskList = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(PromotionTaskListSchema)` to create a new message.
  */
 export const PromotionTaskListSchema: GenMessage<PromotionTaskList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 46);
+  messageDesc(file_api_v1alpha1_generated, 47);
 
 /**
  * PromotionTaskReference describes a reference to a PromotionTask.
@@ -2289,7 +2327,7 @@ export type PromotionTaskReference = Message<"github.com.akuity.kargo.api.v1alph
  * Use `create(PromotionTaskReferenceSchema)` to create a new message.
  */
 export const PromotionTaskReferenceSchema: GenMessage<PromotionTaskReference> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 47);
+  messageDesc(file_api_v1alpha1_generated, 48);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionTaskSpec
@@ -2300,9 +2338,9 @@ export type PromotionTaskSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
    * values of these variables are the default values that can be
    * overridden by the step referencing the task.
    *
-   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.PromotionVariable vars = 1;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 1;
    */
-  vars: PromotionVariable[];
+  vars: ExpressionVariable[];
 
   /**
    * Steps specifies the directives to be executed as part of this
@@ -2323,7 +2361,7 @@ export type PromotionTaskSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(PromotionTaskSpecSchema)` to create a new message.
  */
 export const PromotionTaskSpecSchema: GenMessage<PromotionTaskSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 48);
+  messageDesc(file_api_v1alpha1_generated, 49);
 
 /**
  * PromotionTemplate defines a template for a Promotion that can be used to
@@ -2343,7 +2381,7 @@ export type PromotionTemplate = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(PromotionTemplateSchema)` to create a new message.
  */
 export const PromotionTemplateSchema: GenMessage<PromotionTemplate> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 49);
+  messageDesc(file_api_v1alpha1_generated, 50);
 
 /**
  * PromotionTemplateSpec describes the (partial) specification of a Promotion
@@ -2357,9 +2395,9 @@ export type PromotionTemplateSpec = Message<"github.com.akuity.kargo.api.v1alpha
    * Vars is a list of variables that can be referenced by expressions in
    * promotion steps.
    *
-   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.PromotionVariable vars = 2;
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 2;
    */
-  vars: PromotionVariable[];
+  vars: ExpressionVariable[];
 
   /**
    * Steps specifies the directives to be executed as part of a Promotion.
@@ -2379,40 +2417,6 @@ export type PromotionTemplateSpec = Message<"github.com.akuity.kargo.api.v1alpha
  * Use `create(PromotionTemplateSpecSchema)` to create a new message.
  */
 export const PromotionTemplateSpecSchema: GenMessage<PromotionTemplateSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 50);
-
-/**
- * PromotionVariable describes a single variable that may be referenced by
- * expressions in promotion steps.
- *
- * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionVariable
- */
-export type PromotionVariable = Message<"github.com.akuity.kargo.api.v1alpha1.PromotionVariable"> & {
-  /**
-   * Name is the name of the variable.
-   *
-   * +kubebuilder:validation:MinLength=1
-   * +kubebuilder:validation:Pattern=^[a-zA-Z_]\w*$
-   *
-   * @generated from field: optional string name = 1;
-   */
-  name: string;
-
-  /**
-   * Value is the value of the variable. It is allowed to utilize expressions
-   * in the value.
-   * See https://docs.kargo.io/user-guide/reference-docs/expressions for details.
-   *
-   * @generated from field: optional string value = 2;
-   */
-  value: string;
-};
-
-/**
- * Describes the message github.com.akuity.kargo.api.v1alpha1.PromotionVariable.
- * Use `create(PromotionVariableSchema)` to create a new message.
- */
-export const PromotionVariableSchema: GenMessage<PromotionVariable> = /*@__PURE__*/
   messageDesc(file_api_v1alpha1_generated, 51);
 
 /**
@@ -2529,6 +2533,15 @@ export type StageSpec = Message<"github.com.akuity.kargo.api.v1alpha1.StageSpec"
    */
   shard: string;
 
+  /**
+   * Vars is a list of variables that can be referenced anywhere in the
+   * StageSpec that supports expressions. For example, the PromotionTemplate
+   * and arguments of the Verification.
+   *
+   * @generated from field: repeated github.com.akuity.kargo.api.v1alpha1.ExpressionVariable vars = 7;
+   */
+  vars: ExpressionVariable[];
+
   /**
    * RequestedFreight expresses the Stage's need for certain pieces of Freight,
    * each having originated from a particular Warehouse. This list must be
diff --git a/ui/src/gen/directives/helm-template-config.json b/ui/src/gen/directives/helm-template-config.json
index c5f99095..a85e35b7 100644
--- a/ui/src/gen/directives/helm-template-config.json
+++ b/ui/src/gen/directives/helm-template-config.json
@@ -1,6 +1,23 @@
 {
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "HelmTemplateConfig",
+ "definitions": {
+  "setValues": {
+   "type": "object",
+   "additionalProperties": false,
+   "properties": {
+    "key": {
+     "type": "string",
+     "description": "The key whose value should be set. For nested values, use dots to delimit key parts. e.g. `image.tag`.",
+     "minLength": 1
+    },
+    "value": {
+     "type": "string",
+     "description": "The new value for the key."
+    }
+   }
+  }
+ },
  "type": "object",
  "additionalProperties": false,
  "properties": {
@@ -36,6 +53,25 @@
     "minLength": 1
    }
   },
+  "setValues": {
+   "type": "array",
+   "description": "Allows for amending chart configuration inline as one would with the `helm template` command's `--set` flag.",
+   "items": {
+    "type": "object",
+    "additionalProperties": false,
+    "properties": {
+     "key": {
+      "type": "string",
+      "description": "The key whose value should be set. For nested values, use dots to delimit key parts. e.g. `image.tag`.",
+      "minLength": 1
+     },
+     "value": {
+      "type": "string",
+      "description": "The new value for the key."
+     }
+    }
+   }
+  },
   "includeCRDs": {
    "type": "boolean",
    "description": "Whether to include CRDs in the rendered manifests.",
diff --git a/ui/src/gen/schema/clusterpromotiontasks.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/clusterpromotiontasks.kargo.akuity.io_v1alpha1.json
index 33108dcf..d53e31b1 100644
--- a/ui/src/gen/schema/clusterpromotiontasks.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/clusterpromotiontasks.kargo.akuity.io_v1alpha1.json
@@ -81,7 +81,7 @@
               "vars": {
                 "description": "Vars is a list of variables that can be referenced by expressions in\nthe step's Config. The values override the values specified in the\nPromotionSpec.",
                 "items": {
-                  "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                  "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                   "properties": {
                     "name": {
                       "description": "Name is the name of the variable.",
@@ -116,7 +116,7 @@
         "vars": {
           "description": "Vars specifies the variables available to the PromotionTask. The\nvalues of these variables are the default values that can be\noverridden by the step referencing the task.",
           "items": {
-            "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+            "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
             "properties": {
               "name": {
                 "description": "Name is the name of the variable.",
diff --git a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
index 3321b59d..32005620 100644
--- a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
@@ -96,7 +96,7 @@
               "vars": {
                 "description": "Vars is a list of variables that can be referenced by expressions in\nthe step's Config. The values override the values specified in the\nPromotionSpec.",
                 "items": {
-                  "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                  "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                   "properties": {
                     "name": {
                       "description": "Name is the name of the variable.",
@@ -131,7 +131,7 @@
         "vars": {
           "description": "Vars is a list of variables that can be referenced by expressions in\npromotion steps.",
           "items": {
-            "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+            "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
             "properties": {
               "name": {
                 "description": "Name is the name of the variable.",
diff --git a/ui/src/gen/schema/promotiontasks.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/promotiontasks.kargo.akuity.io_v1alpha1.json
index 396f2e15..cfc7be8d 100644
--- a/ui/src/gen/schema/promotiontasks.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/promotiontasks.kargo.akuity.io_v1alpha1.json
@@ -81,7 +81,7 @@
               "vars": {
                 "description": "Vars is a list of variables that can be referenced by expressions in\nthe step's Config. The values override the values specified in the\nPromotionSpec.",
                 "items": {
-                  "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                  "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                   "properties": {
                     "name": {
                       "description": "Name is the name of the variable.",
@@ -116,7 +116,7 @@
         "vars": {
           "description": "Vars specifies the variables available to the PromotionTask. The\nvalues of these variables are the default values that can be\noverridden by the step referencing the task.",
           "items": {
-            "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+            "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
             "properties": {
               "name": {
                 "description": "Name is the name of the variable.",
diff --git a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
index c2cb4eb0..0548d430 100644
--- a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
@@ -88,7 +88,7 @@
                       "vars": {
                         "description": "Vars is a list of variables that can be referenced by expressions in\nthe step's Config. The values override the values specified in the\nPromotionSpec.",
                         "items": {
-                          "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                          "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                           "properties": {
                             "name": {
                               "description": "Name is the name of the variable.",
@@ -123,7 +123,7 @@
                 "vars": {
                   "description": "Vars is a list of variables that can be referenced by expressions in\npromotion steps.",
                   "items": {
-                    "description": "PromotionVariable describes a single variable that may be referenced by\nexpressions in promotion steps.",
+                    "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
                     "properties": {
                       "name": {
                         "description": "Name is the name of the variable.",
@@ -223,6 +223,29 @@
           "description": "Shard is the name of the shard that this Stage belongs to. This is an\noptional field. If not specified, the Stage will belong to the default\nshard. A defaulting webhook will sync the value of the\nkargo.akuity.io/shard label with the value of this field. When this field\nis empty, the webhook will ensure that label is absent.",
           "type": "string"
         },
+        "vars": {
+          "description": "Vars is a list of variables that can be referenced anywhere in the\nStageSpec that supports expressions. For example, the PromotionTemplate\nand arguments of the Verification.",
+          "items": {
+            "description": "ExpressionVariable describes a single variable that may be referenced by\nexpressions in the context of a ClusterPromotionTask, PromotionTask,\nPromotion, AnalysisRun arguments, or other objects that support expressions.\n\nIt is used to pass information to the expression evaluation engine, and to\nallow for dynamic evaluation of expressions based on the variable values.",
+            "properties": {
+              "name": {
+                "description": "Name is the name of the variable.",
+                "minLength": 1,
+                "pattern": "^[a-zA-Z_]\\w*$",
+                "type": "string"
+              },
+              "value": {
+                "description": "Value is the value of the variable. It is allowed to utilize expressions\nin the value.\nSee https://docs.kargo.io/user-guide/reference-docs/expressions for details.",
+                "type": "string"
+              }
+            },
+            "required": [
+              "name"
+            ],
+            "type": "object"
+          },
+          "type": "array"
+        },
         "verification": {
           "description": "Verification describes how to verify a Stage's current Freight is fit for\npromotion downstream.",
           "properties": {



Test patch:
diff --git a/internal/controller/freight/finder_test.go b/internal/controller/freight/finder_test.go
index 2df033e5..4ee9a259 100644
--- a/internal/controller/freight/finder_test.go
+++ b/internal/controller/freight/finder_test.go
@@ -59,8 +59,10 @@ func TestFindCommit(t *testing.T) {
 					Commits: []kargoapi.GitCommit{testCommit2},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.GitCommit, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, commit *kargoapi.GitCommit, err error) {
+				require.NoError(t, err)
+				require.Nil(t, commit)
+
 			},
 		},
 		{
@@ -127,8 +129,9 @@ func TestFindCommit(t *testing.T) {
 					RequestedFreight: []kargoapi.FreightRequest{{Origin: testOrigin1}},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.GitCommit, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, commit *kargoapi.GitCommit, err error) {
+				require.NoError(t, err)
+				require.Nil(t, commit)
 			},
 		},
 		{
@@ -293,8 +296,9 @@ func TestFindImage(t *testing.T) {
 					Images: []kargoapi.Image{testImage2},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.Image, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, image *kargoapi.Image, err error) {
+				require.NoError(t, err)
+				require.Nil(t, image)
 			},
 		},
 		{
@@ -359,8 +363,9 @@ func TestFindImage(t *testing.T) {
 					RequestedFreight: []kargoapi.FreightRequest{{Origin: testOrigin1}},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.Image, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, image *kargoapi.Image, err error) {
+				require.NoError(t, err)
+				require.Nil(t, image)
 			},
 		},
 		{
@@ -656,8 +661,9 @@ func TestFindChart(t *testing.T) {
 					Charts: []kargoapi.Chart{testChart2},
 				},
 			},
-			assertions: func(t *testing.T, _ *kargoapi.Chart, err error) {
-				require.ErrorContains(t, err, "not found in referenced Freight")
+			assertions: func(t *testing.T, chart *kargoapi.Chart, err error) {
+				require.NoError(t, err)
+				require.Nil(t, chart)
 			},
 		},
 		{
diff --git a/internal/expressions/json_templates_test.go b/internal/expressions/json_templates_test.go
index 74941658..ae3d9f21 100644
--- a/internal/expressions/json_templates_test.go
+++ b/internal/expressions/json_templates_test.go
@@ -220,6 +220,16 @@ func TestEvaluateJSONTemplate(t *testing.T) {
 				require.Equal(t, `{"foo":"bar"}`, parsed.AString)
 			},
 		},
+		{
+			name:         "quote function doesn't double quote string input",
+			jsonTemplate: `{ "AString": "${{ quote('foo') }}" }`,
+			assertions: func(t *testing.T, jsonOutput []byte, err error) {
+				require.NoError(t, err)
+				parsed := testStruct{}
+				require.NoError(t, json.Unmarshal(jsonOutput, &parsed))
+				require.Equal(t, "foo", parsed.AString)
+			},
+		},
 		{
 			name: "a variety of tricky cases dealing with YAML and quotes",
 			yamlTemplate: `
diff --git a/internal/indexer/indexer_test.go b/internal/indexer/indexer_test.go
index 7e4ae63b..9d552681 100644
--- a/internal/indexer/indexer_test.go
+++ b/internal/indexer/indexer_test.go
@@ -9,8 +9,8 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	rbacapi "github.com/akuity/kargo/api/rbac/v1alpha1"
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
@@ -329,7 +329,7 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
 				},
 				Spec: kargoapi.PromotionSpec{
 					Stage: "fake-stage",
-					Vars: []kargoapi.PromotionVariable{
+					Vars: []kargoapi.ExpressionVariable{
 						{
 							Name:  "app",
 							Value: "fake-app-from-var",
@@ -361,7 +361,7 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
 						},
 						{
 							Uses: "argocd-update",
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{
 									Name:  "app",
 									Value: "fake-app-from-step-var",
@@ -381,7 +381,7 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
 						},
 						{
 							Uses: "argocd-update",
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{
 									Name:  "input",
 									Value: "${{ outputs.composition.name }}",
@@ -449,13 +449,12 @@ func TestRunningPromotionsByArgoCDApplications(t *testing.T) {
 	}
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
-			scheme := runtime.NewScheme()
-			require.NoError(t, kargoapi.AddToScheme(scheme))
 			require.Equal(
 				t,
 				testCase.expected,
 				RunningPromotionsByArgoCDApplications(
 					context.TODO(),
+					fake.NewClientBuilder().Build(),
 					testCase.shardName,
 				)(testCase.obj),
 			)
diff --git a/internal/kargo/promotion_builder_test.go b/internal/kargo/promotion_builder_test.go
index 8f1a5947..666c6be0 100644
--- a/internal/kargo/promotion_builder_test.go
+++ b/internal/kargo/promotion_builder_test.go
@@ -82,9 +82,12 @@ func TestPromotionBuilder_Build(t *testing.T) {
 					Namespace: "test-project",
 				},
 				Spec: kargoapi.StageSpec{
+					Vars: []kargoapi.ExpressionVariable{
+						{Name: "key0", Value: "value0"},
+					},
 					PromotionTemplate: &kargoapi.PromotionTemplate{
 						Spec: kargoapi.PromotionTemplateSpec{
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{Name: "key1", Value: "value1"},
 							},
 							Steps: []kargoapi.PromotionStep{
@@ -108,7 +111,11 @@ func TestPromotionBuilder_Build(t *testing.T) {
 				assert.Equal(t, "abc123", promotion.Spec.Freight)
 
 				// Check vars
-				assert.Equal(t, []kargoapi.PromotionVariable{
+				assert.Equal(t, []kargoapi.ExpressionVariable{
+					{
+						Name:  "key0",
+						Value: "value0",
+					},
 					{
 						Name:  "key1",
 						Value: "value1",
@@ -141,7 +148,7 @@ func TestPromotionBuilder_Build(t *testing.T) {
 									Task: &kargoapi.PromotionTaskReference{
 										Name: "test-task",
 									},
-									Vars: []kargoapi.PromotionVariable{
+									Vars: []kargoapi.ExpressionVariable{
 										{Name: "input1", Value: "value1"},
 									},
 								},
@@ -252,7 +259,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 							Task: &kargoapi.PromotionTaskReference{
 								Name: "test-task",
 							},
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{Name: "input1", Value: "value1"},
 							},
 						},
@@ -266,7 +273,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -289,7 +296,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 				// Check inflated task step
 				assert.Equal(t, "task-step::sub-step", steps[1].As)
 				assert.Equal(t, "other-fake-step", steps[1].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 				}, steps[1].Vars)
 			},
@@ -308,7 +315,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 							Task: &kargoapi.PromotionTaskReference{
 								Name: "test-task-1",
 							},
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{Name: "input1", Value: "value1"},
 							},
 						},
@@ -318,7 +325,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 								Kind: "ClusterPromotionTask",
 								Name: "test-task-2",
 							},
-							Vars: []kargoapi.PromotionVariable{
+							Vars: []kargoapi.ExpressionVariable{
 								{Name: "input2", Value: "value2"},
 							},
 						},
@@ -332,7 +339,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -348,7 +355,7 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 						Name: "test-task-2",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input2"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -366,13 +373,13 @@ func TestPromotionBuilder_InflateSteps(t *testing.T) {
 
 				assert.Equal(t, "task1::step1", steps[0].As)
 				assert.Equal(t, "fake-step", steps[0].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 				}, steps[0].Vars)
 
 				assert.Equal(t, "task2::step2", steps[1].As)
 				assert.Equal(t, "other-fake-step", steps[1].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input2", Value: "value2"},
 				}, steps[1].Vars)
 			},
@@ -402,7 +409,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 		name       string
 		project    string
 		taskAlias  string
-		promoVars  []kargoapi.PromotionVariable
+		promoVars  []kargoapi.ExpressionVariable
 		taskStep   kargoapi.PromotionStep
 		objects    []client.Object
 		assertions func(*testing.T, []kargoapi.PromotionStep, error)
@@ -439,7 +446,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 					},
@@ -454,14 +461,14 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 			name:      "successful task step inflation",
 			project:   "test-project",
 			taskAlias: "task-1",
-			promoVars: []kargoapi.PromotionVariable{
+			promoVars: []kargoapi.ExpressionVariable{
 				{Name: "input3", Value: "value1"},
 			},
 			taskStep: kargoapi.PromotionStep{
 				Task: &kargoapi.PromotionTaskReference{
 					Name: "test-task",
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 					{Name: "input2", Value: "value2"},
 				},
@@ -473,7 +480,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 							{Name: "input2", Value: "default2"},
 							{Name: "input3"},
@@ -486,7 +493,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 							{
 								As:   "step2",
 								Uses: "other-fake-step",
-								Vars: []kargoapi.PromotionVariable{
+								Vars: []kargoapi.ExpressionVariable{
 									{Name: "input4", Value: "value4"},
 								},
 							},
@@ -500,7 +507,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 
 				assert.Equal(t, "task-1::step1", steps[0].As)
 				assert.Equal(t, "fake-step", steps[0].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input2", Value: "default2"},
 					{Name: "input1", Value: "value1"},
 					{Name: "input2", Value: "value2"},
@@ -508,7 +515,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 
 				assert.Equal(t, "task-1::step2", steps[1].As)
 				assert.Equal(t, "other-fake-step", steps[1].Uses)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input2", Value: "default2"},
 					{Name: "input1", Value: "value1"},
 					{Name: "input2", Value: "value2"},
@@ -524,7 +531,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 				Task: &kargoapi.PromotionTaskReference{
 					Name: "test-task",
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 				},
 			},
@@ -535,7 +542,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -566,7 +573,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 					Kind: "ClusterPromotionTask",
 					Name: "test-cluster-task",
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "value1"},
 				},
 			},
@@ -576,7 +583,7 @@ func TestPromotionBuilder_inflateTaskSteps(t *testing.T) {
 						Name: "test-cluster-task",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1"},
 						},
 						Steps: []kargoapi.PromotionStep{
@@ -703,7 +710,7 @@ func TestPromotionBuilder_getTaskSpec(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1", Value: "value1"},
 						},
 					},
@@ -731,7 +738,7 @@ func TestPromotionBuilder_getTaskSpec(t *testing.T) {
 						Name: "test-cluster-task",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1", Value: "value1"},
 						},
 					},
@@ -760,7 +767,7 @@ func TestPromotionBuilder_getTaskSpec(t *testing.T) {
 						Namespace: "test-project",
 					},
 					Spec: kargoapi.PromotionTaskSpec{
-						Vars: []kargoapi.PromotionVariable{
+						Vars: []kargoapi.ExpressionVariable{
 							{Name: "input1", Value: "value1"},
 						},
 					},
@@ -937,66 +944,66 @@ func Test_generatePromotionTaskStepName(t *testing.T) {
 func Test_promotionTaskVarsToStepVars(t *testing.T) {
 	tests := []struct {
 		name       string
-		taskVars   []kargoapi.PromotionVariable
-		promoVars  []kargoapi.PromotionVariable
-		stepVars   []kargoapi.PromotionVariable
-		assertions func(t *testing.T, result []kargoapi.PromotionVariable, err error)
+		taskVars   []kargoapi.ExpressionVariable
+		promoVars  []kargoapi.ExpressionVariable
+		stepVars   []kargoapi.ExpressionVariable
+		assertions func(t *testing.T, result []kargoapi.ExpressionVariable, err error)
 	}{
 		{
 			name:     "nil inputs returns nil map and no error",
 			taskVars: nil,
 			stepVars: nil,
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
 				assert.Nil(t, result)
 			},
 		},
 		{
 			name:     "empty inputs returns nil map and no error",
-			taskVars: []kargoapi.PromotionVariable{},
+			taskVars: []kargoapi.ExpressionVariable{},
 			stepVars: nil,
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
 				assert.Nil(t, result)
 			},
 		},
 		{
 			name: "missing required variable returns error",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1"},
 			},
-			stepVars: []kargoapi.PromotionVariable{
+			stepVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: ""},
 			},
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				assert.ErrorContains(t, err, "missing value for variable \"input1\"")
 				assert.Nil(t, result)
 			},
 		},
 		{
 			name: "default value used when config value not provided",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "default1"},
 			},
 			stepVars: nil,
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "default1"},
 				}, result)
 			},
 		},
 		{
 			name: "step value appends task default value",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "default1"},
 			},
-			stepVars: []kargoapi.PromotionVariable{
+			stepVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "override1"},
 			},
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "default1"},
 					{Name: "input1", Value: "override1"},
 				}, result)
@@ -1004,14 +1011,14 @@ func Test_promotionTaskVarsToStepVars(t *testing.T) {
 		},
 		{
 			name: "promotion variable overrides default value",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "default1"},
 			},
-			promoVars: []kargoapi.PromotionVariable{
+			promoVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "override1"},
 			},
 			stepVars: nil,
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
 				// Variable is set by engine at runtime
 				assert.Empty(t, result)
@@ -1019,18 +1026,18 @@ func Test_promotionTaskVarsToStepVars(t *testing.T) {
 		},
 		{
 			name: "multiple inputs processed correctly",
-			taskVars: []kargoapi.PromotionVariable{
+			taskVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "default1"},
 				{Name: "input2", Value: "default2"},
 				{Name: "input3"},
 			},
-			stepVars: []kargoapi.PromotionVariable{
+			stepVars: []kargoapi.ExpressionVariable{
 				{Name: "input1", Value: "override1"},
 				{Name: "input3", Value: "value3"},
 			},
-			assertions: func(t *testing.T, result []kargoapi.PromotionVariable, err error) {
+			assertions: func(t *testing.T, result []kargoapi.ExpressionVariable, err error) {
 				require.NoError(t, err)
-				assert.ElementsMatch(t, []kargoapi.PromotionVariable{
+				assert.ElementsMatch(t, []kargoapi.ExpressionVariable{
 					{Name: "input1", Value: "default1"},
 					{Name: "input2", Value: "default2"},
 					{Name: "input1", Value: "override1"},
diff --git a/internal/promotion/promotion_test.go b/internal/promotion/promotion_test.go
index fbf669a0..ff823e45 100644
--- a/internal/promotion/promotion_test.go
+++ b/internal/promotion/promotion_test.go
@@ -97,8 +97,8 @@ func TestStep_GetTimeout(t *testing.T) {
 
 func TestStep_GetConfig(t *testing.T) {
 	testScheme := k8sruntime.NewScheme()
-	err := kargoapi.AddToScheme(testScheme)
-	require.NoError(t, err)
+	require.NoError(t, kargoapi.AddToScheme(testScheme))
+
 	testClient := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(
 		&kargoapi.Warehouse{
 			ObjectMeta: v1.ObjectMeta{
@@ -194,7 +194,7 @@ func TestStep_GetConfig(t *testing.T) {
 			name: "test vars with literal values",
 			// Test that vars can be assigned literal values
 			promoCtx: Context{
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "strVar",
 						Value: "foo",
@@ -224,7 +224,7 @@ func TestStep_GetConfig(t *testing.T) {
 			name: "test vars with expressions",
 			// Test using expressions to define the value of vars
 			promoCtx: Context{
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "strVar",
 						Value: "${{ 'f' + 'o' + 'o' }}",
@@ -274,7 +274,7 @@ func TestStep_GetConfig(t *testing.T) {
 			// Test that the warehouse() function can be used to reference freight
 			// origins
 			promoCtx: Context{
-				Vars: []kargoapi.PromotionVariable{{
+				Vars: []kargoapi.ExpressionVariable{{
 					Name:  "warehouseName",
 					Value: "fake-warehouse",
 				}},
@@ -322,7 +322,7 @@ func TestStep_GetConfig(t *testing.T) {
 						},
 					},
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "warehouseName",
 						Value: "fake-warehouse",
@@ -377,7 +377,7 @@ func TestStep_GetConfig(t *testing.T) {
 						},
 					},
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "warehouseName",
 						Value: "fake-warehouse",
@@ -439,7 +439,7 @@ func TestStep_GetConfig(t *testing.T) {
 						},
 					},
 				},
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "warehouseName",
 						Value: "fake-warehouse",
@@ -502,6 +502,384 @@ func TestStep_GetConfig(t *testing.T) {
 		})
 	}
 }
+
+func TestStep_GetVars(t *testing.T) {
+	testScheme := k8sruntime.NewScheme()
+	require.NoError(t, kargoapi.AddToScheme(testScheme))
+
+	testClient := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(
+		&kargoapi.Warehouse{
+			ObjectMeta: v1.ObjectMeta{
+				Name:      "fake-warehouse",
+				Namespace: "fake-project",
+			},
+			Spec: kargoapi.WarehouseSpec{
+				Subscriptions: []kargoapi.RepoSubscription{
+					{
+						Git: &kargoapi.GitSubscription{
+							RepoURL: "https://fake-git-repo",
+						},
+					},
+				},
+			},
+		},
+	).Build()
+
+	testCases := []struct {
+		name         string
+		promoCtx     Context
+		promoState   promotion.State
+		step         Step
+		expectedVars map[string]any
+		expectErr    bool
+	}{
+		{
+			name: "global vars with literal values",
+			promoCtx: Context{
+				Project: "fake-project",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "str",
+						Value: "foo",
+					},
+					{
+						Name:  "bool",
+						Value: "true",
+					},
+					{
+						Name:  "num",
+						Value: "42",
+					},
+				},
+			},
+			step: Step{},
+			expectedVars: map[string]any{
+				"str":  "foo",
+				"bool": "true",
+				"num":  "42",
+			},
+		},
+		{
+			name: "global vars with expressions",
+			promoCtx: Context{
+				Project: "fake-project",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "str",
+						Value: "${{ 'f' + 'o' + 'o' }}",
+					},
+					{
+						Name:  "bool",
+						Value: "${{ 1 == 1 }}",
+					},
+					{
+						Name:  "num",
+						Value: "${{ 40 + 2 }}",
+					},
+				},
+			},
+			step: Step{},
+			expectedVars: map[string]any{
+				"str":  "foo",
+				"bool": true,
+				"num":  float64(42),
+			},
+		},
+		{
+			name: "step vars with literal values",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "str",
+						Value: "foo",
+					},
+					{
+						Name:  "bool",
+						Value: "true",
+					},
+					{
+						Name:  "num",
+						Value: "42",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"str":  "foo",
+				"bool": "true",
+				"num":  "42",
+			},
+		},
+		{
+			name: "step vars with expressions",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "str",
+						Value: "${{ 'f' + 'o' + 'o' }}",
+					},
+					{
+						Name:  "bool",
+						Value: "${{ 1 == 1 }}",
+					},
+					{
+						Name:  "num",
+						Value: "${{ 40 + 2 }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"str":  "foo",
+				"bool": true,
+				"num":  float64(42),
+			},
+		},
+		{
+			name: "step vars referencing global vars",
+			promoCtx: Context{
+				Project: "fake-project",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "global",
+						Value: "global-value",
+					},
+				},
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "local",
+						Value: "${{ vars.global + '-suffix' }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"global": "global-value",
+				"local":  "global-value-suffix",
+			},
+		},
+		{
+			name: "step vars referencing other step vars",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "v1",
+						Value: "value1",
+					},
+					{
+						Name:  "v2",
+						Value: "${{ vars.v1 + '-suffix' }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"v1": "value1",
+				"v2": "value1-suffix",
+			},
+		},
+		{
+			name: "step vars referencing outputs",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			promoState: promotion.State{
+				"output1": "output-value",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "fromOutput",
+						Value: "${{ outputs.output1 + '-suffix' }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"fromOutput": "output-value-suffix",
+			},
+		},
+		{
+			name: "step vars referencing task outputs",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			promoState: promotion.State{
+				"task::alias": map[string]any{
+					"foo": "baz",
+				},
+			},
+			step: Step{
+				Alias: "task::other-alias",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "fromTask",
+						Value: "${{ task.outputs.alias.foo + '-suffix' }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"fromTask": "baz-suffix",
+			},
+		},
+		{
+			name: "invalid expression in global var",
+			promoCtx: Context{
+				Project: "fake-project",
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "invalid",
+						Value: "${{ invalid.expression }}",
+					},
+				},
+			},
+			step:      Step{},
+			expectErr: true,
+		},
+		{
+			name: "invalid expression in step var",
+			promoCtx: Context{
+				Project: "fake-project",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "invalid",
+						Value: "${{ invalid.expression }}",
+					},
+				},
+			},
+			expectErr: true,
+		},
+		{
+			name: "warehouse function in vars",
+			promoCtx: Context{
+				Project: "fake-project",
+				FreightRequests: []kargoapi.FreightRequest{{
+					Origin: kargoapi.FreightOrigin{
+						Kind: kargoapi.FreightOriginKindWarehouse,
+						Name: "fake-warehouse",
+					},
+				}},
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "wh",
+						Value: "${{ warehouse('fake-warehouse') }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"wh": map[string]any{
+					"kind": string(kargoapi.FreightOriginKindWarehouse),
+					"name": "fake-warehouse",
+				},
+			},
+		},
+		{
+			name: "context properties in vars",
+			promoCtx: Context{
+				Project:   "fake-project",
+				Stage:     "fake-stage",
+				Promotion: "fake-promotion",
+				Actor:     "fake-creator",
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "proj",
+						Value: "${{ ctx.project }}",
+					},
+					{
+						Name:  "stage",
+						Value: "${{ ctx.stage }}",
+					},
+					{
+						Name:  "promo",
+						Value: "${{ ctx.promotion }}",
+					},
+					{
+						Name:  "actor",
+						Value: "${{ ctx.meta.promotion.actor }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"proj":  "fake-project",
+				"stage": "fake-stage",
+				"promo": "fake-promotion",
+				"actor": "fake-creator",
+			},
+		},
+		{
+			name: "freight functions in vars",
+			promoCtx: Context{
+				Project: "fake-project",
+				FreightRequests: []kargoapi.FreightRequest{{
+					Origin: kargoapi.FreightOrigin{
+						Kind: kargoapi.FreightOriginKindWarehouse,
+						Name: "fake-warehouse",
+					},
+					Sources: kargoapi.FreightSources{
+						Direct: true,
+					},
+				}},
+				Freight: kargoapi.FreightCollection{
+					Freight: map[string]kargoapi.FreightReference{
+						"Warehouse/fake-warehouse": {
+							Origin: kargoapi.FreightOrigin{
+								Kind: kargoapi.FreightOriginKindWarehouse,
+								Name: "fake-warehouse",
+							},
+							Commits: []kargoapi.GitCommit{{
+								RepoURL: "https://fake-git-repo",
+								ID:      "fake-commit-id",
+							}},
+						},
+					},
+				},
+			},
+			step: Step{
+				Vars: []kargoapi.ExpressionVariable{
+					{
+						Name:  "commit",
+						Value: "${{ commitFrom('https://fake-git-repo').ID }}",
+					},
+				},
+			},
+			expectedVars: map[string]any{
+				"commit": "fake-commit-id",
+			},
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			vars, err := testCase.step.GetVars(
+				context.Background(),
+				testClient,
+				testCase.promoCtx,
+				testCase.promoState,
+			)
+
+			if testCase.expectErr {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, testCase.expectedVars, vars)
+		})
+	}
+}
+
 func TestStep_Skip(t *testing.T) {
 	tests := []struct {
 		name       string
@@ -524,7 +902,7 @@ func TestStep_Skip(t *testing.T) {
 				If: "${{ vars.foo == 'bar' }}",
 			},
 			ctx: Context{
-				Vars: []kargoapi.PromotionVariable{
+				Vars: []kargoapi.ExpressionVariable{
 					{
 						Name:  "foo",
 						Value: "bar",
@@ -565,6 +943,33 @@ func TestStep_Skip(t *testing.T) {
 				assert.True(t, b)
 			},
 		},
+		{
+			name: "if condition uses expression function",
+			step: &Step{
+				If: "${{ commitFrom('https://git.example.com', warehouse('fake-warehouse')).ID == 'foo' }}",
+			},
+			ctx: Context{
+				Project: "fake-project",
+				Freight: kargoapi.FreightCollection{
+					Freight: map[string]kargoapi.FreightReference{
+						"Warehouse/fake-warehouse": {
+							Origin: kargoapi.FreightOrigin{
+								Kind: kargoapi.FreightOriginKindWarehouse,
+								Name: "fake-warehouse",
+							},
+							Commits: []kargoapi.GitCommit{{
+								RepoURL: "https://git.example.com",
+								ID:      "foo",
+							}},
+						},
+					},
+				},
+			},
+			assertions: func(t *testing.T, b bool, err error) {
+				assert.NoError(t, err)
+				assert.False(t, b)
+			},
+		},
 		{
 			name: "if condition does not evaluate to a boolean",
 			step: &Step{
@@ -578,7 +983,12 @@ func TestStep_Skip(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := tt.step.Skip(tt.ctx, tt.state)
+			got, err := tt.step.Skip(
+				context.Background(),
+				fake.NewClientBuilder().Build(),
+				tt.ctx,
+				tt.state,
+			)
 			tt.assertions(t, got, err)
 		})
 	}
diff --git a/internal/promotion/runner/builtin/helm_template_runner_test.go b/internal/promotion/runner/builtin/helm_template_runner_test.go
index 924f2a88..cbd6819a 100644
--- a/internal/promotion/runner/builtin/helm_template_runner_test.go
+++ b/internal/promotion/runner/builtin/helm_template_runner_test.go
@@ -107,6 +107,53 @@ data:
 # Source: test-chart/templates/test.yaml
 apiVersion: v1
 kind: ConfigMap
+metadata:
+  name: test-release-configmap
+  namespace: test-namespace
+data:
+  value1: value1
+  value2: value2
+`, string(content))
+			},
+		},
+		{
+			name: "successful run with set values",
+			files: map[string]string{
+				"values.yaml": "key1: value1",
+				"charts/test-chart/Chart.yaml": `apiVersion: v1
+name: test-chart
+version: 0.1.0`,
+				"charts/test-chart/templates/test.yaml": `---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-configmap
+  namespace: {{ .Release.Namespace }}
+data:
+  value1: {{ .Values.key1 }}
+  value2: {{ .Values.key2 }}
+`,
+			},
+			cfg: builtin.HelmTemplateConfig{
+				Path:        "charts/test-chart",
+				ValuesFiles: []string{"values.yaml"},
+				SetValues:   []builtin.SetValues{{Key: "key2", Value: "value2"}},
+				OutPath:     "output.yaml",
+				ReleaseName: "test-release",
+				Namespace:   "test-namespace",
+			},
+			assertions: func(t *testing.T, workDir string, result promotion.StepResult, err error) {
+				require.NoError(t, err)
+				assert.Equal(t, promotion.StepResult{Status: kargoapi.PromotionPhaseSucceeded}, result)
+
+				outPath := filepath.Join(workDir, "output.yaml")
+				require.FileExists(t, outPath)
+				content, err := os.ReadFile(outPath)
+				require.NoError(t, err)
+				assert.Equal(t, `---
+# Source: test-chart/templates/test.yaml
+apiVersion: v1
+kind: ConfigMap
 metadata:
   name: test-release-configmap
   namespace: test-namespace
@@ -302,19 +349,56 @@ func Test_helmTemplateRunner_composeValues(t *testing.T) {
 	tests := []struct {
 		name           string
 		workDir        string
-		valuesFiles    []string
+		cfg            builtin.HelmTemplateConfig
 		valuesContents map[string]string
 		assertions     func(*testing.T, map[string]any, error)
 	}{
 		{
-			name:        "successful composition",
-			workDir:     t.TempDir(),
-			valuesFiles: []string{"values1.yaml", "values2.yaml"},
+			name:    "successful composition",
+			workDir: t.TempDir(),
+			cfg: builtin.HelmTemplateConfig{
+				ValuesFiles: []string{"values1.yaml", "values2.yaml"},
+				SetValues:   []builtin.SetValues{{Key: "key3", Value: "value3"}},
+			},
 			valuesContents: map[string]string{
 				"values1.yaml": "key1: value1",
 				"values2.yaml": "key2: value2",
 			},
 			assertions: func(t *testing.T, result map[string]any, err error) {
+				assert.NoError(t, err)
+				assert.Equal(t, map[string]any{
+					"key1": "value1",
+					"key2": "value2",
+					"key3": "value3",
+				}, result)
+			},
+		},
+		{
+			name:    "successful composition with no files",
+			workDir: t.TempDir(),
+			cfg: builtin.HelmTemplateConfig{
+				ValuesFiles: []string{},
+				SetValues:   []builtin.SetValues{{Key: "key3", Value: "value3"}},
+			},
+			valuesContents: map[string]string{},
+			assertions: func(t *testing.T, result map[string]any, err error) {
+				assert.NoError(t, err)
+				assert.Equal(t, map[string]any{
+					"key3": "value3",
+				}, result)
+			},
+		},
+		{
+			name:    "successful composition with no set values",
+			workDir: t.TempDir(),
+			cfg: builtin.HelmTemplateConfig{
+				ValuesFiles: []string{"values1.yaml", "values2.yaml"},
+				SetValues:   []builtin.SetValues{},
+			},
+			valuesContents: map[string]string{
+				"values1.yaml": "key1: value1",
+				"values2.yaml": "key2: value2",
+			}, assertions: func(t *testing.T, result map[string]any, err error) {
 				assert.NoError(t, err)
 				assert.Equal(t, map[string]any{
 					"key1": "value1",
@@ -323,9 +407,12 @@ func Test_helmTemplateRunner_composeValues(t *testing.T) {
 			},
 		},
 		{
-			name:        "file not found",
-			workDir:     t.TempDir(),
-			valuesFiles: []string{"non_existent.yaml"},
+			name:    "file not found",
+			workDir: t.TempDir(),
+			cfg: builtin.HelmTemplateConfig{
+				ValuesFiles: []string{"non_existent.yaml"},
+				SetValues:   []builtin.SetValues{},
+			},
 			assertions: func(t *testing.T, result map[string]any, err error) {
 				assert.Error(t, err)
 				assert.Nil(t, result)
@@ -340,7 +427,7 @@ func Test_helmTemplateRunner_composeValues(t *testing.T) {
 			for p, c := range tt.valuesContents {
 				require.NoError(t, os.WriteFile(filepath.Join(tt.workDir, p), []byte(c), 0o600))
 			}
-			result, err := runner.composeValues(tt.workDir, tt.valuesFiles)
+			result, err := runner.composeValues(tt.workDir, tt.cfg)
 			tt.assertions(t, result, err)
 		})
 	}
diff --git a/internal/rollouts/analysis_run_builder_test.go b/internal/rollouts/analysis_run_builder_test.go
index 852536d1..bf7fb3b0 100644
--- a/internal/rollouts/analysis_run_builder_test.go
+++ b/internal/rollouts/analysis_run_builder_test.go
@@ -624,6 +624,60 @@ func TestAnalysisRunBuilder_buildArgs(t *testing.T) {
 				require.ErrorContains(t, err, "evaluate argument \"param1\"")
 			},
 		},
+		{
+			name: "evaluate variable in argument",
+			template: &rolloutsapi.AnalysisTemplate{
+				Spec: rolloutsapi.AnalysisTemplateSpec{
+					Args: []rolloutsapi.Argument{
+						{Name: "param1"},
+					},
+				},
+			},
+			args: []kargoapi.AnalysisRunArgument{
+				{Name: "param1", Value: "${{ vars.test }}"},
+			},
+			exprCfg: &ArgumentEvaluationConfig{
+				Vars: []ArgumentVariable{
+					{Name: "test", Value: "value1"},
+					// Ensure overriding and expression evaluation works.
+					{Name: "test", Value: "${{ vars.test + '-suffix'}}"},
+				},
+			},
+			assertions: func(t *testing.T, args []rolloutsapi.Argument, err error) {
+				require.NoError(t, err)
+				assert.Len(t, args, 1)
+				assert.Equal(t, "value1-suffix", *args[0].Value)
+			},
+		},
+		{
+			name:     "variable evaluation error",
+			template: &rolloutsapi.AnalysisTemplate{},
+			exprCfg: &ArgumentEvaluationConfig{
+				Vars: []ArgumentVariable{
+					{Name: "test", Value: "${{ ctx.test }}"},
+				},
+			},
+			assertions: func(t *testing.T, _ []rolloutsapi.Argument, err error) {
+				require.ErrorContains(t, err, "evaluate variable \"test\"")
+			},
+		},
+		{
+			name:     "existing vars in env",
+			template: &rolloutsapi.AnalysisTemplate{},
+			exprCfg: &ArgumentEvaluationConfig{
+				Env: map[string]any{
+					"vars": map[string]string{
+						"test": "value1",
+					},
+				},
+				Vars: []ArgumentVariable{
+					{Name: "test", Value: "value2"},
+				},
+			},
+			assertions: func(t *testing.T, _ []rolloutsapi.Argument, err error) {
+				require.ErrorContains(t, err, "vars in argument evaluation config env is of unexpected type")
+			},
+		},
 	}
 
 	for _, tt := range tests {