Repository: akuity/kargo
Pull request #2294 (2024-07-14 05:25:20)

chore(deps): bump vitest from 1.6.0 to 2.0.2 in /ui in the js-major group
Bumps the js-major group in /ui with 1 update: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).

Updates `vitest` from 1.6.0 to 2.0.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/vitest-dev/vitest/releases">vitest's releases</a>.</em></p>
<blockquote>
<h2>v2.0.2</h2>
<h3>   🐞 Bug Fixes</h3>
<ul>
<li><strong>browser</strong>:
<ul>
<li>Remove <code>@​vitest/runner</code> &gt; pretty-format from prebundling because it doesn't exist  -  by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6075">vitest-dev/vitest#6075</a> <a href="https://github.com/vitest-dev/vitest/commit/a169d25a"><!-- raw HTML omitted -->(a169d)<!-- raw HTML omitted --></a></li>
<li>Inline pretty-format and replace picocolors with tinyrainbow  -  by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6077">vitest-dev/vitest#6077</a> <a href="https://github.com/vitest-dev/vitest/commit/80a43d5c"><!-- raw HTML omitted -->(80a43)<!-- raw HTML omitted --></a></li>
<li>Remove <code>crypto.randomUUID</code> from the client  -  by <a href="https://github.com/userquin"><code>@​userquin</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6079">vitest-dev/vitest#6079</a> <a href="https://github.com/vitest-dev/vitest/commit/04c83acc"><!-- raw HTML omitted -->(04c83)<!-- raw HTML omitted --></a></li>
<li>Correctly overwrite loupe require  -  by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> <a href="https://github.com/vitest-dev/vitest/commit/756ac2bc"><!-- raw HTML omitted -->(756ac)<!-- raw HTML omitted --></a></li>
<li>Dedupe <code>@​vitest/browser/</code>client  -  by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6081">vitest-dev/vitest#6081</a> <a href="https://github.com/vitest-dev/vitest/commit/ee725184"><!-- raw HTML omitted -->(ee725)<!-- raw HTML omitted --></a></li>
<li>Don't release keyboard automatically  -  by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6083">vitest-dev/vitest#6083</a> <a href="https://github.com/vitest-dev/vitest/commit/58fac77b"><!-- raw HTML omitted -->(58fac)<!-- raw HTML omitted --></a></li>
</ul>
</li>
<li><strong>ui</strong>:
<ul>
<li>Test files notified only when running  -  by <a href="https://github.com/userquin"><code>@​userquin</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6069">vitest-dev/vitest#6069</a> <a href="https://github.com/vitest-dev/vitest/commit/af7fec58"><!-- raw HTML omitted -->(af7fe)<!-- raw HTML omitted --></a></li>
<li>Encode html entities in task name  -  by <a href="https://github.com/userquin"><code>@​userquin</code></a> and <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6070">vitest-dev/vitest#6070</a> <a href="https://github.com/vitest-dev/vitest/commit/7f0cc24e"><!-- raw HTML omitted -->(7f0cc)<!-- raw HTML omitted --></a></li>
</ul>
</li>
<li><strong>web-worker</strong>:
<ul>
<li>Peer dependency version on <code>vitest</code>  -  by <a href="https://github.com/AriPerkkio"><code>@​AriPerkkio</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6060">vitest-dev/vitest#6060</a> <a href="https://github.com/vitest-dev/vitest/commit/95a2d870"><!-- raw HTML omitted -->(95a2d)<!-- raw HTML omitted --></a></li>
</ul>
</li>
</ul>
<h3>   🏎 Performance</h3>
<ul>
<li><strong>browser</strong>: Don't import msw if no modules are mocked  -  by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6073">vitest-dev/vitest#6073</a> <a href="https://github.com/vitest-dev/vitest/commit/aa52792a"><!-- raw HTML omitted -->(aa527)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a href="https://github.com/vitest-dev/vitest/compare/v2.0.1...v2.0.2">View changes on GitHub</a></h5>
<h2>v2.0.1</h2>
<h3>   🐞 Bug Fixes</h3>
<ul>
<li><strong>browser</strong>: Correctly inherit browser config in a workspace  -  by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6054">vitest-dev/vitest#6054</a> <a href="https://github.com/vitest-dev/vitest/commit/4b03e72b"><!-- raw HTML omitted -->(4b03e)<!-- raw HTML omitted --></a></li>
<li><strong>ui</strong>: Move virtual scroller to dev dependencies  -  by <a href="https://github.com/userquin"><code>@​userquin</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6053">vitest-dev/vitest#6053</a> <a href="https://github.com/vitest-dev/vitest/commit/f94ede02"><!-- raw HTML omitted -->(f94ed)<!-- raw HTML omitted --></a></li>
<li><strong>vitest</strong>: Print only running files, not every file  -  by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/6052">vitest-dev/vitest#6052</a> <a href="https://github.com/vitest-dev/vitest/commit/4d5597df"><!-- raw HTML omitted -->(4d559)<!-- raw HTML omitted --></a></li>
</ul>
<h5>    <a href="https://github.com/vitest-dev/vitest/compare/v2.0.0...v2.0.1">View changes on GitHub</a></h5>
<h2>v2.0.0</h2>
<p>Vitest 2.0 is here! This release page lists all changes made to the project during the beta. For the migration guide, please refer to the <a href="https://vitest.dev/guide/migration.html#migrating-to-vitest-2-0">documentation</a>.</p>
<h3>🚨 Breaking Changes</h3>
<ul>
<li>Simplify mock function generic types and align with jest - by <strong><a href="https://github.com/hi-ogawa"><code>@​hi-ogawa</code></a></strong> in <a href="https://redirect.github.com/vitest-dev/vitest/pull/4784">vitest-dev/vitest#4784</a> <a href="https://github.com/vitest-dev/vitest/commit/a0c1d371edb35abfa0e6f53b05086a54d2ecc0a6"><!-- raw HTML omitted -->(a0c1d37)<!-- raw HTML omitted --></a></li>
<li>Remove <code>--segfault-retry</code> - by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/5514">vitest-dev/vitest#5514</a> <a href="https://github.com/vitest-dev/vitest/commit/ed60e405e"><!-- raw HTML omitted -->(ed60e)<!-- raw HTML omitted --></a>
<ul>
<li>This flag was introduced to combat <code>threads</code> segfaults. Our current recommendation is to use the new default <code>forks</code> pool instead.</li>
</ul>
</li>
<li>Run suite hooks in a stack - by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/5609">vitest-dev/vitest#5609</a> <a href="https://github.com/vitest-dev/vitest/commit/1277dc1e3"><!-- raw HTML omitted -->(1277d)<!-- raw HTML omitted --></a>
<ul>
<li>This feels like a more sensible default. Especially with the new <a href="https://vitest.dev/api/#ontestfinished"><code>onTestFinished</code></a> hook. This can make your tests run a little bit slower.</li>
</ul>
</li>
<li>Enable <code>coverage.ignoreEmptyLines</code> by default - by <a href="https://github.com/AriPerkkio"><code>@​AriPerkkio</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/5543">vitest-dev/vitest#5543</a> <a href="https://github.com/vitest-dev/vitest/commit/31994942f"><!-- raw HTML omitted -->(31994)<!-- raw HTML omitted --></a>
<ul>
<li>⚠️ This change may cause significant differences in your coverage results compared to Vitest v1. These changes are expected as coverage reporting is now more accurate. See <a href="https://redirect.github.com/vitest-dev/vitest/issues/5423">vitest-dev/vitest#5423</a> for more details.</li>
</ul>
</li>
<li>Add correct location and snapshot fields in json reporter - by <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/5434">vitest-dev/vitest#5434</a> <a href="https://github.com/vitest-dev/vitest/commit/bcccce6df"><!-- raw HTML omitted -->(bcccc)<!-- raw HTML omitted --></a>
<ul>
<li>Previously, the <code>location</code> field pointed to the error location instead of the test location. Now it is aligned with jest and contains the <code>line</code> and <code>column</code> of a test function, but requires <a href="https://vitest.dev/config/#includeTaskLocation"><code>includeTaskLocation</code></a> to be enabled.</li>
</ul>
</li>
<li>Update dependency chai to v5 - by <strong>renovate[bot]</strong> and <a href="https://github.com/sheremet-va"><code>@​sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/5135">vitest-dev/vitest#5135</a> <a href="https://github.com/vitest-dev/vitest/commit/73646b638"><!-- raw HTML omitted -->(73646)<!-- raw HTML omitted --></a></li>
<li>Remove watchExclude - by <a href="https://github.com/patak-dev"><code>@​patak-dev</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/5177">vitest-dev/vitest#5177</a> <a href="https://github.com/vitest-dev/vitest/commit/d7371eae3"><!-- raw HTML omitted -->(d7371)<!-- raw HTML omitted --></a></li>
<li>Change default <code>pool</code> to <code>'forks'</code> - by <a href="https://github.com/AriPerkkio"><code>@​AriPerkkio</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/5047">vitest-dev/vitest#5047</a> <a href="https://github.com/vitest-dev/vitest/commit/7f8f9c0c0"><!-- raw HTML omitted -->(7f8f9)<!-- raw HTML omitted --></a>
<ul>
<li>This change is done for compatibility issues</li>
<li>This pool may be slightly slower than previous <code>threads</code> pool: <a href="https://vitest.dev/guide/improving-performance.html#pool">https://vitest.dev/guide/improving-performance.html#pool</a></li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/vitest-dev/vitest/commit/99a12aec2bb3baf361bf15afb2a9df9b0938518b"><code>99a12ae</code></a> chore: release v2.0.2</li>
<li><a href="https://github.com/vitest-dev/vitest/commit/80a43d5c7c1c541d3dcc0996d411fc39ecbb2e35"><code>80a43d5</code></a> fix(browser): inline pretty-format and replace picocolors with tinyrainbow (#...</li>
<li><a href="https://github.com/vitest-dev/vitest/commit/7012f8c12a3d0004e7e7651bbeee905ac9102098"><code>7012f8c</code></a> docs: add env documentation (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/6063">#6063</a>)</li>
<li><a href="https://github.com/vitest-dev/vitest/commit/16eb6c83f84b8f5ed06a625c8ad517e281112c5f"><code>16eb6c8</code></a> chore: release v2.0.1</li>
<li><a href="https://github.com/vitest-dev/vitest/commit/4b03e72b9afc2140e6d6b84f0e1d4e369de887d0"><code>4b03e72</code></a> fix(browser): correctly inherit browser config in a workspace (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/6054">#6054</a>)</li>
<li><a href="https://github.com/vitest-dev/vitest/commit/4d5597df7b343a444329d9f789fbb3a4b22d5cbc"><code>4d5597d</code></a> fix(vitest): print only running files, not every file (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/6052">#6052</a>)</li>
<li><a href="https://github.com/vitest-dev/vitest/commit/1b150a38918d157242c9259d6b1138e79fc8ed44"><code>1b150a3</code></a> chore: release v2.0.0</li>
<li><a href="https://github.com/vitest-dev/vitest/commit/51319929e5e25069ce57f15f8a4cdccb314b65ad"><code>5131992</code></a> docs: default pool in the types of 2.0 config (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/6043">#6043</a>)</li>
<li><a href="https://github.com/vitest-dev/vitest/commit/56118954e62b71ff9b53e096616511f9ecadc1ee"><code>5611895</code></a> chore: release v2.0.0-beta.13</li>
<li><a href="https://github.com/vitest-dev/vitest/commit/8f65ae906775562ecd244775cc77cface65d50ff"><code>8f65ae9</code></a> fix: transpile esnext to node18 to support newest JS and TS features (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/4409">#4409</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/vitest-dev/vitest/commits/v2.0.2/packages/vitest">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vitest&package-manager=npm_and_yarn&previous-version=1.6.0&new-version=2.0.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *docs-kargo-akuity-io* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | a9e8ad20eb6f11189a880f31636753fb42466ca6 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/docs-kargo-akuity-io/deploys/66936142492118000804e20d |

Patch:
diff --git a/api/v1alpha1/freight_types.go b/api/v1alpha1/freight_types.go
index d143c1d1..704bb071 100644
--- a/api/v1alpha1/freight_types.go
+++ b/api/v1alpha1/freight_types.go
@@ -120,7 +120,7 @@ type GitCommit struct {
 	// resolved to this commit.
 	Tag string `json:"tag,omitempty" protobuf:"bytes,4,opt,name=tag"`
 	// HealthCheckCommit is the ID of a specific commit. When specified,
-	// assessments of Stage health will used this value (instead of ID) when
+	// assessments of Stage health will use this value (instead of ID) when
 	// determining if applicable sources of Argo CD Application resources
 	// associated with the Stage are or are not synced to this commit. Note that
 	// there are cases (as in that of Kargo Render being utilized as a promotion
@@ -136,6 +136,37 @@ type GitCommit struct {
 	Committer string `json:"committer,omitempty" protobuf:"bytes,8,opt,name=committer"`
 }
 
+// DeepEquals returns a bool indicating whether the receiver deep-equals the
+// provided GitCommit. I.e., all fields must be equal.
+func (g *GitCommit) DeepEquals(other *GitCommit) bool {
+	if g == nil && other == nil {
+		return true
+	}
+	if g == nil || other == nil {
+		return false
+	}
+	return g.RepoURL == other.RepoURL &&
+		g.ID == other.ID &&
+		g.Branch == other.Branch &&
+		g.Tag == other.Tag &&
+		g.HealthCheckCommit == other.HealthCheckCommit &&
+		g.Message == other.Message &&
+		g.Author == other.Author &&
+		g.Committer == other.Committer
+}
+
+// Equals returns a bool indicating whether two GitCommits are equivalent.
+func (g *GitCommit) Equals(rhs *GitCommit) bool {
+	if g == nil && rhs == nil {
+		return true
+	}
+	if (g == nil && rhs != nil) || (g != nil && rhs == nil) {
+		return false
+	}
+	// If we get to here, both operands are non-nil
+	return g.RepoURL == rhs.RepoURL && g.ID == rhs.ID
+}
+
 // FreightStatus describes a piece of Freight's most recently observed state.
 type FreightStatus struct {
 	// VerifiedIn describes the Stages in which this Freight has been verified
diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto
index 1c9bbc86..e67dbf27 100644
--- a/api/v1alpha1/generated.proto
+++ b/api/v1alpha1/generated.proto
@@ -485,7 +485,7 @@ message GitCommit {
   optional string tag = 4;
 
   // HealthCheckCommit is the ID of a specific commit. When specified,
-  // assessments of Stage health will used this value (instead of ID) when
+  // assessments of Stage health will use this value (instead of ID) when
   // determining if applicable sources of Argo CD Application resources
   // associated with the Stage are or are not synced to this commit. Note that
   // there are cases (as in that of Kargo Render being utilized as a promotion
diff --git a/api/v1alpha1/stage_types.go b/api/v1alpha1/stage_types.go
index a2cb8bb1..c05e2d4f 100644
--- a/api/v1alpha1/stage_types.go
+++ b/api/v1alpha1/stage_types.go
@@ -613,6 +613,21 @@ type Image struct {
 	Digest string `json:"digest,omitempty" protobuf:"bytes,4,opt,name=digest"`
 }
 
+// DeepEquals returns a bool indicating whether the receiver deep-equals the
+// provided Image. I.e., all fields must be equal.
+func (i *Image) DeepEquals(other *Image) bool {
+	if i == nil && other == nil {
+		return true
+	}
+	if i == nil || other == nil {
+		return false
+	}
+	return i.RepoURL == other.RepoURL &&
+		i.GitRepoURL == other.GitRepoURL &&
+		i.Tag == other.Tag &&
+		i.Digest == other.Digest
+}
+
 // Chart describes a specific version of a Helm chart.
 type Chart struct {
 	// RepoURL specifies the URL of a Helm chart repository. Classic chart
@@ -628,16 +643,18 @@ type Chart struct {
 	Version string `json:"version,omitempty" protobuf:"bytes,3,opt,name=version"`
 }
 
-// Equals returns a bool indicating whether two GitCommits are equivalent.
-func (g *GitCommit) Equals(rhs *GitCommit) bool {
-	if g == nil && rhs == nil {
+// DeepEquals returns a bool indicating whether the receiver deep-equals the
+// provided Chart. I.e., all fields must be equal.
+func (c *Chart) DeepEquals(other *Chart) bool {
+	if c == nil && other == nil {
 		return true
 	}
-	if (g == nil && rhs != nil) || (g != nil && rhs == nil) {
+	if c == nil || other == nil {
 		return false
 	}
-	// If we get to here, both operands are non-nil
-	return g.RepoURL == rhs.RepoURL && g.ID == rhs.ID
+	return c.RepoURL == other.RepoURL &&
+		c.Name == other.Name &&
+		c.Version == other.Version
 }
 
 // Health describes the health of a Stage.
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml b/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml
index 11d2f167..5526ab12 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_freights.yaml
@@ -86,7 +86,7 @@ spec:
                 healthCheckCommit:
                   description: |-
                     HealthCheckCommit is the ID of a specific commit. When specified,
-                    assessments of Stage health will used this value (instead of ID) when
+                    assessments of Stage health will use this value (instead of ID) when
                     determining if applicable sources of Argo CD Application resources
                     associated with the Stage are or are not synced to this commit. Note that
                     there are cases (as in that of Kargo Render being utilized as a promotion
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
index 9e781d3e..4234489e 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_promotions.yaml
@@ -136,7 +136,7 @@ spec:
                         healthCheckCommit:
                           description: |-
                             HealthCheckCommit is the ID of a specific commit. When specified,
-                            assessments of Stage health will used this value (instead of ID) when
+                            assessments of Stage health will use this value (instead of ID) when
                             determining if applicable sources of Argo CD Application resources
                             associated with the Stage are or are not synced to this commit. Note that
                             there are cases (as in that of Kargo Render being utilized as a promotion
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
index a6ac7de5..7110827b 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
@@ -608,7 +608,7 @@ spec:
                         healthCheckCommit:
                           description: |-
                             HealthCheckCommit is the ID of a specific commit. When specified,
-                            assessments of Stage health will used this value (instead of ID) when
+                            assessments of Stage health will use this value (instead of ID) when
                             determining if applicable sources of Argo CD Application resources
                             associated with the Stage are or are not synced to this commit. Note that
                             there are cases (as in that of Kargo Render being utilized as a promotion
@@ -849,7 +849,7 @@ spec:
                             healthCheckCommit:
                               description: |-
                                 HealthCheckCommit is the ID of a specific commit. When specified,
-                                assessments of Stage health will used this value (instead of ID) when
+                                assessments of Stage health will use this value (instead of ID) when
                                 determining if applicable sources of Argo CD Application resources
                                 associated with the Stage are or are not synced to this commit. Note that
                                 there are cases (as in that of Kargo Render being utilized as a promotion
@@ -1096,7 +1096,7 @@ spec:
                                 healthCheckCommit:
                                   description: |-
                                     HealthCheckCommit is the ID of a specific commit. When specified,
-                                    assessments of Stage health will used this value (instead of ID) when
+                                    assessments of Stage health will use this value (instead of ID) when
                                     determining if applicable sources of Argo CD Application resources
                                     associated with the Stage are or are not synced to this commit. Note that
                                     there are cases (as in that of Kargo Render being utilized as a promotion
@@ -1424,7 +1424,7 @@ spec:
                           healthCheckCommit:
                             description: |-
                               HealthCheckCommit is the ID of a specific commit. When specified,
-                              assessments of Stage health will used this value (instead of ID) when
+                              assessments of Stage health will use this value (instead of ID) when
                               determining if applicable sources of Argo CD Application resources
                               associated with the Stage are or are not synced to this commit. Note that
                               there are cases (as in that of Kargo Render being utilized as a promotion
@@ -1672,7 +1672,7 @@ spec:
                             healthCheckCommit:
                               description: |-
                                 HealthCheckCommit is the ID of a specific commit. When specified,
-                                assessments of Stage health will used this value (instead of ID) when
+                                assessments of Stage health will use this value (instead of ID) when
                                 determining if applicable sources of Argo CD Application resources
                                 associated with the Stage are or are not synced to this commit. Note that
                                 there are cases (as in that of Kargo Render being utilized as a promotion
@@ -1919,7 +1919,7 @@ spec:
                                 healthCheckCommit:
                                   description: |-
                                     HealthCheckCommit is the ID of a specific commit. When specified,
-                                    assessments of Stage health will used this value (instead of ID) when
+                                    assessments of Stage health will use this value (instead of ID) when
                                     determining if applicable sources of Argo CD Application resources
                                     associated with the Stage are or are not synced to this commit. Note that
                                     there are cases (as in that of Kargo Render being utilized as a promotion
diff --git a/go.mod b/go.mod
index f23c7105..06a446bb 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	connectrpc.com/grpchealth v1.3.0
 	github.com/AlecAivazis/survey/v2 v2.3.7
 	github.com/Masterminds/semver/v3 v3.2.1
-	github.com/adrg/xdg v0.4.0
+	github.com/adrg/xdg v0.5.0
 	github.com/aws/aws-sdk-go-v2 v1.30.1
 	github.com/aws/aws-sdk-go-v2/config v1.27.24
 	github.com/aws/aws-sdk-go-v2/credentials v1.17.24
@@ -17,13 +17,13 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sts v1.30.1
 	github.com/bacongobbler/browser v1.1.0
 	github.com/bombsimon/logrusr/v4 v4.1.0
-	github.com/coreos/go-oidc/v3 v3.10.0
+	github.com/coreos/go-oidc/v3 v3.11.0
 	github.com/evanphx/json-patch/v5 v5.9.0
 	github.com/fatih/structtag v1.2.0
 	github.com/gobwas/glob v0.2.3
 	github.com/gogo/protobuf v1.3.2
 	github.com/golang-jwt/jwt/v5 v5.2.1
-	github.com/google/go-containerregistry v0.19.2
+	github.com/google/go-containerregistry v0.20.0
 	github.com/google/uuid v1.6.0
 	github.com/hashicorp/go-cleanhttp v0.5.2
 	github.com/jferrl/go-githubauth v1.0.2
@@ -42,7 +42,7 @@ require (
 	golang.org/x/net v0.27.0
 	golang.org/x/oauth2 v0.21.0
 	golang.org/x/sync v0.7.0
-	google.golang.org/api v0.187.0
+	google.golang.org/api v0.188.0
 	google.golang.org/grpc v1.65.0
 	google.golang.org/protobuf v1.34.2
 	gopkg.in/yaml.v3 v3.0.1
@@ -59,7 +59,7 @@ require (
 )
 
 require (
-	cloud.google.com/go/auth v0.6.1 // indirect
+	cloud.google.com/go/auth v0.7.0 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 // indirect
@@ -75,11 +75,11 @@ require (
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.5 // indirect
 	go.opencensus.io v0.24.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
 )
 
 require (
-	cloud.google.com/go/compute/metadata v0.4.0
+	cloud.google.com/go/compute/metadata v0.5.0
 	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
 	github.com/Microsoft/hcsshim v0.11.4 // indirect
@@ -104,7 +104,7 @@ require (
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/go-errors/errors v1.4.2 // indirect
-	github.com/go-jose/go-jose/v4 v4.0.1 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.2 // indirect
 	github.com/go-logr/logr v1.4.2
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.19.6 // indirect
@@ -164,7 +164,7 @@ require (
 	golang.org/x/text v0.16.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect
 	gopkg.in/evanphx/json-patch.v5 v5.6.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 7af738cc..3de27091 100644
--- a/go.sum
+++ b/go.sum
@@ -1,10 +1,10 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38=
-cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4=
+cloud.google.com/go/auth v0.7.0 h1:kf/x9B3WTbBUHkC+1VS8wwwli9TzhSt0vSTVBmMR8Ts=
+cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw=
 cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
 cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
-cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c=
-cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M=
+cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
 connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE=
 connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc=
 connectrpc.com/grpchealth v1.3.0 h1:FA3OIwAvuMokQIXQrY5LbIy8IenftksTP/lG4PbYN+E=
@@ -27,8 +27,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
-github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
-github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
+github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
+github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/aws/aws-sdk-go-v2 v1.30.1 h1:4y/5Dvfrhd1MxRDD77SrfsDaj8kUkkljU7XE83NPV+o=
@@ -95,8 +95,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
 github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
 github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
 github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
-github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
-github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
+github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
+github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -146,8 +146,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
 github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
-github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
-github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
+github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
+github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@@ -208,8 +208,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w=
-github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
+github.com/google/go-containerregistry v0.20.0 h1:wRqHpOeVh3DnenOrPy9xDOLdnLatiGuuNRVelR2gSbg=
+github.com/google/go-containerregistry v0.20.0/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
 github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4=
 github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0=
 github.com/google/go-github/v59 v59.0.0 h1:7h6bgpF5as0YQLLkEiVqpgtJqjimMYhBkD4jT5aN3VA=
@@ -489,7 +489,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -528,17 +527,17 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
 gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
-google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo=
-google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk=
+google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
+google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc=
-google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d h1:k3zyW3BYYR30e8v3x0bTDdE9vpYFjZHK+HcyqkrppWk=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b h1:04+jVzTs2XBnOZcPsLnmrTGqltqJbZQ1Ey26hjYdQQ0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
diff --git a/internal/webhook/freight/webhook.go b/internal/webhook/freight/webhook.go
index af920e6c..57e0fdee 100644
--- a/internal/webhook/freight/webhook.go
+++ b/internal/webhook/freight/webhook.go
@@ -116,17 +116,20 @@ func newWebhook(
 
 func (w *webhook) Default(ctx context.Context, obj runtime.Object) error {
 	freight := obj.(*kargoapi.Freight) // nolint: forcetypeassert
-	// Re-calculate ID in case it wasn't set correctly to begin with -- possible
-	// if/when we allow users to create their own Freight.
-	freight.Name = freight.GenerateID()
 
-	// Sync the convenience alias field with the alias label
 	req, err := admission.RequestFromContext(ctx)
 	if err != nil {
 		return apierrors.NewInternalError(
 			fmt.Errorf("error getting admission request from context: %w", err),
 		)
 	}
+	if req.Operation == admissionv1.Create {
+		// Re-calculate ID in case it wasn't set correctly to begin with -- possible
+		// when users create their own Freight.
+		freight.Name = freight.GenerateID()
+	}
+
+	// Sync the convenience alias field with the alias label
 	if freight.Labels == nil {
 		freight.Labels = make(map[string]string, 1)
 	}
@@ -252,18 +255,16 @@ func (w *webhook) ValidateUpdate(
 		)
 	}
 
-	// Freight is meant to be immutable. We only need to compare the Name to a
-	// newly generated ID because these are both fingerprints that are
-	// deterministically derived from the artifacts referenced by the Freight.
-	if newFreight.Name != newFreight.GenerateID() || oldFreight.Warehouse != newFreight.Warehouse {
+	// Freight is meant to be immutable.
+	if changedPath, change, ok := compareFreight(oldFreight, newFreight); !ok {
 		return nil, apierrors.NewInvalid(
 			freightGroupKind,
 			oldFreight.Name,
 			field.ErrorList{
 				field.Invalid(
-					field.NewPath(""),
-					oldFreight,
-					"freight is immutable",
+					changedPath,
+					change,
+					"Freight is immutable",
 				),
 			},
 		)
@@ -501,3 +502,41 @@ func validateFreightArtifacts(
 
 	return nil
 }
+
+// compareFreight compares two Freight objects and returns the first field path
+// that differs between them, the new value, and a boolean indicating whether
+// the two Freight objects are equal.
+func compareFreight(old, new *kargoapi.Freight) (*field.Path, any, bool) {
+	if old.Warehouse != new.Warehouse {
+		return field.NewPath("warehouse"), new.Warehouse, false
+	}
+
+	if len(old.Commits) != len(new.Commits) {
+		return field.NewPath("commits"), new.Commits, false
+	}
+	for i, commit := range old.Commits {
+		if !commit.DeepEquals(&new.Commits[i]) {
+			return field.NewPath("commits").Index(i), new.Commits[i], false
+		}
+	}
+
+	if len(old.Images) != len(new.Images) {
+		return field.NewPath("images"), new.Images, false
+	}
+	for i, image := range old.Images {
+		if !image.DeepEquals(&new.Images[i]) {
+			return field.NewPath("images").Index(i), new.Images[i], false
+		}
+	}
+
+	if len(old.Charts) != len(new.Charts) {
+		return field.NewPath("charts"), new.Charts, false
+	}
+	for i, chart := range old.Charts {
+		if !chart.DeepEquals(&new.Charts[i]) {
+			return field.NewPath("charts").Index(i), new.Charts[i], false
+		}
+	}
+
+	return nil, nil, true
+}
diff --git a/ui/package.json b/ui/package.json
index 5e38b738..77934038 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -44,7 +44,7 @@
     "vite": "^5.3.3",
     "vite-plugin-compression": "^0.5.1",
     "vite-tsconfig-paths": "^4.3.2",
-    "vitest": "^1.6.0",
+    "vitest": "^2.0.2",
     "zx": "^8.1.4"
   },
   "packageManager": "pnpm@9.0.3",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 82d60d7b..fa1cace5 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -184,8 +184,8 @@ importers:
         specifier: ^4.3.2
         version: 4.3.2(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(less@4.2.0))
       vitest:
-        specifier: ^1.6.0
-        version: 1.6.0(@types/node@20.14.10)(less@4.2.0)
+        specifier: ^2.0.2
+        version: 2.0.2(@types/node@20.14.10)(less@4.2.0)
       zx:
         specifier: ^8.1.4
         version: 8.1.4
@@ -575,10 +575,6 @@ packages:
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
 
-  '@jest/schemas@29.6.3':
-    resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
   '@jridgewell/gen-mapping@0.3.5':
     resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
     engines: {node: '>=6.0.0'}
@@ -591,8 +587,8 @@ packages:
     resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
     engines: {node: '>=6.0.0'}
 
-  '@jridgewell/sourcemap-codec@1.4.15':
-    resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
+  '@jridgewell/sourcemap-codec@1.5.0':
+    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
 
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
@@ -773,9 +769,6 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@sinclair/typebox@0.27.8':
-    resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
-
   '@tanstack/query-core@5.50.1':
     resolution: {integrity: sha512-lpfhKPrJlyV2DSVcQb/HuozH3Av3kws4ge22agx+lNGpFkS4vLZ7St0l3GLwlAD+bqB+qXGex3JdRKUNtMviEQ==}
 
@@ -930,30 +923,29 @@ packages:
     peerDependencies:
       vite: ^4.2.0 || ^5.0.0
 
-  '@vitest/expect@1.6.0':
-    resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==}
+  '@vitest/expect@2.0.2':
+    resolution: {integrity: sha512-nKAvxBYqcDugYZ4nJvnm5OR8eDJdgWjk4XM9owQKUjzW70q0icGV2HVnQOyYsp906xJaBDUXw0+9EHw2T8e0mQ==}
+
+  '@vitest/pretty-format@2.0.2':
+    resolution: {integrity: sha512-SBCyOXfGVvddRd9r2PwoVR0fonQjh9BMIcBMlSzbcNwFfGr6ZhOhvBzurjvi2F4ryut2HcqiFhNeDVGwru8tLg==}
 
-  '@vitest/runner@1.6.0':
-    resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==}
+  '@vitest/runner@2.0.2':
+    resolution: {integrity: sha512-OCh437Vi8Wdbif1e0OvQcbfM3sW4s2lpmOjAE7qfLrpzJX2M7J1IQlNvEcb/fu6kaIB9n9n35wS0G2Q3en5kHg==}
 
-  '@vitest/snapshot@1.6.0':
-    resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==}
+  '@vitest/snapshot@2.0.2':
+    resolution: {integrity: sha512-Yc2ewhhZhx+0f9cSUdfzPRcsM6PhIb+S43wxE7OG0kTxqgqzo8tHkXFuFlndXeDMp09G3sY/X5OAo/RfYydf1g==}
 
-  '@vitest/spy@1.6.0':
-    resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==}
+  '@vitest/spy@2.0.2':
+    resolution: {integrity: sha512-MgwJ4AZtCgqyp2d7WcQVE8aNG5vQ9zu9qMPYQHjsld/QVsrvg78beNrXdO4HYkP0lDahCO3P4F27aagIag+SGQ==}
 
-  '@vitest/utils@1.6.0':
-    resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==}
+  '@vitest/utils@2.0.2':
+    resolution: {integrity: sha512-pxCY1v7kmOCWYWjzc0zfjGTA3Wmn8PKnlPvSrsA643P1NHl1fOyXj2Q9SaNlrlFE+ivCsxM80Ov3AR82RmHCWQ==}
 
   acorn-jsx@5.3.2:
     resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
     peerDependencies:
       acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
 
-  acorn-walk@8.3.3:
-    resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==}
-    engines: {node: '>=0.4.0'}
-
   acorn@8.12.0:
     resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==}
     engines: {node: '>=0.4.0'}
@@ -978,10 +970,6 @@ packages:
     resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
     engines: {node: '>=8'}
 
-  ansi-styles@5.2.0:
-    resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
-    engines: {node: '>=10'}
-
   ansi-styles@6.2.1:
     resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
     engines: {node: '>=12'}
@@ -1058,8 +1046,9 @@ packages:
     resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==}
     engines: {node: '>= 0.4'}
 
-  assertion-error@1.1.0:
-    resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
+  assertion-error@2.0.1:
+    resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
+    engines: {node: '>=12'}
 
   autoprefixer@10.4.19:
     resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==}
@@ -1131,9 +1120,9 @@ packages:
   caniuse-lite@1.0.30001640:
     resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==}
 
-  chai@4.4.1:
-    resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
-    engines: {node: '>=4'}
+  chai@5.1.1:
+    resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==}
+    engines: {node: '>=12'}
 
   chalk@2.4.2:
     resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
@@ -1143,8 +1132,9 @@ packages:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
     engines: {node: '>=10'}
 
-  check-error@1.0.3:
-    resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
+  check-error@2.1.1:
+    resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
+    engines: {node: '>= 16'}
 
   chokidar@3.6.0:
     resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
@@ -1194,9 +1184,6 @@ packages:
   concat-map@0.0.1:
     resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
 
-  confbox@0.1.7:
-    resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==}
-
   convert-source-map@2.0.0:
     resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
 
@@ -1355,8 +1342,8 @@ packages:
   decimal.js-light@2.5.1:
     resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
 
-  deep-eql@4.1.4:
-    resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
+  deep-eql@5.0.2:
+    resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
     engines: {node: '>=6'}
 
   deep-is@0.1.4:
@@ -1373,10 +1360,6 @@ packages:
   didyoumean@1.2.2:
     resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
 
-  diff-sequences@29.6.3:
-    resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
   dir-glob@3.0.1:
     resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
     engines: {node: '>=8'}
@@ -1960,9 +1943,6 @@ packages:
   js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
-  js-tokens@9.0.0:
-    resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==}
-
   js-yaml@4.1.0:
     resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
     hasBin: true
@@ -2033,10 +2013,6 @@ packages:
   lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
-  local-pkg@0.5.0:
-    resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
-    engines: {node: '>=14'}
-
   locate-path@6.0.0:
     resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
     engines: {node: '>=10'}
@@ -2057,8 +2033,8 @@ packages:
     resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
     hasBin: true
 
-  loupe@2.3.7:
-    resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
+  loupe@3.1.1:
+    resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==}
 
   lru-cache@10.3.0:
     resolution: {integrity: sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==}
@@ -2124,9 +2100,6 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
-  mlly@1.7.1:
-    resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==}
-
   moment@2.30.1:
     resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
 
@@ -2262,10 +2235,6 @@ packages:
     resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
     engines: {node: '>=10'}
 
-  p-limit@5.0.0:
-    resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
-    engines: {node: '>=18'}
-
   p-locate@5.0.0:
     resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
     engines: {node: '>=10'}
@@ -2310,8 +2279,9 @@ packages:
   pathe@1.1.2:
     resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
 
-  pathval@1.1.1:
-    resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
+  pathval@2.0.0:
+    resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
+    engines: {node: '>= 14.16'}
 
   picocolors@1.0.1:
     resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
@@ -2332,9 +2302,6 @@ packages:
     resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
     engines: {node: '>= 6'}
 
-  pkg-types@1.1.1:
-    resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==}
-
   possible-typed-array-names@1.0.0:
     resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
     engines: {node: '>= 0.4'}
@@ -2560,10 +2527,6 @@ packages:
     engines: {node: '>=14'}
     hasBin: true
 
-  pretty-format@29.7.0:
-    resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
-    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
-
   prop-types@15.8.1:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
@@ -3062,9 +3025,6 @@ packages:
     resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
     engines: {node: '>=8'}
 
-  strip-literal@2.1.0:
-    resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==}
-
   stylehacks@7.0.2:
     resolution: {integrity: sha512-HdkWZS9b4gbgYTdMg4gJLmm7biAUug1qTqXjS+u8X+/pUd+9Px1E+520GnOW3rST9MNsVOVpsJG+mPHNosxjOQ==}
     engines: {node: ^18.12.0 || ^20.9.0 || >=22.0}
@@ -3128,12 +3088,16 @@ packages:
   tinybench@2.8.0:
     resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==}
 
-  tinypool@0.8.4:
-    resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==}
+  tinypool@1.0.0:
+    resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+
+  tinyrainbow@1.2.0:
+    resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
     engines: {node: '>=14.0.0'}
 
-  tinyspy@2.2.1:
-    resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
+  tinyspy@3.0.0:
+    resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==}
     engines: {node: '>=14.0.0'}
 
   to-fast-properties@2.0.0:
@@ -3179,10 +3143,6 @@ packages:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
     engines: {node: '>= 0.8.0'}
 
-  type-detect@4.0.8:
-    resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
-    engines: {node: '>=4'}
-
   typed-array-buffer@1.0.0:
     resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==}
     engines: {node: '>= 0.4'}
@@ -3219,9 +3179,6 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
-  ufo@1.5.3:
-    resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
-
   unbox-primitive@1.0.2:
     resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
 
@@ -3257,8 +3214,8 @@ packages:
   victory-vendor@36.9.2:
     resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
 
-  vite-node@1.6.0:
-    resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
+  vite-node@2.0.2:
+    resolution: {integrity: sha512-w4vkSz1Wo+NIQg8pjlEn0jQbcM/0D+xVaYjhw3cvarTanLLBh54oNiRbsT8PNK5GfuST0IlVXjsNRoNlqvY/fw==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
 
@@ -3308,15 +3265,15 @@ packages:
       terser:
         optional: true
 
-  vitest@1.6.0:
-    resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
+  vitest@2.0.2:
+    resolution: {integrity: sha512-WlpZ9neRIjNBIOQwBYfBSr0+of5ZCbxT2TVGKW4Lv0c8+srCFIiRdsP7U009t8mMn821HQ4XKgkx5dVWpyoyLw==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:
       '@edge-runtime/vm': '*'
       '@types/node': ^18.0.0 || >=20.0.0
-      '@vitest/browser': 1.6.0
-      '@vitest/ui': 1.6.0
+      '@vitest/browser': 2.0.2
+      '@vitest/ui': 2.0.2
       happy-dom: '*'
       jsdom: '*'
     peerDependenciesMeta:
@@ -3373,8 +3330,8 @@ packages:
     engines: {node: '>= 8'}
     hasBin: true
 
-  why-is-node-running@2.2.2:
-    resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==}
+  why-is-node-running@2.3.0:
+    resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
     engines: {node: '>=8'}
     hasBin: true
 
@@ -3414,10 +3371,6 @@ packages:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
 
-  yocto-queue@1.0.0:
-    resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
-    engines: {node: '>=12.20'}
-
   zod@3.23.8:
     resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
 
@@ -3790,26 +3743,22 @@ snapshots:
       wrap-ansi: 8.1.0
       wrap-ansi-cjs: wrap-ansi@7.0.0
 
-  '@jest/schemas@29.6.3':
-    dependencies:
-      '@sinclair/typebox': 0.27.8
-
   '@jridgewell/gen-mapping@0.3.5':
     dependencies:
       '@jridgewell/set-array': 1.2.1
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
       '@jridgewell/trace-mapping': 0.3.25
 
   '@jridgewell/resolve-uri@3.1.2': {}
 
   '@jridgewell/set-array@1.2.1': {}
 
-  '@jridgewell/sourcemap-codec@1.4.15': {}
+  '@jridgewell/sourcemap-codec@1.5.0': {}
 
   '@jridgewell/trace-mapping@0.3.25':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
 
   '@monaco-editor/loader@1.4.0(monaco-editor@0.50.0)':
     dependencies:
@@ -3969,8 +3918,6 @@ snapshots:
   '@rollup/rollup-win32-x64-msvc@4.18.1':
     optional: true
 
-  '@sinclair/typebox@0.27.8': {}
-
   '@tanstack/query-core@5.50.1': {}
 
   '@tanstack/react-query@5.50.1(react@18.3.1)':
@@ -4164,43 +4111,43 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@vitest/expect@1.6.0':
+  '@vitest/expect@2.0.2':
+    dependencies:
+      '@vitest/spy': 2.0.2
+      '@vitest/utils': 2.0.2
+      chai: 5.1.1
+      tinyrainbow: 1.2.0
+
+  '@vitest/pretty-format@2.0.2':
     dependencies:
-      '@vitest/spy': 1.6.0
-      '@vitest/utils': 1.6.0
-      chai: 4.4.1
+      tinyrainbow: 1.2.0
 
-  '@vitest/runner@1.6.0':
+  '@vitest/runner@2.0.2':
     dependencies:
-      '@vitest/utils': 1.6.0
-      p-limit: 5.0.0
+      '@vitest/utils': 2.0.2
       pathe: 1.1.2
 
-  '@vitest/snapshot@1.6.0':
+  '@vitest/snapshot@2.0.2':
     dependencies:
+      '@vitest/pretty-format': 2.0.2
       magic-string: 0.30.10
       pathe: 1.1.2
-      pretty-format: 29.7.0
 
-  '@vitest/spy@1.6.0':
+  '@vitest/spy@2.0.2':
     dependencies:
-      tinyspy: 2.2.1
+      tinyspy: 3.0.0
 
-  '@vitest/utils@1.6.0':
+  '@vitest/utils@2.0.2':
     dependencies:
-      diff-sequences: 29.6.3
+      '@vitest/pretty-format': 2.0.2
       estree-walker: 3.0.3
-      loupe: 2.3.7
-      pretty-format: 29.7.0
+      loupe: 3.1.1
+      tinyrainbow: 1.2.0
 
   acorn-jsx@5.3.2(acorn@8.12.0):
     dependencies:
       acorn: 8.12.0
 
-  acorn-walk@8.3.3:
-    dependencies:
-      acorn: 8.12.0
-
   acorn@8.12.0: {}
 
   ajv@6.12.6:
@@ -4222,8 +4169,6 @@ snapshots:
     dependencies:
       color-convert: 2.0.1
 
-  ansi-styles@5.2.0: {}
-
   ansi-styles@6.2.1: {}
 
   antd@5.19.1(date-fns@3.6.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@@ -4392,7 +4337,7 @@ snapshots:
       is-array-buffer: 3.0.4
       is-shared-array-buffer: 1.0.3
 
-  assertion-error@1.1.0: {}
+  assertion-error@2.0.1: {}
 
   autoprefixer@10.4.19(postcss@8.4.39):
     dependencies:
@@ -4468,15 +4413,13 @@ snapshots:
 
   caniuse-lite@1.0.30001640: {}
 
-  chai@4.4.1:
+  chai@5.1.1:
     dependencies:
-      assertion-error: 1.1.0
-      check-error: 1.0.3
-      deep-eql: 4.1.4
-      get-func-name: 2.0.2
-      loupe: 2.3.7
-      pathval: 1.1.1
-      type-detect: 4.0.8
+      assertion-error: 2.0.1
+      check-error: 2.1.1
+      deep-eql: 5.0.2
+      loupe: 3.1.1
+      pathval: 2.0.0
 
   chalk@2.4.2:
     dependencies:
@@ -4489,9 +4432,7 @@ snapshots:
       ansi-styles: 4.3.0
       supports-color: 7.2.0
 
-  check-error@1.0.3:
-    dependencies:
-      get-func-name: 2.0.2
+  check-error@2.1.1: {}
 
   chokidar@3.6.0:
     dependencies:
@@ -4539,8 +4480,6 @@ snapshots:
 
   concat-map@0.0.1: {}
 
-  confbox@0.1.7: {}
-
   convert-source-map@2.0.0: {}
 
   copy-anything@2.0.6:
@@ -4712,9 +4651,7 @@ snapshots:
 
   decimal.js-light@2.5.1: {}
 
-  deep-eql@4.1.4:
-    dependencies:
-      type-detect: 4.0.8
+  deep-eql@5.0.2: {}
 
   deep-is@0.1.4: {}
 
@@ -4732,8 +4669,6 @@ snapshots:
 
   didyoumean@1.2.2: {}
 
-  diff-sequences@29.6.3: {}
-
   dir-glob@3.0.1:
     dependencies:
       path-type: 4.0.0
@@ -5468,8 +5403,6 @@ snapshots:
 
   js-tokens@4.0.0: {}
 
-  js-tokens@9.0.0: {}
-
   js-yaml@4.1.0:
     dependencies:
       argparse: 2.0.1
@@ -5545,11 +5478,6 @@ snapshots:
 
   lines-and-columns@1.2.4: {}
 
-  local-pkg@0.5.0:
-    dependencies:
-      mlly: 1.7.1
-      pkg-types: 1.1.1
-
   locate-path@6.0.0:
     dependencies:
       p-locate: 5.0.0
@@ -5566,7 +5494,7 @@ snapshots:
     dependencies:
       js-tokens: 4.0.0
 
-  loupe@2.3.7:
+  loupe@3.1.1:
     dependencies:
       get-func-name: 2.0.2
 
@@ -5578,7 +5506,7 @@ snapshots:
 
   magic-string@0.30.10:
     dependencies:
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
 
   make-dir@2.1.0:
     dependencies:
@@ -5620,13 +5548,6 @@ snapshots:
 
   mkdirp@3.0.1: {}
 
-  mlly@1.7.1:
-    dependencies:
-      acorn: 8.12.0
-      pathe: 1.1.2
-      pkg-types: 1.1.1
-      ufo: 1.5.3
-
   moment@2.30.1: {}
 
   monaco-editor@0.50.0: {}
@@ -5785,10 +5706,6 @@ snapshots:
     dependencies:
       yocto-queue: 0.1.0
 
-  p-limit@5.0.0:
-    dependencies:
-      yocto-queue: 1.0.0
-
   p-locate@5.0.0:
     dependencies:
       p-limit: 3.1.0
@@ -5820,7 +5737,7 @@ snapshots:
 
   pathe@1.1.2: {}
 
-  pathval@1.1.1: {}
+  pathval@2.0.0: {}
 
   picocolors@1.0.1: {}
 
@@ -5833,12 +5750,6 @@ snapshots:
 
   pirates@4.0.6: {}
 
-  pkg-types@1.1.1:
-    dependencies:
-      confbox: 0.1.7
-      mlly: 1.7.1
-      pathe: 1.1.2
-
   possible-typed-array-names@1.0.0: {}
 
   postcss-calc@10.0.0(postcss@8.4.39):
@@ -6037,12 +5948,6 @@ snapshots:
 
   prettier@3.3.2: {}
 
-  pretty-format@29.7.0:
-    dependencies:
-      '@jest/schemas': 29.6.3
-      ansi-styles: 5.2.0
-      react-is: 18.3.1
-
   prop-types@15.8.1:
     dependencies:
       loose-envify: 1.4.0
@@ -6689,10 +6594,6 @@ snapshots:
 
   strip-json-comments@3.1.1: {}
 
-  strip-literal@2.1.0:
-    dependencies:
-      js-tokens: 9.0.0
-
   stylehacks@7.0.2(postcss@8.4.39):
     dependencies:
       browserslist: 4.23.1
@@ -6784,9 +6685,11 @@ snapshots:
 
   tinybench@2.8.0: {}
 
-  tinypool@0.8.4: {}
+  tinypool@1.0.0: {}
+
+  tinyrainbow@1.2.0: {}
 
-  tinyspy@2.2.1: {}
+  tinyspy@3.0.0: {}
 
   to-fast-properties@2.0.0: {}
 
@@ -6824,8 +6727,6 @@ snapshots:
     dependencies:
       prelude-ls: 1.2.1
 
-  type-detect@4.0.8: {}
-
   typed-array-buffer@1.0.0:
     dependencies:
       call-bind: 1.0.7
@@ -6887,8 +6788,6 @@ snapshots:
 
   typescript@5.5.3: {}
 
-  ufo@1.5.3: {}
-
   unbox-primitive@1.0.2:
     dependencies:
       call-bind: 1.0.7
@@ -6939,12 +6838,12 @@ snapshots:
       d3-time: 3.1.0
       d3-timer: 3.0.1
 
-  vite-node@1.6.0(@types/node@20.14.10)(less@4.2.0):
+  vite-node@2.0.2(@types/node@20.14.10)(less@4.2.0):
     dependencies:
       cac: 6.7.14
       debug: 4.3.5
       pathe: 1.1.2
-      picocolors: 1.0.1
+      tinyrainbow: 1.2.0
       vite: 5.3.3(@types/node@20.14.10)(less@4.2.0)
     transitivePeerDependencies:
       - '@types/node'
@@ -6990,28 +6889,27 @@ snapshots:
       fsevents: 2.3.3
       less: 4.2.0
 
-  vitest@1.6.0(@types/node@20.14.10)(less@4.2.0):
+  vitest@2.0.2(@types/node@20.14.10)(less@4.2.0):
     dependencies:
-      '@vitest/expect': 1.6.0
-      '@vitest/runner': 1.6.0
-      '@vitest/snapshot': 1.6.0
-      '@vitest/spy': 1.6.0
-      '@vitest/utils': 1.6.0
-      acorn-walk: 8.3.3
-      chai: 4.4.1
+      '@ampproject/remapping': 2.3.0
+      '@vitest/expect': 2.0.2
+      '@vitest/pretty-format': 2.0.2
+      '@vitest/runner': 2.0.2
+      '@vitest/snapshot': 2.0.2
+      '@vitest/spy': 2.0.2
+      '@vitest/utils': 2.0.2
+      chai: 5.1.1
       debug: 4.3.5
       execa: 8.0.1
-      local-pkg: 0.5.0
       magic-string: 0.30.10
       pathe: 1.1.2
-      picocolors: 1.0.1
       std-env: 3.7.0
-      strip-literal: 2.1.0
       tinybench: 2.8.0
-      tinypool: 0.8.4
+      tinypool: 1.0.0
+      tinyrainbow: 1.2.0
       vite: 5.3.3(@types/node@20.14.10)(less@4.2.0)
-      vite-node: 1.6.0(@types/node@20.14.10)(less@4.2.0)
-      why-is-node-running: 2.2.2
+      vite-node: 2.0.2(@types/node@20.14.10)(less@4.2.0)
+      why-is-node-running: 2.3.0
     optionalDependencies:
       '@types/node': 20.14.10
     transitivePeerDependencies:
@@ -7086,7 +6984,7 @@ snapshots:
     dependencies:
       isexe: 2.0.0
 
-  why-is-node-running@2.2.2:
+  why-is-node-running@2.3.0:
     dependencies:
       siginfo: 2.0.0
       stackback: 0.0.2
@@ -7125,8 +7023,6 @@ snapshots:
 
   yocto-queue@0.1.0: {}
 
-  yocto-queue@1.0.0: {}
-
   zod@3.23.8: {}
 
   zx@8.1.4:
diff --git a/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json
index 17f8cf84..cb162ed8 100644
--- a/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/freights.kargo.akuity.io_v1alpha1.json
@@ -50,7 +50,7 @@
             "type": "string"
           },
           "healthCheckCommit": {
-            "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will used this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
+            "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will use this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
             "type": "string"
           },
           "id": {
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 f8e8136c..e833419a 100644
--- a/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/promotions.kargo.akuity.io_v1alpha1.json
@@ -85,7 +85,7 @@
                     "type": "string"
                   },
                   "healthCheckCommit": {
-                    "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will used this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
+                    "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will use this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
                     "type": "string"
                   },
                   "id": {
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 3c4d2c9a..36472a68 100644
--- a/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/stages.kargo.akuity.io_v1alpha1.json
@@ -495,7 +495,7 @@
                     "type": "string"
                   },
                   "healthCheckCommit": {
-                    "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will used this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
+                    "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will use this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
                     "type": "string"
                   },
                   "id": {
@@ -721,7 +721,7 @@
                         "type": "string"
                       },
                       "healthCheckCommit": {
-                        "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will used this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
+                        "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will use this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
                         "type": "string"
                       },
                       "id": {
@@ -951,7 +951,7 @@
                             "type": "string"
                           },
                           "healthCheckCommit": {
-                            "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will used this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
+                            "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will use this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
                             "type": "string"
                           },
                           "id": {
@@ -1275,7 +1275,7 @@
                       "type": "string"
                     },
                     "healthCheckCommit": {
-                      "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will used this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
+                      "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will use this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
                       "type": "string"
                     },
                     "id": {
@@ -1507,7 +1507,7 @@
                         "type": "string"
                       },
                       "healthCheckCommit": {
-                        "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will used this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
+                        "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will use this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
                         "type": "string"
                       },
                       "id": {
@@ -1737,7 +1737,7 @@
                             "type": "string"
                           },
                           "healthCheckCommit": {
-                            "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will used this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
+                            "description": "HealthCheckCommit is the ID of a specific commit. When specified,\nassessments of Stage health will use this value (instead of ID) when\ndetermining if applicable sources of Argo CD Application resources\nassociated with the Stage are or are not synced to this commit. Note that\nthere are cases (as in that of Kargo Render being utilized as a promotion\nmechanism) wherein the value of this field may differ from the commit ID\nfound in the ID field.",
                             "type": "string"
                           },
                           "id": {
diff --git a/ui/src/gen/v1alpha1/generated_pb.ts b/ui/src/gen/v1alpha1/generated_pb.ts
index bc6c2d52..dfe92c0b 100644
--- a/ui/src/gen/v1alpha1/generated_pb.ts
+++ b/ui/src/gen/v1alpha1/generated_pb.ts
@@ -1589,7 +1589,7 @@ export class GitCommit extends Message<GitCommit> {
 
   /**
    * HealthCheckCommit is the ID of a specific commit. When specified,
-   * assessments of Stage health will used this value (instead of ID) when
+   * assessments of Stage health will use this value (instead of ID) when
    * determining if applicable sources of Argo CD Application resources
    * associated with the Stage are or are not synced to this commit. Note that
    * there are cases (as in that of Kargo Render being utilized as a promotion


Test patch:
diff --git a/api/v1alpha1/freight_types_test.go b/api/v1alpha1/freight_types_test.go
index 2901e761..a71c94d5 100644
--- a/api/v1alpha1/freight_types_test.go
+++ b/api/v1alpha1/freight_types_test.go
@@ -6,6 +6,167 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func TestGitCommitDeepEquals(t *testing.T) {
+	testCases := []struct {
+		name           string
+		a              *GitCommit
+		b              *GitCommit
+		expectedResult bool
+	}{
+		{
+			name:           "a and b both nil",
+			expectedResult: true,
+		},
+		{
+			name:           "only a is nil",
+			b:              &GitCommit{},
+			expectedResult: false,
+		},
+		{
+			name:           "only b is nil",
+			a:              &GitCommit{},
+			expectedResult: false,
+		},
+		{
+			name: "repoURLs differ",
+			a: &GitCommit{
+				RepoURL: "foo",
+			},
+			b: &GitCommit{
+				RepoURL: "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "commit IDs differ",
+			a: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "foo",
+			},
+			b: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "branch names differ",
+			a: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "fake-commit-id",
+				Branch:  "foo",
+			},
+			b: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "fake-commit-id",
+				Branch:  "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "tags differ",
+			a: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "fake-commit-id",
+				Tag:     "foo",
+			},
+			b: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "fake-commit-id",
+				Tag:     "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "health check commits differ",
+			a: &GitCommit{
+				RepoURL:           "fake-url",
+				ID:                "fake-commit-id",
+				HealthCheckCommit: "foo",
+			},
+			b: &GitCommit{
+				RepoURL:           "fake-url",
+				ID:                "fake-commit-id",
+				HealthCheckCommit: "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "messages differ",
+			a: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "fake-commit-id",
+				Message: "foo",
+			},
+			b: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "fake-commit-id",
+				Message: "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "authors differ",
+			a: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "fake-commit-id",
+				Author:  "foo",
+			},
+			b: &GitCommit{
+				RepoURL: "fake-url",
+				ID:      "fake-commit-id",
+				Author:  "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "committers differ",
+			a: &GitCommit{
+				RepoURL:   "fake-url",
+				ID:        "fake-commit-id",
+				Committer: "foo",
+			},
+			b: &GitCommit{
+				RepoURL:   "fake-url",
+				ID:        "fake-commit-id",
+				Committer: "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "perfect match",
+			a: &GitCommit{
+				RepoURL:           "fake-url",
+				ID:                "fake-commit-id",
+				Branch:            "fake-branch",
+				Tag:               "fake-tag",
+				HealthCheckCommit: "fake-health-id",
+				Message:           "fake-message",
+				Author:            "fake-author",
+				Committer:         "fake-committer",
+			},
+			b: &GitCommit{
+				RepoURL:           "fake-url",
+				ID:                "fake-commit-id",
+				Branch:            "fake-branch",
+				Tag:               "fake-tag",
+				HealthCheckCommit: "fake-health-id",
+				Message:           "fake-message",
+				Author:            "fake-author",
+				Committer:         "fake-committer",
+			},
+			expectedResult: true,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			require.Equal(t, testCase.expectedResult, testCase.a.DeepEquals(testCase.b))
+			require.Equal(t, testCase.expectedResult, testCase.b.DeepEquals(testCase.a))
+		})
+	}
+}
+
 func TestGitCommitEquals(t *testing.T) {
 	testCases := []struct {
 		name           string
diff --git a/api/v1alpha1/stage_types_test.go b/api/v1alpha1/stage_types_test.go
index ea4612d8..7bbae43e 100644
--- a/api/v1alpha1/stage_types_test.go
+++ b/api/v1alpha1/stage_types_test.go
@@ -224,3 +224,177 @@ func TestVerificationInfoStack_UpdateOrPush(t *testing.T) {
 		})
 	}
 }
+
+func TestImageDeepEquals(t *testing.T) {
+	testCases := []struct {
+		name           string
+		a              *Image
+		b              *Image
+		expectedResult bool
+	}{
+		{
+			name:           "a and b both nil",
+			expectedResult: true,
+		},
+		{
+			name:           "only a is nil",
+			b:              &Image{},
+			expectedResult: false,
+		},
+		{
+			name:           "only b is nil",
+			a:              &Image{},
+			expectedResult: false,
+		},
+		{
+			name: "repo URLs differ",
+			a: &Image{
+				RepoURL: "foo",
+			},
+			b: &Image{
+				RepoURL: "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "git repo URLs differ",
+			a: &Image{
+				RepoURL:    "fake-url",
+				GitRepoURL: "foo",
+			},
+			b: &Image{
+				RepoURL:    "fake-url",
+				GitRepoURL: "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "image tags differ",
+			a: &Image{
+				RepoURL: "fake-url",
+				Tag:     "foo",
+			},
+			b: &Image{
+				RepoURL: "fake-url",
+				Tag:     "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "image digests differ",
+			a: &Image{
+				RepoURL: "fake-url",
+				Digest:  "foo",
+			},
+			b: &Image{
+				RepoURL: "fake-url",
+				Digest:  "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "perfect match",
+			a: &Image{
+				RepoURL:    "fake-url",
+				GitRepoURL: "fake-repo-url",
+				Tag:        "fake-tag",
+				Digest:     "fake-digest",
+			},
+			b: &Image{
+				RepoURL:    "fake-url",
+				GitRepoURL: "fake-repo-url",
+				Tag:        "fake-tag",
+				Digest:     "fake-digest",
+			},
+			expectedResult: true,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			require.Equal(t, testCase.expectedResult, testCase.a.DeepEquals(testCase.b))
+			require.Equal(t, testCase.expectedResult, testCase.b.DeepEquals(testCase.a))
+		})
+	}
+}
+
+func TestChartDeepEquals(t *testing.T) {
+	testCases := []struct {
+		name           string
+		a              *Chart
+		b              *Chart
+		expectedResult bool
+	}{
+		{
+			name:           "a and b both nil",
+			expectedResult: true,
+		},
+		{
+			name:           "only a is nil",
+			b:              &Chart{},
+			expectedResult: false,
+		},
+		{
+			name:           "only b is nil",
+			a:              &Chart{},
+			expectedResult: false,
+		},
+		{
+			name: "repo URLs differ",
+			a: &Chart{
+				RepoURL: "foo",
+			},
+			b: &Chart{
+				RepoURL: "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "chart names differ",
+			a: &Chart{
+				RepoURL: "fake-url",
+				Name:    "foo",
+			},
+			b: &Chart{
+				RepoURL: "fake-url",
+				Name:    "bar",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "chart versions differ",
+			a: &Chart{
+				RepoURL: "fake-url",
+				Name:    "fake-name",
+				Version: "v1.0.0",
+			},
+			b: &Chart{
+				RepoURL: "fake-url",
+				Name:    "fake-name",
+				Version: "v2.0.0",
+			},
+			expectedResult: false,
+		},
+		{
+			name: "perfect match",
+			a: &Chart{
+				RepoURL: "fake-url",
+				Name:    "fake-name",
+				Version: "v1.0.0",
+			},
+			b: &Chart{
+				RepoURL: "fake-url",
+				Name:    "fake-name",
+				Version: "v1.0.0",
+			},
+			expectedResult: true,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			require.Equal(t, testCase.expectedResult, testCase.a.DeepEquals(testCase.b))
+			require.Equal(t, testCase.expectedResult, testCase.b.DeepEquals(testCase.a))
+		})
+	}
+}
diff --git a/internal/webhook/freight/webhook_test.go b/internal/webhook/freight/webhook_test.go
index 3d095035..bfbcb9c4 100644
--- a/internal/webhook/freight/webhook_test.go
+++ b/internal/webhook/freight/webhook_test.go
@@ -16,6 +16,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/validation/field"
 	"k8s.io/apiserver/pkg/authentication/serviceaccount"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -105,6 +106,18 @@ func TestDefault(t *testing.T) {
 				require.Equal(t, "fake-alias", freight.Labels[kargoapi.AliasLabelKey])
 			},
 		},
+		{
+			name:    "create with empty name",
+			op:      admissionv1.Create,
+			webhook: &webhook{},
+			freight: &kargoapi.Freight{
+				Alias: "fake-alias",
+			},
+			assertions: func(t *testing.T, freight *kargoapi.Freight, err error) {
+				require.NoError(t, err)
+				require.NotEmpty(t, freight.Name)
+			},
+		},
 		{
 			name:    "update with empty alias",
 			op:      admissionv1.Update,
@@ -118,7 +131,6 @@ func TestDefault(t *testing.T) {
 			},
 			assertions: func(t *testing.T, freight *kargoapi.Freight, err error) {
 				require.NoError(t, err)
-				require.NotEmpty(t, freight.Name)
 				require.Empty(t, freight.Alias)
 				_, ok := freight.Labels[kargoapi.AliasLabelKey]
 				require.False(t, ok)
@@ -532,7 +544,7 @@ func TestValidateUpdate(t *testing.T) {
 			},
 			assertions: func(t *testing.T, _ *fakeevent.EventRecorder, err error) {
 				require.ErrorContains(t, err, "is invalid")
-				require.ErrorContains(t, err, "freight is immutable")
+				require.ErrorContains(t, err, "Freight is immutable")
 			},
 		},
 
@@ -561,7 +573,7 @@ func TestValidateUpdate(t *testing.T) {
 			},
 			assertions: func(t *testing.T, _ *fakeevent.EventRecorder, err error) {
 				require.ErrorContains(t, err, "is invalid")
-				require.ErrorContains(t, err, "freight is immutable")
+				require.ErrorContains(t, err, "Freight is immutable")
 			},
 		},
 		{
@@ -1034,3 +1046,108 @@ func TestValidateFreightArtifacts(t *testing.T) {
 		})
 	}
 }
+
+func TestCompareFreight(t *testing.T) {
+	tests := []struct {
+		name       string
+		old        *kargoapi.Freight
+		new        *kargoapi.Freight
+		assertions func(*testing.T, *kargoapi.Freight, *field.Path, any, bool)
+	}{
+		{
+			name: "Equal Freights",
+			old: &kargoapi.Freight{
+				Warehouse: "warehouse1",
+				Commits:   []kargoapi.GitCommit{{ID: "commit1"}}, Images: []kargoapi.Image{{RepoURL: "image1"}},
+				Charts: []kargoapi.Chart{{Name: "chart1"}},
+			},
+			new: &kargoapi.Freight{
+				Warehouse: "warehouse1",
+				Commits:   []kargoapi.GitCommit{{ID: "commit1"}},
+				Images:    []kargoapi.Image{{RepoURL: "image1"}}, Charts: []kargoapi.Chart{{Name: "chart1"}},
+			},
+			assertions: func(t *testing.T, _ *kargoapi.Freight, path *field.Path, val any, eq bool) {
+				require.Nil(t, path)
+				require.Nil(t, val)
+				require.True(t, eq)
+			},
+		},
+		{
+			name: "different Warehouse",
+			old:  &kargoapi.Freight{Warehouse: "warehouse1"},
+			new:  &kargoapi.Freight{Warehouse: "warehouse2"},
+			assertions: func(t *testing.T, freight *kargoapi.Freight, path *field.Path, val any, eq bool) {
+				require.Equal(t, field.NewPath("warehouse"), path)
+				require.Equal(t, freight.Warehouse, val)
+				require.False(t, eq)
+			},
+		},
+		{
+			name: "different number of commits",
+			old:  &kargoapi.Freight{Commits: []kargoapi.GitCommit{{ID: "commit1"}}},
+			new:  &kargoapi.Freight{Commits: []kargoapi.GitCommit{{ID: "commit1"}, {ID: "commit2"}}},
+			assertions: func(t *testing.T, freight *kargoapi.Freight, path *field.Path, val any, eq bool) {
+				require.Equal(t, field.NewPath("commits"), path)
+				require.Equal(t, freight.Commits, val)
+				require.False(t, eq)
+			},
+		},
+		{
+			name: "different commit contents",
+			old:  &kargoapi.Freight{Commits: []kargoapi.GitCommit{{ID: "commit1"}, {ID: "commit2"}}},
+			new:  &kargoapi.Freight{Commits: []kargoapi.GitCommit{{ID: "commit1"}, {ID: "commit3"}}},
+			assertions: func(t *testing.T, freight *kargoapi.Freight, path *field.Path, val any, eq bool) {
+				require.Equal(t, field.NewPath("commits").Index(1), path)
+				require.Equal(t, freight.Commits[1], val)
+				require.False(t, eq)
+			},
+		},
+		{
+			name: "different number of images",
+			old:  &kargoapi.Freight{Images: []kargoapi.Image{{RepoURL: "image1"}}},
+			new:  &kargoapi.Freight{Images: []kargoapi.Image{{RepoURL: "image1"}, {RepoURL: "image2"}}},
+			assertions: func(t *testing.T, freight *kargoapi.Freight, path *field.Path, val any, eq bool) {
+				require.Equal(t, field.NewPath("images"), path)
+				require.Equal(t, freight.Images, val)
+				require.False(t, eq)
+			},
+		},
+		{
+			name: "different image contents",
+			old:  &kargoapi.Freight{Images: []kargoapi.Image{{RepoURL: "image1"}}},
+			new:  &kargoapi.Freight{Images: []kargoapi.Image{{RepoURL: "image2"}}},
+			assertions: func(t *testing.T, freight *kargoapi.Freight, path *field.Path, val any, eq bool) {
+				require.Equal(t, field.NewPath("images").Index(0), path)
+				require.Equal(t, freight.Images[0], val)
+				require.False(t, eq)
+			},
+		},
+		{
+			name: "different number of charts",
+			old:  &kargoapi.Freight{Charts: []kargoapi.Chart{{Name: "chart1"}}},
+			new:  &kargoapi.Freight{Charts: []kargoapi.Chart{{Name: "chart1"}, {Name: "chart2"}}},
+			assertions: func(t *testing.T, freight *kargoapi.Freight, path *field.Path, val any, eq bool) {
+				require.Equal(t, field.NewPath("charts"), path)
+				require.Equal(t, freight.Charts, val)
+				require.False(t, eq)
+			},
+		},
+		{
+			name: "different chart contents",
+			old:  &kargoapi.Freight{Charts: []kargoapi.Chart{{Name: "chart1"}, {Name: "chart2"}, {Name: "chart3"}}},
+			new:  &kargoapi.Freight{Charts: []kargoapi.Chart{{Name: "chart1"}, {Name: "chart2"}, {Name: "chart4"}}},
+			assertions: func(t *testing.T, freight *kargoapi.Freight, path *field.Path, val any, eq bool) {
+				require.Equal(t, field.NewPath("charts").Index(2), path)
+				require.Equal(t, freight.Charts[2], val)
+				require.False(t, eq)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			path, val, eq := compareFreight(tt.old, tt.new)
+			tt.assertions(t, tt.new, path, val, eq)
+		})
+	}
+}