Repository: akuity/kargo

Issue:
Promotion / create PR feature for Bitbucket server
We are currently evaluating Kargo and we would like Kargo to have a feature to open up Pull requests to Bitbucket server for our prod staging promotion process. We see that Kargo has a feature to support GitHub and GitLab, but not BB server. Is that some thing that can be added in the future releases , that will enable us to get going with Kargo.


Discussion:
@krancour: This isn't a high priority for the maintainers at the moment, but we would fully support this. It's an issue of medium difficulty and we would welcome someone from the community to pick it up and run with it.@chamodshehanka: I would like to take this, and thinking to use this https://github.com/ktrysmt/go-bitbucket (if maintainers are agree) or BitBucket API @krancour: No objection to that package.@chamodshehanka: > krancour

Awesome. I'll start working on it@dspereira004: > > krancour
> 
> Awesome. I'll start working on it

Hey @chamodshehanka, any news regarding the development of the Bitbucket PR functionality? Do you still plan to do it?@chamodshehanka: > dspereira004

yes. Can I open a draft PR?@dspereira004: > > dspereira004
> 
> yes. Can I open a draft PR?

That would be awesome! But, for more context: we are also evaluating Kargo for our company and our initial impression is good. However, we really need Bitbucket PR support, which is why I was asking you if you'll add support for it.@chamodshehanka: > > > dspereira004
> > 
> > 
> > yes. Can I open a draft PR?
> 
> That would be awesome! But, for more context: we are also evaluating Kargo for our company and our initial impression is good. However, we really need Bitbucket PR support, which is why I was asking you if you'll add support for it.

that's great! we could work together then @krancour: Calling attention to #3128 which is an in-progress PR to add Azure DevOps support. It's a good example of how support for a new Git provider is added.@valkiriaaquatica: Hi  @krancour , after reading the issue I get for Azure Devops and Bitbucket is community made right? In my case is  Gitea integration, would it follow the same proccedure?

Thanks in advanced!@krancour: Yes. Support for any/all Git hosting providers all should implement the same interface.@github-actions: This issue has been automatically marked as stale because it had no activity for 90 days. It will be closed if no activity occurs in the next 30 days but can be reopened if it becomes relevant again.@krancour: Not stale. In progress in #3209.

Solution patch:
diff --git a/api/v1alpha1/generated.pb.go b/api/v1alpha1/generated.pb.go
index e954b3b9..c93016d1 100644
--- a/api/v1alpha1/generated.pb.go
+++ b/api/v1alpha1/generated.pb.go
@@ -898,10 +898,38 @@ func (m *HealthCheckStep) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_HealthCheckStep proto.InternalMessageInfo
 
+func (m *HealthStats) Reset()      { *m = HealthStats{} }
+func (*HealthStats) ProtoMessage() {}
+func (*HealthStats) Descriptor() ([]byte, []int) {
+	return fileDescriptor_e26b7f7bbc391025, []int{31}
+}
+func (m *HealthStats) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *HealthStats) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	b = b[:cap(b)]
+	n, err := m.MarshalToSizedBuffer(b)
+	if err != nil {
+		return nil, err
+	}
+	return b[:n], nil
+}
+func (m *HealthStats) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_HealthStats.Merge(m, src)
+}
+func (m *HealthStats) XXX_Size() int {
+	return m.Size()
+}
+func (m *HealthStats) XXX_DiscardUnknown() {
+	xxx_messageInfo_HealthStats.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_HealthStats proto.InternalMessageInfo
+
 func (m *Image) Reset()      { *m = Image{} }
 func (*Image) ProtoMessage() {}
 func (*Image) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{31}
+	return fileDescriptor_e26b7f7bbc391025, []int{32}
 }
 func (m *Image) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -929,7 +957,7 @@ var xxx_messageInfo_Image proto.InternalMessageInfo
 func (m *ImageDiscoveryResult) Reset()      { *m = ImageDiscoveryResult{} }
 func (*ImageDiscoveryResult) ProtoMessage() {}
 func (*ImageDiscoveryResult) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{32}
+	return fileDescriptor_e26b7f7bbc391025, []int{33}
 }
 func (m *ImageDiscoveryResult) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -957,7 +985,7 @@ var xxx_messageInfo_ImageDiscoveryResult proto.InternalMessageInfo
 func (m *ImageSubscription) Reset()      { *m = ImageSubscription{} }
 func (*ImageSubscription) ProtoMessage() {}
 func (*ImageSubscription) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{33}
+	return fileDescriptor_e26b7f7bbc391025, []int{34}
 }
 func (m *ImageSubscription) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -985,7 +1013,7 @@ var xxx_messageInfo_ImageSubscription proto.InternalMessageInfo
 func (m *Project) Reset()      { *m = Project{} }
 func (*Project) ProtoMessage() {}
 func (*Project) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{34}
+	return fileDescriptor_e26b7f7bbc391025, []int{35}
 }
 func (m *Project) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1013,7 +1041,7 @@ var xxx_messageInfo_Project proto.InternalMessageInfo
 func (m *ProjectConfig) Reset()      { *m = ProjectConfig{} }
 func (*ProjectConfig) ProtoMessage() {}
 func (*ProjectConfig) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{35}
+	return fileDescriptor_e26b7f7bbc391025, []int{36}
 }
 func (m *ProjectConfig) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1041,7 +1069,7 @@ var xxx_messageInfo_ProjectConfig proto.InternalMessageInfo
 func (m *ProjectConfigList) Reset()      { *m = ProjectConfigList{} }
 func (*ProjectConfigList) ProtoMessage() {}
 func (*ProjectConfigList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{36}
+	return fileDescriptor_e26b7f7bbc391025, []int{37}
 }
 func (m *ProjectConfigList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1069,7 +1097,7 @@ var xxx_messageInfo_ProjectConfigList proto.InternalMessageInfo
 func (m *ProjectConfigSpec) Reset()      { *m = ProjectConfigSpec{} }
 func (*ProjectConfigSpec) ProtoMessage() {}
 func (*ProjectConfigSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{37}
+	return fileDescriptor_e26b7f7bbc391025, []int{38}
 }
 func (m *ProjectConfigSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1097,7 +1125,7 @@ var xxx_messageInfo_ProjectConfigSpec proto.InternalMessageInfo
 func (m *ProjectList) Reset()      { *m = ProjectList{} }
 func (*ProjectList) ProtoMessage() {}
 func (*ProjectList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{38}
+	return fileDescriptor_e26b7f7bbc391025, []int{39}
 }
 func (m *ProjectList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1122,10 +1150,38 @@ func (m *ProjectList) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_ProjectList proto.InternalMessageInfo
 
+func (m *ProjectStats) Reset()      { *m = ProjectStats{} }
+func (*ProjectStats) ProtoMessage() {}
+func (*ProjectStats) Descriptor() ([]byte, []int) {
+	return fileDescriptor_e26b7f7bbc391025, []int{40}
+}
+func (m *ProjectStats) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *ProjectStats) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	b = b[:cap(b)]
+	n, err := m.MarshalToSizedBuffer(b)
+	if err != nil {
+		return nil, err
+	}
+	return b[:n], nil
+}
+func (m *ProjectStats) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ProjectStats.Merge(m, src)
+}
+func (m *ProjectStats) XXX_Size() int {
+	return m.Size()
+}
+func (m *ProjectStats) XXX_DiscardUnknown() {
+	xxx_messageInfo_ProjectStats.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ProjectStats proto.InternalMessageInfo
+
 func (m *ProjectStatus) Reset()      { *m = ProjectStatus{} }
 func (*ProjectStatus) ProtoMessage() {}
 func (*ProjectStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{39}
+	return fileDescriptor_e26b7f7bbc391025, []int{41}
 }
 func (m *ProjectStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1153,7 +1209,7 @@ var xxx_messageInfo_ProjectStatus proto.InternalMessageInfo
 func (m *Promotion) Reset()      { *m = Promotion{} }
 func (*Promotion) ProtoMessage() {}
 func (*Promotion) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{40}
+	return fileDescriptor_e26b7f7bbc391025, []int{42}
 }
 func (m *Promotion) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1181,7 +1237,7 @@ var xxx_messageInfo_Promotion proto.InternalMessageInfo
 func (m *PromotionList) Reset()      { *m = PromotionList{} }
 func (*PromotionList) ProtoMessage() {}
 func (*PromotionList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{41}
+	return fileDescriptor_e26b7f7bbc391025, []int{43}
 }
 func (m *PromotionList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1209,7 +1265,7 @@ var xxx_messageInfo_PromotionList proto.InternalMessageInfo
 func (m *PromotionPolicy) Reset()      { *m = PromotionPolicy{} }
 func (*PromotionPolicy) ProtoMessage() {}
 func (*PromotionPolicy) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{42}
+	return fileDescriptor_e26b7f7bbc391025, []int{44}
 }
 func (m *PromotionPolicy) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1237,7 +1293,7 @@ var xxx_messageInfo_PromotionPolicy proto.InternalMessageInfo
 func (m *PromotionReference) Reset()      { *m = PromotionReference{} }
 func (*PromotionReference) ProtoMessage() {}
 func (*PromotionReference) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{43}
+	return fileDescriptor_e26b7f7bbc391025, []int{45}
 }
 func (m *PromotionReference) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1265,7 +1321,7 @@ var xxx_messageInfo_PromotionReference proto.InternalMessageInfo
 func (m *PromotionSpec) Reset()      { *m = PromotionSpec{} }
 func (*PromotionSpec) ProtoMessage() {}
 func (*PromotionSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{44}
+	return fileDescriptor_e26b7f7bbc391025, []int{46}
 }
 func (m *PromotionSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1293,7 +1349,7 @@ var xxx_messageInfo_PromotionSpec proto.InternalMessageInfo
 func (m *PromotionStatus) Reset()      { *m = PromotionStatus{} }
 func (*PromotionStatus) ProtoMessage() {}
 func (*PromotionStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{45}
+	return fileDescriptor_e26b7f7bbc391025, []int{47}
 }
 func (m *PromotionStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1321,7 +1377,7 @@ var xxx_messageInfo_PromotionStatus proto.InternalMessageInfo
 func (m *PromotionStep) Reset()      { *m = PromotionStep{} }
 func (*PromotionStep) ProtoMessage() {}
 func (*PromotionStep) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{46}
+	return fileDescriptor_e26b7f7bbc391025, []int{48}
 }
 func (m *PromotionStep) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1349,7 +1405,7 @@ var xxx_messageInfo_PromotionStep proto.InternalMessageInfo
 func (m *PromotionStepRetry) Reset()      { *m = PromotionStepRetry{} }
 func (*PromotionStepRetry) ProtoMessage() {}
 func (*PromotionStepRetry) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{47}
+	return fileDescriptor_e26b7f7bbc391025, []int{49}
 }
 func (m *PromotionStepRetry) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1377,7 +1433,7 @@ var xxx_messageInfo_PromotionStepRetry proto.InternalMessageInfo
 func (m *PromotionTask) Reset()      { *m = PromotionTask{} }
 func (*PromotionTask) ProtoMessage() {}
 func (*PromotionTask) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{48}
+	return fileDescriptor_e26b7f7bbc391025, []int{50}
 }
 func (m *PromotionTask) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1405,7 +1461,7 @@ var xxx_messageInfo_PromotionTask proto.InternalMessageInfo
 func (m *PromotionTaskList) Reset()      { *m = PromotionTaskList{} }
 func (*PromotionTaskList) ProtoMessage() {}
 func (*PromotionTaskList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{49}
+	return fileDescriptor_e26b7f7bbc391025, []int{51}
 }
 func (m *PromotionTaskList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1433,7 +1489,7 @@ var xxx_messageInfo_PromotionTaskList proto.InternalMessageInfo
 func (m *PromotionTaskReference) Reset()      { *m = PromotionTaskReference{} }
 func (*PromotionTaskReference) ProtoMessage() {}
 func (*PromotionTaskReference) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{50}
+	return fileDescriptor_e26b7f7bbc391025, []int{52}
 }
 func (m *PromotionTaskReference) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1461,7 +1517,7 @@ var xxx_messageInfo_PromotionTaskReference proto.InternalMessageInfo
 func (m *PromotionTaskSpec) Reset()      { *m = PromotionTaskSpec{} }
 func (*PromotionTaskSpec) ProtoMessage() {}
 func (*PromotionTaskSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{51}
+	return fileDescriptor_e26b7f7bbc391025, []int{53}
 }
 func (m *PromotionTaskSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1489,7 +1545,7 @@ var xxx_messageInfo_PromotionTaskSpec proto.InternalMessageInfo
 func (m *PromotionTemplate) Reset()      { *m = PromotionTemplate{} }
 func (*PromotionTemplate) ProtoMessage() {}
 func (*PromotionTemplate) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{52}
+	return fileDescriptor_e26b7f7bbc391025, []int{54}
 }
 func (m *PromotionTemplate) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1517,7 +1573,7 @@ var xxx_messageInfo_PromotionTemplate proto.InternalMessageInfo
 func (m *PromotionTemplateSpec) Reset()      { *m = PromotionTemplateSpec{} }
 func (*PromotionTemplateSpec) ProtoMessage() {}
 func (*PromotionTemplateSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{53}
+	return fileDescriptor_e26b7f7bbc391025, []int{55}
 }
 func (m *PromotionTemplateSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1545,7 +1601,7 @@ var xxx_messageInfo_PromotionTemplateSpec proto.InternalMessageInfo
 func (m *RepoSubscription) Reset()      { *m = RepoSubscription{} }
 func (*RepoSubscription) ProtoMessage() {}
 func (*RepoSubscription) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{54}
+	return fileDescriptor_e26b7f7bbc391025, []int{56}
 }
 func (m *RepoSubscription) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1573,7 +1629,7 @@ var xxx_messageInfo_RepoSubscription proto.InternalMessageInfo
 func (m *Stage) Reset()      { *m = Stage{} }
 func (*Stage) ProtoMessage() {}
 func (*Stage) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{55}
+	return fileDescriptor_e26b7f7bbc391025, []int{57}
 }
 func (m *Stage) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1601,7 +1657,7 @@ var xxx_messageInfo_Stage proto.InternalMessageInfo
 func (m *StageList) Reset()      { *m = StageList{} }
 func (*StageList) ProtoMessage() {}
 func (*StageList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{56}
+	return fileDescriptor_e26b7f7bbc391025, []int{58}
 }
 func (m *StageList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1629,7 +1685,7 @@ var xxx_messageInfo_StageList proto.InternalMessageInfo
 func (m *StageSpec) Reset()      { *m = StageSpec{} }
 func (*StageSpec) ProtoMessage() {}
 func (*StageSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{57}
+	return fileDescriptor_e26b7f7bbc391025, []int{59}
 }
 func (m *StageSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1654,10 +1710,38 @@ func (m *StageSpec) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_StageSpec proto.InternalMessageInfo
 
+func (m *StageStats) Reset()      { *m = StageStats{} }
+func (*StageStats) ProtoMessage() {}
+func (*StageStats) Descriptor() ([]byte, []int) {
+	return fileDescriptor_e26b7f7bbc391025, []int{60}
+}
+func (m *StageStats) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *StageStats) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	b = b[:cap(b)]
+	n, err := m.MarshalToSizedBuffer(b)
+	if err != nil {
+		return nil, err
+	}
+	return b[:n], nil
+}
+func (m *StageStats) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_StageStats.Merge(m, src)
+}
+func (m *StageStats) XXX_Size() int {
+	return m.Size()
+}
+func (m *StageStats) XXX_DiscardUnknown() {
+	xxx_messageInfo_StageStats.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_StageStats proto.InternalMessageInfo
+
 func (m *StageStatus) Reset()      { *m = StageStatus{} }
 func (*StageStatus) ProtoMessage() {}
 func (*StageStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{58}
+	return fileDescriptor_e26b7f7bbc391025, []int{61}
 }
 func (m *StageStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1685,7 +1769,7 @@ var xxx_messageInfo_StageStatus proto.InternalMessageInfo
 func (m *StepExecutionMetadata) Reset()      { *m = StepExecutionMetadata{} }
 func (*StepExecutionMetadata) ProtoMessage() {}
 func (*StepExecutionMetadata) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{59}
+	return fileDescriptor_e26b7f7bbc391025, []int{62}
 }
 func (m *StepExecutionMetadata) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1713,7 +1797,7 @@ var xxx_messageInfo_StepExecutionMetadata proto.InternalMessageInfo
 func (m *Verification) Reset()      { *m = Verification{} }
 func (*Verification) ProtoMessage() {}
 func (*Verification) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{60}
+	return fileDescriptor_e26b7f7bbc391025, []int{63}
 }
 func (m *Verification) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1741,7 +1825,7 @@ var xxx_messageInfo_Verification proto.InternalMessageInfo
 func (m *VerificationInfo) Reset()      { *m = VerificationInfo{} }
 func (*VerificationInfo) ProtoMessage() {}
 func (*VerificationInfo) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{61}
+	return fileDescriptor_e26b7f7bbc391025, []int{64}
 }
 func (m *VerificationInfo) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1769,7 +1853,7 @@ var xxx_messageInfo_VerificationInfo proto.InternalMessageInfo
 func (m *VerifiedStage) Reset()      { *m = VerifiedStage{} }
 func (*VerifiedStage) ProtoMessage() {}
 func (*VerifiedStage) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{62}
+	return fileDescriptor_e26b7f7bbc391025, []int{65}
 }
 func (m *VerifiedStage) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1797,7 +1881,7 @@ var xxx_messageInfo_VerifiedStage proto.InternalMessageInfo
 func (m *Warehouse) Reset()      { *m = Warehouse{} }
 func (*Warehouse) ProtoMessage() {}
 func (*Warehouse) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{63}
+	return fileDescriptor_e26b7f7bbc391025, []int{66}
 }
 func (m *Warehouse) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1825,7 +1909,7 @@ var xxx_messageInfo_Warehouse proto.InternalMessageInfo
 func (m *WarehouseList) Reset()      { *m = WarehouseList{} }
 func (*WarehouseList) ProtoMessage() {}
 func (*WarehouseList) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{64}
+	return fileDescriptor_e26b7f7bbc391025, []int{67}
 }
 func (m *WarehouseList) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1853,7 +1937,7 @@ var xxx_messageInfo_WarehouseList proto.InternalMessageInfo
 func (m *WarehouseSpec) Reset()      { *m = WarehouseSpec{} }
 func (*WarehouseSpec) ProtoMessage() {}
 func (*WarehouseSpec) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{65}
+	return fileDescriptor_e26b7f7bbc391025, []int{68}
 }
 func (m *WarehouseSpec) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1878,10 +1962,38 @@ func (m *WarehouseSpec) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_WarehouseSpec proto.InternalMessageInfo
 
+func (m *WarehouseStats) Reset()      { *m = WarehouseStats{} }
+func (*WarehouseStats) ProtoMessage() {}
+func (*WarehouseStats) Descriptor() ([]byte, []int) {
+	return fileDescriptor_e26b7f7bbc391025, []int{69}
+}
+func (m *WarehouseStats) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *WarehouseStats) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	b = b[:cap(b)]
+	n, err := m.MarshalToSizedBuffer(b)
+	if err != nil {
+		return nil, err
+	}
+	return b[:n], nil
+}
+func (m *WarehouseStats) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_WarehouseStats.Merge(m, src)
+}
+func (m *WarehouseStats) XXX_Size() int {
+	return m.Size()
+}
+func (m *WarehouseStats) XXX_DiscardUnknown() {
+	xxx_messageInfo_WarehouseStats.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_WarehouseStats proto.InternalMessageInfo
+
 func (m *WarehouseStatus) Reset()      { *m = WarehouseStatus{} }
 func (*WarehouseStatus) ProtoMessage() {}
 func (*WarehouseStatus) Descriptor() ([]byte, []int) {
-	return fileDescriptor_e26b7f7bbc391025, []int{66}
+	return fileDescriptor_e26b7f7bbc391025, []int{70}
 }
 func (m *WarehouseStatus) XXX_Unmarshal(b []byte) error {
 	return m.Unmarshal(b)
@@ -1945,6 +2057,7 @@ func init() {
 	proto.RegisterType((*GitSubscription)(nil), "github.com.akuity.kargo.api.v1alpha1.GitSubscription")
 	proto.RegisterType((*Health)(nil), "github.com.akuity.kargo.api.v1alpha1.Health")
 	proto.RegisterType((*HealthCheckStep)(nil), "github.com.akuity.kargo.api.v1alpha1.HealthCheckStep")
+	proto.RegisterType((*HealthStats)(nil), "github.com.akuity.kargo.api.v1alpha1.HealthStats")
 	proto.RegisterType((*Image)(nil), "github.com.akuity.kargo.api.v1alpha1.Image")
 	proto.RegisterMapType((map[string]string)(nil), "github.com.akuity.kargo.api.v1alpha1.Image.AnnotationsEntry")
 	proto.RegisterType((*ImageDiscoveryResult)(nil), "github.com.akuity.kargo.api.v1alpha1.ImageDiscoveryResult")
@@ -1954,6 +2067,7 @@ func init() {
 	proto.RegisterType((*ProjectConfigList)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectConfigList")
 	proto.RegisterType((*ProjectConfigSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectConfigSpec")
 	proto.RegisterType((*ProjectList)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectList")
+	proto.RegisterType((*ProjectStats)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectStats")
 	proto.RegisterType((*ProjectStatus)(nil), "github.com.akuity.kargo.api.v1alpha1.ProjectStatus")
 	proto.RegisterType((*Promotion)(nil), "github.com.akuity.kargo.api.v1alpha1.Promotion")
 	proto.RegisterType((*PromotionList)(nil), "github.com.akuity.kargo.api.v1alpha1.PromotionList")
@@ -1973,6 +2087,7 @@ func init() {
 	proto.RegisterType((*Stage)(nil), "github.com.akuity.kargo.api.v1alpha1.Stage")
 	proto.RegisterType((*StageList)(nil), "github.com.akuity.kargo.api.v1alpha1.StageList")
 	proto.RegisterType((*StageSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.StageSpec")
+	proto.RegisterType((*StageStats)(nil), "github.com.akuity.kargo.api.v1alpha1.StageStats")
 	proto.RegisterType((*StageStatus)(nil), "github.com.akuity.kargo.api.v1alpha1.StageStatus")
 	proto.RegisterType((*StepExecutionMetadata)(nil), "github.com.akuity.kargo.api.v1alpha1.StepExecutionMetadata")
 	proto.RegisterType((*Verification)(nil), "github.com.akuity.kargo.api.v1alpha1.Verification")
@@ -1981,6 +2096,7 @@ func init() {
 	proto.RegisterType((*Warehouse)(nil), "github.com.akuity.kargo.api.v1alpha1.Warehouse")
 	proto.RegisterType((*WarehouseList)(nil), "github.com.akuity.kargo.api.v1alpha1.WarehouseList")
 	proto.RegisterType((*WarehouseSpec)(nil), "github.com.akuity.kargo.api.v1alpha1.WarehouseSpec")
+	proto.RegisterType((*WarehouseStats)(nil), "github.com.akuity.kargo.api.v1alpha1.WarehouseStats")
 	proto.RegisterType((*WarehouseStatus)(nil), "github.com.akuity.kargo.api.v1alpha1.WarehouseStatus")
 }
 
@@ -1989,270 +2105,278 @@ func init() {
 }
 
 var fileDescriptor_e26b7f7bbc391025 = []byte{
-	// 4195 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5c, 0x4d, 0x6c, 0x23, 0x47,
-	0x76, 0x9e, 0x26, 0x45, 0x4a, 0x7c, 0x94, 0x46, 0x52, 0x8d, 0xc6, 0xab, 0x95, 0x63, 0x69, 0xd2,
-	0x36, 0x0c, 0x3b, 0xb6, 0xa9, 0x8c, 0xc6, 0x63, 0x8f, 0xc7, 0xc6, 0x04, 0x22, 0x35, 0x3f, 0x9a,
-	0xd5, 0x7a, 0x94, 0xa2, 0x3c, 0x8e, 0xc7, 0x36, 0x26, 0x25, 0xb2, 0x44, 0xf6, 0x8a, 0x64, 0xd3,
-	0x5d, 0x45, 0xed, 0x28, 0x09, 0x92, 0xcd, 0x2f, 0x16, 0x09, 0x10, 0x2c, 0x90, 0x0d, 0xbc, 0x87,
-	0x04, 0x09, 0x92, 0xe3, 0x02, 0xb9, 0xe6, 0x90, 0x83, 0x0f, 0xb9, 0x38, 0xc9, 0x26, 0x30, 0x92,
-	0x00, 0x71, 0x80, 0x85, 0xb2, 0xd6, 0x02, 0x39, 0xe7, 0x92, 0xcb, 0x00, 0x01, 0x82, 0xfa, 0xe9,
-	0xee, 0xea, 0x66, 0x73, 0xc4, 0xa6, 0x7e, 0x30, 0xd8, 0x9b, 0x54, 0xaf, 0xea, 0x7b, 0x55, 0xaf,
-	0x5e, 0xbd, 0x7a, 0xef, 0xd5, 0x63, 0xc3, 0xeb, 0x0d, 0x87, 0x37, 0x7b, 0xdb, 0xa5, 0x9a, 0xdb,
-	0x5e, 0x26, 0xbb, 0x3d, 0x87, 0xef, 0x2f, 0xef, 0x12, 0xaf, 0xe1, 0x2e, 0x93, 0xae, 0xb3, 0xbc,
-	0x77, 0x99, 0xb4, 0xba, 0x4d, 0x72, 0x79, 0xb9, 0x41, 0x3b, 0xd4, 0x23, 0x9c, 0xd6, 0x4b, 0x5d,
-	0xcf, 0xe5, 0x2e, 0x7a, 0x21, 0x1c, 0x55, 0x52, 0xa3, 0x4a, 0x72, 0x54, 0x89, 0x74, 0x9d, 0x92,
-	0x3f, 0x6a, 0xe1, 0x35, 0x03, 0xbb, 0xe1, 0x36, 0xdc, 0x65, 0x39, 0x78, 0xbb, 0xb7, 0x23, 0xff,
-	0x93, 0xff, 0xc8, 0xbf, 0x14, 0xe8, 0xc2, 0x9d, 0xdd, 0x6b, 0xac, 0xe4, 0x48, 0xce, 0xf4, 0x11,
-	0xa7, 0x1d, 0xe6, 0xb8, 0x1d, 0xf6, 0x1a, 0xe9, 0x3a, 0x8c, 0x7a, 0x7b, 0xd4, 0x5b, 0xee, 0xee,
-	0x36, 0x04, 0x8d, 0x45, 0x3b, 0x2c, 0xef, 0xf5, 0x4d, 0x6f, 0xe1, 0xf5, 0x10, 0xa9, 0x4d, 0x6a,
-	0x4d, 0xa7, 0x43, 0xbd, 0xfd, 0x70, 0x78, 0x9b, 0x72, 0x92, 0x34, 0x6a, 0x79, 0xd0, 0x28, 0xaf,
-	0xd7, 0xe1, 0x4e, 0x9b, 0xf6, 0x0d, 0x78, 0xe3, 0xa8, 0x01, 0xac, 0xd6, 0xa4, 0x6d, 0x12, 0x1f,
-	0x67, 0x7f, 0x04, 0x17, 0x56, 0x3b, 0xa4, 0xb5, 0xcf, 0x1c, 0x86, 0x7b, 0x9d, 0x55, 0xaf, 0xd1,
-	0x6b, 0xd3, 0x0e, 0x47, 0x97, 0x60, 0xac, 0x43, 0xda, 0x74, 0xde, 0xba, 0x64, 0xbd, 0x54, 0x28,
-	0x4f, 0x7e, 0x7e, 0xb0, 0x74, 0xee, 0xf0, 0x60, 0x69, 0xec, 0x5d, 0xd2, 0xa6, 0x58, 0x52, 0xd0,
-	0xf3, 0x90, 0xdb, 0x23, 0xad, 0x1e, 0x9d, 0xcf, 0xc8, 0x2e, 0x53, 0xba, 0x4b, 0xee, 0xbe, 0x68,
-	0xc4, 0x8a, 0x66, 0xff, 0x6e, 0x36, 0x02, 0xff, 0x4d, 0xca, 0x49, 0x9d, 0x70, 0x82, 0xda, 0x90,
-	0x6f, 0x91, 0x6d, 0xda, 0x62, 0xf3, 0xd6, 0xa5, 0xec, 0x4b, 0xc5, 0x95, 0x9b, 0xa5, 0x61, 0x36,
-	0xb1, 0x94, 0x00, 0x55, 0xda, 0x90, 0x38, 0x37, 0x3b, 0xdc, 0xdb, 0x2f, 0x9f, 0xd7, 0x93, 0xc8,
-	0xab, 0x46, 0xac, 0x99, 0xa0, 0xdf, 0xb6, 0xa0, 0x48, 0x3a, 0x1d, 0x97, 0x13, 0x2e, 0xb6, 0x69,
-	0x3e, 0x23, 0x99, 0xde, 0x1d, 0x9d, 0xe9, 0x6a, 0x08, 0xa6, 0x38, 0x5f, 0xd0, 0x9c, 0x8b, 0x06,
-	0x05, 0x9b, 0x3c, 0x17, 0xde, 0x82, 0xa2, 0x31, 0x55, 0x34, 0x03, 0xd9, 0x5d, 0xba, 0xaf, 0xe4,
-	0x8b, 0xc5, 0x9f, 0x68, 0x2e, 0x22, 0x50, 0x2d, 0xc1, 0xeb, 0x99, 0x6b, 0xd6, 0xc2, 0x0d, 0x98,
-	0x89, 0x33, 0x4c, 0x33, 0xde, 0xfe, 0x63, 0x0b, 0xe6, 0x8c, 0x55, 0x60, 0xba, 0x43, 0x3d, 0xda,
-	0xa9, 0x51, 0xb4, 0x0c, 0x05, 0xb1, 0x97, 0xac, 0x4b, 0x6a, 0xfe, 0x56, 0xcf, 0xea, 0x85, 0x14,
-	0xde, 0xf5, 0x09, 0x38, 0xec, 0x13, 0xa8, 0x45, 0xe6, 0x49, 0x6a, 0xd1, 0x6d, 0x12, 0x46, 0xe7,
-	0xb3, 0x51, 0xb5, 0xd8, 0x14, 0x8d, 0x58, 0xd1, 0xec, 0x87, 0xf0, 0x75, 0x7f, 0x3e, 0x5b, 0xb4,
-	0xdd, 0x6d, 0x11, 0x4e, 0xc3, 0x49, 0x1d, 0xad, 0x7a, 0x97, 0x60, 0x6c, 0xd7, 0xe9, 0xd4, 0xe3,
-	0xb3, 0xf8, 0x86, 0xd3, 0xa9, 0x63, 0x49, 0xb1, 0x77, 0x61, 0x6a, 0xb5, 0xdb, 0xf5, 0xdc, 0x3d,
-	0x5a, 0xaf, 0x72, 0xd2, 0xa0, 0xe8, 0x01, 0x00, 0xd1, 0x0d, 0xab, 0x5c, 0x42, 0x17, 0x57, 0x7e,
-	0xa1, 0xa4, 0xce, 0x4c, 0xc9, 0x3c, 0x33, 0xa5, 0xee, 0x6e, 0x43, 0x34, 0xb0, 0x92, 0x38, 0x9a,
-	0xa5, 0xbd, 0xcb, 0xa5, 0x2d, 0xa7, 0x4d, 0xcb, 0xe7, 0x0f, 0x0f, 0x96, 0x60, 0x35, 0x40, 0xc0,
-	0x06, 0x9a, 0xfd, 0x3b, 0x16, 0x5c, 0x5c, 0xf5, 0x1a, 0x6e, 0x65, 0x6d, 0xb5, 0xdb, 0xbd, 0x43,
-	0x49, 0x8b, 0x37, 0xab, 0x9c, 0xf0, 0x1e, 0x43, 0x37, 0x20, 0xcf, 0xe4, 0x5f, 0x7a, 0x31, 0x2f,
-	0xfa, 0xfa, 0xa9, 0xe8, 0x8f, 0x0f, 0x96, 0xe6, 0x12, 0x06, 0x52, 0xac, 0x47, 0xa1, 0x97, 0x61,
-	0xbc, 0x4d, 0x19, 0x23, 0x0d, 0x5f, 0xe2, 0xd3, 0x1a, 0x60, 0xfc, 0x9b, 0xaa, 0x19, 0xfb, 0x74,
-	0xfb, 0x1f, 0x33, 0x30, 0x1d, 0x60, 0x69, 0xf6, 0xa7, 0xb0, 0xbd, 0x3d, 0x98, 0x6c, 0x1a, 0x2b,
-	0x94, 0xbb, 0x5c, 0x5c, 0x79, 0x7b, 0xc8, 0x93, 0x94, 0x24, 0xa4, 0xf2, 0x9c, 0x66, 0x33, 0x69,
-	0xb6, 0xe2, 0x08, 0x1b, 0xd4, 0x06, 0x60, 0xfb, 0x9d, 0x9a, 0x66, 0x3a, 0x26, 0x99, 0xbe, 0x95,
-	0x92, 0x69, 0x35, 0x00, 0x28, 0x23, 0xcd, 0x12, 0xc2, 0x36, 0x6c, 0x30, 0xb0, 0xff, 0xc6, 0x82,
-	0x0b, 0x09, 0xe3, 0xd0, 0x3b, 0xb1, 0xfd, 0x7c, 0xa1, 0x6f, 0x3f, 0x51, 0xdf, 0xb0, 0x70, 0x37,
-	0x5f, 0x85, 0x09, 0x8f, 0xee, 0x39, 0xe2, 0xa6, 0xd0, 0x12, 0x9e, 0xd1, 0xe3, 0x27, 0xb0, 0x6e,
-	0xc7, 0x41, 0x0f, 0xf4, 0x0a, 0x14, 0xfc, 0xbf, 0x85, 0x98, 0xb3, 0xe2, 0x30, 0x89, 0x8d, 0xf3,
-	0xbb, 0x32, 0x1c, 0xd2, 0xed, 0xdf, 0x82, 0x5c, 0xa5, 0x49, 0x3c, 0x2e, 0x34, 0xc6, 0xa3, 0x5d,
-	0xf7, 0x3d, 0xbc, 0xa1, 0xa7, 0x18, 0x68, 0x0c, 0x56, 0xcd, 0xd8, 0xa7, 0x0f, 0xb1, 0xd9, 0x2f,
-	0xc3, 0xf8, 0x1e, 0xf5, 0xe4, 0x7c, 0xb3, 0x51, 0xb0, 0xfb, 0xaa, 0x19, 0xfb, 0x74, 0xfb, 0xdf,
-	0x2c, 0x98, 0x93, 0x33, 0x58, 0x73, 0x58, 0xcd, 0xdd, 0xa3, 0xde, 0x3e, 0xa6, 0xac, 0xd7, 0x3a,
-	0xe1, 0x09, 0xad, 0xc1, 0x0c, 0xa3, 0xed, 0x3d, 0xea, 0x55, 0xdc, 0x0e, 0xe3, 0x1e, 0x71, 0x3a,
-	0x5c, 0xcf, 0x6c, 0x5e, 0xf7, 0x9e, 0xa9, 0xc6, 0xe8, 0xb8, 0x6f, 0x04, 0x7a, 0x09, 0x26, 0xf4,
-	0xb4, 0x85, 0x2a, 0x09, 0xc1, 0x4e, 0x8a, 0x3d, 0xd0, 0x6b, 0x62, 0x38, 0xa0, 0xda, 0xff, 0x6d,
-	0xc1, 0xac, 0x5c, 0x55, 0xb5, 0xb7, 0xcd, 0x6a, 0x9e, 0xd3, 0x15, 0x06, 0xf8, 0x69, 0x5c, 0xd2,
-	0x0d, 0x38, 0x5f, 0xf7, 0x05, 0xbf, 0xe1, 0xb4, 0x1d, 0x2e, 0xcf, 0x48, 0xae, 0xfc, 0x8c, 0xc6,
-	0x38, 0xbf, 0x16, 0xa1, 0xe2, 0x58, 0x6f, 0xb5, 0x7d, 0xad, 0x1e, 0xe3, 0xd4, 0xdb, 0xf4, 0xdc,
-	0xb6, 0x2b, 0xd6, 0xb9, 0x45, 0xd8, 0x2e, 0xfa, 0x55, 0x98, 0x68, 0xeb, 0x4b, 0x4f, 0x5b, 0xcd,
-	0x5f, 0x1c, 0xce, 0x6a, 0xde, 0xdb, 0xfe, 0x16, 0xad, 0x71, 0x71, 0x61, 0x86, 0xa7, 0x2d, 0x6c,
-	0xc3, 0x01, 0x2a, 0xfa, 0x00, 0xc6, 0x58, 0x97, 0xd6, 0xa4, 0x88, 0x8a, 0x2b, 0x6f, 0x0e, 0x77,
-	0xa8, 0x23, 0x93, 0xac, 0x76, 0x69, 0x2d, 0x94, 0xad, 0xf8, 0x0f, 0x4b, 0x48, 0xfb, 0x3f, 0x2d,
-	0x98, 0x4f, 0x5a, 0xd5, 0x86, 0xc3, 0x38, 0xfa, 0xa8, 0x6f, 0x65, 0xa5, 0xe1, 0x56, 0x26, 0x46,
-	0xcb, 0x75, 0x05, 0xa7, 0xd7, 0x6f, 0x31, 0x56, 0xf5, 0x10, 0x72, 0x0e, 0xa7, 0x6d, 0xdf, 0xd5,
-	0xb8, 0x3e, 0xdc, 0xb2, 0x92, 0x26, 0x1b, 0x5e, 0xa1, 0xeb, 0x02, 0x10, 0x2b, 0x5c, 0xfb, 0x43,
-	0x98, 0xac, 0xf4, 0x3c, 0x8f, 0x76, 0xb8, 0xba, 0xe0, 0xbe, 0x01, 0x39, 0xe6, 0x74, 0xb4, 0x9d,
-	0x4f, 0x77, 0xb7, 0x15, 0x04, 0x78, 0x55, 0x0c, 0xc6, 0x0a, 0xc3, 0xfe, 0xb3, 0x2c, 0x5c, 0xf0,
-	0x35, 0x86, 0xd6, 0x57, 0x3d, 0xee, 0xec, 0x90, 0x1a, 0x67, 0xa8, 0x0e, 0x93, 0xf5, 0xb0, 0x99,
-	0x6b, 0x43, 0x9c, 0x86, 0x57, 0x60, 0xec, 0x0d, 0x78, 0x8e, 0x23, 0xa8, 0xe8, 0x7d, 0xc8, 0x36,
-	0x1c, 0xae, 0x3d, 0xc3, 0x6b, 0xc3, 0x49, 0xee, 0xb6, 0x13, 0xb7, 0x3c, 0xe5, 0xa2, 0x66, 0x95,
-	0xbd, 0xed, 0x70, 0x2c, 0x10, 0xd1, 0x36, 0xe4, 0x9d, 0x36, 0x69, 0xd0, 0x94, 0xbb, 0xb2, 0x2e,
-	0xc6, 0xc4, 0xd1, 0x03, 0x57, 0x53, 0x52, 0x19, 0xd6, 0xc8, 0x82, 0x47, 0x4d, 0x58, 0x0c, 0x65,
-	0xb3, 0x87, 0xdf, 0xf9, 0x04, 0xdb, 0x19, 0xf2, 0x90, 0x54, 0x86, 0x35, 0xb2, 0xfd, 0x65, 0x06,
-	0x66, 0x42, 0xf9, 0x55, 0xdc, 0x76, 0xdb, 0xe1, 0x68, 0x01, 0x32, 0x4e, 0x5d, 0x1b, 0x24, 0xd0,
-	0x03, 0x33, 0xeb, 0x6b, 0x38, 0xe3, 0xd4, 0xd1, 0x8b, 0x90, 0xdf, 0xf6, 0x48, 0xa7, 0xd6, 0xd4,
-	0x86, 0x28, 0x00, 0x2e, 0xcb, 0x56, 0xac, 0xa9, 0xe8, 0x39, 0xc8, 0x72, 0xd2, 0xd0, 0xf6, 0x27,
-	0x90, 0xdf, 0x16, 0x69, 0x60, 0xd1, 0x2e, 0x0c, 0x1f, 0xeb, 0xc9, 0x33, 0x2c, 0x77, 0xde, 0x30,
-	0x7c, 0x55, 0xd5, 0x8c, 0x7d, 0xba, 0xe0, 0x48, 0x7a, 0xbc, 0xe9, 0x7a, 0xf3, 0xb9, 0x28, 0xc7,
-	0x55, 0xd9, 0x8a, 0x35, 0x55, 0xb8, 0x28, 0x35, 0x39, 0x7f, 0x4e, 0xbd, 0xf9, 0x7c, 0xd4, 0x45,
-	0xa9, 0xf8, 0x04, 0x1c, 0xf6, 0x41, 0x1f, 0x43, 0xb1, 0xe6, 0x51, 0xc2, 0x5d, 0x6f, 0x8d, 0x70,
-	0x3a, 0x3f, 0x9e, 0x5a, 0x03, 0xa7, 0x85, 0x97, 0x5e, 0x09, 0x21, 0xb0, 0x89, 0x67, 0xff, 0x6d,
-	0x16, 0xe6, 0x43, 0xd1, 0xca, 0xbd, 0x0d, 0x3d, 0x53, 0x2d, 0x1e, 0x6b, 0x80, 0x78, 0x5e, 0x84,
-	0x7c, 0xdd, 0x69, 0x50, 0xc6, 0xe3, 0x52, 0x5e, 0x93, 0xad, 0x58, 0x53, 0xd1, 0x1f, 0xc4, 0xa2,
-	0x91, 0x9c, 0x54, 0x94, 0x7b, 0xc3, 0x29, 0xca, 0xa0, 0xc9, 0x8d, 0x10, 0x92, 0xa0, 0x15, 0x80,
-	0x86, 0xc3, 0xf5, 0xa5, 0xa5, 0x77, 0x3d, 0x30, 0xd6, 0xb7, 0x03, 0x0a, 0x36, 0x7a, 0xa1, 0xf7,
-	0xa1, 0x20, 0xe5, 0x35, 0xe2, 0xf9, 0x97, 0x2e, 0x4c, 0xc5, 0x07, 0xc0, 0x21, 0xd6, 0xb1, 0x83,
-	0x9c, 0x0f, 0x01, 0xdd, 0x7c, 0xd4, 0xf5, 0x28, 0x13, 0x57, 0xf7, 0x7d, 0xe2, 0x39, 0x64, 0xbb,
-	0x45, 0x4f, 0x2a, 0x8e, 0xfd, 0x62, 0x0c, 0xc6, 0x6f, 0x79, 0xd4, 0x69, 0x34, 0xf9, 0x19, 0x5c,
-	0x89, 0xcf, 0x43, 0x8e, 0xb4, 0x1c, 0xc2, 0xa4, 0x76, 0x1b, 0x53, 0x5a, 0x15, 0x8d, 0x58, 0xd1,
-	0xd0, 0x87, 0x90, 0x77, 0x3d, 0xa7, 0xe1, 0x74, 0xe6, 0x0b, 0x72, 0x12, 0x57, 0x86, 0xd3, 0x1f,
-	0xbd, 0x8a, 0x7b, 0x72, 0x68, 0xa8, 0xa2, 0xea, 0x7f, 0xac, 0x21, 0xd1, 0x03, 0x18, 0x57, 0x47,
-	0xce, 0x37, 0x63, 0xcb, 0x43, 0x9b, 0x61, 0x75, 0x6a, 0x43, 0xd3, 0xa0, 0xfe, 0x67, 0xd8, 0x07,
-	0x44, 0xd5, 0xc0, 0x0a, 0x8f, 0x49, 0xe8, 0x57, 0x52, 0x58, 0xe1, 0x81, 0x66, 0xb7, 0x1a, 0x98,
-	0xdd, 0x5c, 0x1a, 0x50, 0x69, 0x58, 0x07, 0xd9, 0x59, 0x21, 0x62, 0xed, 0xee, 0xe7, 0x47, 0x10,
-	0xb1, 0x8e, 0x35, 0xce, 0x47, 0x63, 0x04, 0x3f, 0x1a, 0xb0, 0xbf, 0x9f, 0x85, 0x59, 0xdd, 0xb3,
-	0xe2, 0xb6, 0x5a, 0xb4, 0x26, 0x7d, 0x4b, 0x65, 0xc5, 0xb3, 0x89, 0x56, 0xdc, 0xf1, 0x7d, 0x0a,
-	0x75, 0x33, 0x96, 0x53, 0xcd, 0x26, 0xe4, 0x51, 0x92, 0x7e, 0x84, 0xb2, 0x11, 0xc1, 0x2e, 0xe9,
-	0x5e, 0xda, 0xbb, 0x40, 0xbf, 0x6f, 0xc1, 0x85, 0x3d, 0xea, 0x39, 0x3b, 0x4e, 0x4d, 0x9e, 0xc7,
-	0x3b, 0x0e, 0xe3, 0xae, 0xb7, 0xaf, 0xef, 0xcd, 0x37, 0x86, 0xe3, 0x7c, 0xdf, 0x00, 0x58, 0xef,
-	0xec, 0xb8, 0xe5, 0x67, 0x35, 0xb7, 0x0b, 0xf7, 0xfb, 0xa1, 0x71, 0x12, 0xbf, 0x85, 0x2e, 0x40,
-	0x38, 0xdb, 0x04, 0x73, 0xb0, 0x61, 0x1e, 0xde, 0xa1, 0x27, 0xe6, 0x2f, 0xd6, 0xb7, 0x9d, 0xa6,
-	0x19, 0xf9, 0xcc, 0x82, 0xa2, 0xa6, 0x9f, 0x81, 0x9b, 0x88, 0xa3, 0x6e, 0xe2, 0x6b, 0xa9, 0xe6,
-	0x3f, 0xc0, 0x33, 0xf4, 0x60, 0x2a, 0x72, 0xc8, 0xd1, 0x55, 0x9d, 0x2e, 0x51, 0x36, 0xf0, 0xe7,
-	0xcd, 0x74, 0xc9, 0xe3, 0x83, 0xa5, 0xd9, 0x48, 0xe7, 0x30, 0x87, 0x72, 0x74, 0xec, 0x72, 0x7d,
-	0xe2, 0x07, 0x7f, 0xb9, 0x74, 0xee, 0x3b, 0x3f, 0xbe, 0x74, 0xce, 0xfe, 0x34, 0x0b, 0x33, 0x71,
-	0xa9, 0x0e, 0x61, 0x7b, 0x43, 0x1b, 0x36, 0x71, 0xaa, 0x36, 0x2c, 0x73, 0x7a, 0x36, 0x2c, 0x7b,
-	0x1a, 0x36, 0x6c, 0xec, 0xc4, 0x6c, 0x98, 0xfd, 0x2f, 0x16, 0x9c, 0x0f, 0x76, 0xe6, 0x93, 0x9e,
-	0xf0, 0x3f, 0x42, 0xa9, 0x5b, 0x27, 0x2f, 0xf5, 0x87, 0x30, 0xce, 0xdc, 0x9e, 0x57, 0x93, 0x4e,
-	0xb6, 0x40, 0x7f, 0x3d, 0x9d, 0xd1, 0x54, 0x63, 0x0d, 0xcf, 0x52, 0x35, 0x60, 0x1f, 0xd5, 0xfe,
-	0x2c, 0x13, 0x2c, 0x48, 0xd3, 0x94, 0xe3, 0xe5, 0x09, 0xb7, 0x54, 0x2c, 0x68, 0xc2, 0x74, 0xbc,
-	0x44, 0x2b, 0xd6, 0x54, 0x64, 0x4b, 0x7b, 0xee, 0xfb, 0xff, 0x85, 0x32, 0x68, 0xb3, 0x2c, 0x37,
-	0x41, 0x51, 0x50, 0x17, 0x66, 0x3c, 0xfa, 0x49, 0xcf, 0xf1, 0x68, 0xbd, 0xea, 0x92, 0x5d, 0xe1,
-	0xb4, 0xe8, 0x24, 0xd7, 0x90, 0xe7, 0x7e, 0xad, 0xe7, 0x49, 0x13, 0x56, 0x9e, 0x13, 0xb1, 0x3b,
-	0x8e, 0x61, 0xe1, 0x3e, 0x74, 0xe4, 0xc2, 0x1c, 0xd9, 0x23, 0x4e, 0x8b, 0x6c, 0x3b, 0x2d, 0x87,
-	0xef, 0x57, 0xb9, 0x47, 0x38, 0x6d, 0xec, 0x6b, 0x17, 0xfb, 0x6d, 0xbd, 0x96, 0xb9, 0xd5, 0x84,
-	0x3e, 0x8f, 0x0f, 0x96, 0x9e, 0xd5, 0xb2, 0x48, 0x22, 0xe3, 0x44, 0x60, 0xfb, 0xbf, 0x72, 0x81,
-	0x85, 0xd0, 0x79, 0xad, 0x5f, 0x87, 0x62, 0x4d, 0x05, 0x93, 0xad, 0xfd, 0xf5, 0x8e, 0xd6, 0xe9,
-	0xb5, 0x11, 0x6e, 0xbb, 0x52, 0x25, 0x84, 0x89, 0x79, 0xa1, 0x06, 0x05, 0x9b, 0xdc, 0xd0, 0xb7,
-	0x01, 0x94, 0xe9, 0xa7, 0xf5, 0xf5, 0x8e, 0xbe, 0xdb, 0x2a, 0xa3, 0xf0, 0xbe, 0x1f, 0xa0, 0x28,
-	0xd6, 0x81, 0x93, 0x15, 0x12, 0xb0, 0xc1, 0x4a, 0xac, 0xda, 0xcf, 0xe2, 0xde, 0x72, 0x3d, 0x6d,
-	0x24, 0x46, 0x5a, 0xf5, 0x6a, 0x08, 0x13, 0xf7, 0xbd, 0x43, 0x0a, 0x36, 0xb9, 0x2d, 0x78, 0x30,
-	0x13, 0x97, 0x55, 0xc2, 0xfd, 0x76, 0x27, 0x7a, 0xbf, 0xad, 0x0c, 0x69, 0x11, 0x8c, 0xc4, 0x80,
-	0xf9, 0x8e, 0xe0, 0xc1, 0x74, 0x4c, 0x46, 0x09, 0x2c, 0xd7, 0xa3, 0x2c, 0xaf, 0xa4, 0xb9, 0xeb,
-	0x75, 0xb6, 0xdd, 0xe4, 0xc9, 0x60, 0x26, 0x2e, 0x9d, 0x13, 0x63, 0x1a, 0x49, 0xf1, 0x9b, 0x97,
-	0xf8, 0x9f, 0x67, 0xa0, 0x10, 0x98, 0xf1, 0x34, 0xf9, 0x3a, 0xe5, 0x7e, 0x65, 0x8e, 0x08, 0xa2,
-	0xb3, 0xc3, 0x04, 0xd1, 0x63, 0x83, 0x83, 0x68, 0x3f, 0xa7, 0x9f, 0x7f, 0x72, 0x4e, 0xdf, 0x08,
-	0xa2, 0xc7, 0x87, 0x0f, 0xa2, 0x27, 0x8e, 0x0e, 0xa2, 0xed, 0xbf, 0xb2, 0x00, 0xf5, 0x67, 0x4c,
-	0xd2, 0x08, 0x8a, 0xc4, 0x2f, 0xd7, 0x37, 0xd2, 0x86, 0xaf, 0x47, 0xdd, 0xb1, 0xf6, 0x67, 0x39,
-	0x98, 0xbe, 0xed, 0x8c, 0x9c, 0x7a, 0xe5, 0xf0, 0x35, 0x85, 0x54, 0xa5, 0xda, 0xf1, 0x0d, 0x2c,
-	0xab, 0xda, 0xdf, 0xeb, 0x7a, 0xe8, 0xd7, 0x2a, 0xc9, 0xdd, 0x1e, 0x0f, 0x26, 0xe1, 0x41, 0xd0,
-	0x43, 0x2b, 0xc9, 0xdb, 0x30, 0xc5, 0xb8, 0xe7, 0xd4, 0xb8, 0x4a, 0xee, 0xb2, 0xf9, 0xa2, 0xbc,
-	0xb9, 0x2e, 0xea, 0xee, 0x53, 0x55, 0x93, 0x88, 0xa3, 0x7d, 0x13, 0x73, 0xc6, 0x63, 0xa9, 0x73,
-	0xc6, 0xcb, 0x50, 0x20, 0xad, 0x96, 0xfb, 0xed, 0x2d, 0xd2, 0x60, 0x3a, 0x4b, 0x13, 0x68, 0xcd,
-	0xaa, 0x4f, 0xc0, 0x61, 0x1f, 0x54, 0x02, 0x70, 0x1a, 0x1d, 0xd7, 0xa3, 0x72, 0x44, 0x5e, 0x5e,
-	0xa1, 0xf2, 0x5d, 0x6c, 0x3d, 0x68, 0xc5, 0x46, 0x0f, 0x54, 0x85, 0x8b, 0x4e, 0x87, 0xd1, 0x5a,
-	0xcf, 0xa3, 0xd5, 0x5d, 0xa7, 0xbb, 0xb5, 0x51, 0x95, 0x56, 0x62, 0x5f, 0x6a, 0xf3, 0x44, 0xf9,
-	0x39, 0xcd, 0xec, 0xe2, 0x7a, 0x52, 0x27, 0x9c, 0x3c, 0x16, 0xbd, 0x0e, 0x93, 0x4e, 0xa7, 0xd6,
-	0xea, 0xd5, 0xe9, 0x26, 0xe1, 0x4d, 0x36, 0x3f, 0x21, 0xa7, 0x31, 0x73, 0x78, 0xb0, 0x34, 0xb9,
-	0x6e, 0xb4, 0xe3, 0x48, 0x2f, 0x31, 0x8a, 0x3e, 0x32, 0x46, 0x15, 0xc2, 0x51, 0x37, 0x1f, 0x99,
-	0xa3, 0xcc, 0x5e, 0x09, 0x59, 0x75, 0x48, 0x95, 0x55, 0xff, 0x61, 0x06, 0xf2, 0xea, 0x51, 0x0b,
-	0x5d, 0x8d, 0xbd, 0x1c, 0x3d, 0xd7, 0xf7, 0x72, 0x54, 0x4c, 0x7a, 0x00, 0xb4, 0x21, 0xef, 0x30,
-	0xd6, 0x8b, 0x7a, 0x2c, 0xeb, 0xb2, 0x05, 0x6b, 0x8a, 0xcc, 0x38, 0xba, 0x9d, 0x1d, 0xa7, 0xa1,
-	0xd3, 0x31, 0x37, 0x0c, 0x3f, 0x25, 0x2c, 0x4d, 0x78, 0x18, 0xd4, 0x2e, 0x84, 0x2e, 0x4b, 0xa4,
-	0x83, 0xf0, 0x5d, 0xee, 0x56, 0xef, 0xbd, 0xab, 0x78, 0x54, 0x24, 0x22, 0xd6, 0xc8, 0x82, 0x87,
-	0xdb, 0xe3, 0xdd, 0x1e, 0x97, 0x8a, 0x72, 0x42, 0x3c, 0xee, 0x49, 0x44, 0xac, 0x91, 0xed, 0x4f,
-	0x2d, 0x98, 0x56, 0x32, 0xa8, 0x34, 0x69, 0x6d, 0xb7, 0xca, 0x69, 0x57, 0x84, 0x10, 0x3d, 0x46,
-	0x59, 0x3c, 0x84, 0x78, 0x8f, 0x51, 0x86, 0x25, 0xc5, 0x58, 0x7d, 0xe6, 0xb4, 0x56, 0x6f, 0x1f,
-	0x66, 0x20, 0x27, 0x7d, 0xf5, 0x34, 0xf6, 0x27, 0x9a, 0x5c, 0xcb, 0x0c, 0x95, 0x5c, 0x3b, 0x22,
-	0xff, 0x1a, 0x26, 0x18, 0xc7, 0x9e, 0x98, 0x60, 0x64, 0x49, 0xf9, 0xc5, 0x77, 0x52, 0x84, 0x28,
-	0xa3, 0xd4, 0x37, 0x1c, 0x37, 0x7f, 0xf7, 0x53, 0x0b, 0xe6, 0x92, 0x32, 0xed, 0x69, 0x64, 0xfe,
-	0x2a, 0x4c, 0x74, 0x5b, 0x84, 0xef, 0xb8, 0x5e, 0x3b, 0xfe, 0xc2, 0xba, 0xa9, 0xdb, 0x71, 0xd0,
-	0x03, 0x79, 0x00, 0x9e, 0x1f, 0xac, 0xfa, 0x81, 0xdc, 0x8d, 0xe3, 0x65, 0x61, 0xc3, 0x1d, 0x0e,
-	0x9a, 0x18, 0x36, 0xb8, 0xd8, 0x7f, 0x94, 0x83, 0x59, 0x39, 0x64, 0xd4, 0x6b, 0x6d, 0x14, 0xb5,
-	0xea, 0xc2, 0x33, 0x32, 0xc4, 0xec, 0xbf, 0x09, 0x95, 0xa6, 0x5d, 0xd3, 0xe3, 0x9f, 0x59, 0x4f,
-	0xec, 0xf5, 0x78, 0x20, 0x05, 0x0f, 0xc0, 0xed, 0xbf, 0xde, 0xe0, 0x67, 0xef, 0x7a, 0x33, 0x95,
-	0x6d, 0xfc, 0x48, 0x65, 0x1b, 0x78, 0x19, 0x4e, 0x1c, 0xe3, 0x32, 0xec, 0xbf, 0xa0, 0x0a, 0xa9,
-	0x2e, 0xa8, 0xbf, 0xc8, 0xc0, 0xf8, 0xa6, 0xe7, 0xca, 0x17, 0x9b, 0xd3, 0x4f, 0x6b, 0xbf, 0x37,
-	0xe2, 0x4b, 0xaf, 0x80, 0x52, 0x16, 0x59, 0xbe, 0xf4, 0x4e, 0x44, 0x5f, 0x79, 0x8d, 0x2c, 0x6d,
-	0x36, 0x4d, 0xf0, 0xa0, 0x81, 0x8f, 0xc8, 0xd2, 0xfe, 0xc8, 0x82, 0xa9, 0xc8, 0x14, 0x9e, 0xe2,
-	0x17, 0xf1, 0x98, 0x9c, 0x92, 0x5e, 0xc4, 0xff, 0xc9, 0x82, 0xd9, 0x48, 0xcf, 0x33, 0xc8, 0x71,
-	0xfe, 0x4a, 0x34, 0xc7, 0x79, 0x65, 0x84, 0xf5, 0x0c, 0xc8, 0x74, 0xfe, 0x49, 0x7c, 0x35, 0x62,
-	0xa5, 0xe8, 0x37, 0x61, 0xb6, 0xeb, 0x3f, 0xa0, 0x6f, 0xba, 0x2d, 0xa7, 0xe6, 0x50, 0x3f, 0x65,
-	0x7e, 0x35, 0x65, 0x75, 0x81, 0x1c, 0xbe, 0x5f, 0xfe, 0xba, 0xe6, 0x3e, 0xbb, 0x19, 0xc7, 0xc5,
-	0xfd, 0xac, 0x64, 0x06, 0x59, 0xcf, 0xea, 0xa9, 0xcd, 0x20, 0xeb, 0xf9, 0x0d, 0x90, 0xeb, 0xbf,
-	0x87, 0x4a, 0xaf, 0x13, 0x44, 0x35, 0x80, 0x9a, 0xdb, 0xa9, 0x3b, 0x3c, 0xa8, 0x46, 0x2a, 0xae,
-	0x2c, 0x0f, 0xb7, 0x8a, 0x8a, 0x3f, 0x2e, 0xd4, 0xfa, 0xa0, 0x89, 0x61, 0x03, 0x16, 0x5d, 0xf1,
-	0x4b, 0x07, 0xa3, 0x2e, 0xb2, 0x2a, 0x1d, 0x7c, 0x7c, 0xb0, 0x34, 0xa9, 0xe7, 0x64, 0x96, 0x12,
-	0xa6, 0x29, 0x91, 0xfb, 0xeb, 0x0c, 0x14, 0x82, 0x1d, 0x3c, 0x73, 0x7b, 0x77, 0x25, 0xa5, 0xee,
-	0x0d, 0x3a, 0xc3, 0xe8, 0xe3, 0x98, 0xbd, 0x4b, 0xab, 0xd4, 0x47, 0x58, 0xbc, 0xbf, 0x57, 0x9b,
-	0xaf, 0xfa, 0x9e, 0x81, 0x02, 0x6f, 0x45, 0x15, 0x78, 0x39, 0xe5, 0x6a, 0x06, 0xa8, 0xf0, 0x77,
-	0x2d, 0x98, 0x8e, 0x1d, 0x63, 0xf4, 0x3c, 0xe4, 0x64, 0x92, 0x57, 0xeb, 0x57, 0x30, 0x50, 0xa7,
-	0x8f, 0x24, 0x0d, 0x6d, 0xc2, 0x1c, 0xe9, 0x71, 0x37, 0x18, 0x7b, 0xb3, 0x43, 0xb6, 0x5b, 0x54,
-	0xe5, 0x84, 0x26, 0xca, 0x3f, 0x17, 0x64, 0x63, 0x13, 0xfa, 0xe0, 0xc4, 0x91, 0xf6, 0x8f, 0x32,
-	0x80, 0x82, 0xc6, 0x34, 0xaf, 0x23, 0x1f, 0xc3, 0xf8, 0x8e, 0x4a, 0x33, 0x1e, 0xef, 0x79, 0xab,
-	0x5c, 0x34, 0x5f, 0xf8, 0x7c, 0x4c, 0xf4, 0xc1, 0xc9, 0xe8, 0x11, 0xf4, 0xeb, 0x10, 0x7a, 0x00,
-	0xb0, 0xe3, 0x74, 0x1c, 0xd6, 0x1c, 0xb1, 0x4a, 0x40, 0xba, 0x5d, 0xb7, 0x02, 0x04, 0x6c, 0xa0,
-	0xd9, 0x7f, 0x9a, 0x31, 0xf4, 0x53, 0x1a, 0xfc, 0xa1, 0xf6, 0xf5, 0xe5, 0xa8, 0x30, 0x0b, 0xfd,
-	0x4f, 0x9f, 0x81, 0x60, 0x1e, 0xc0, 0xd8, 0x1e, 0xf1, 0xfc, 0x57, 0x98, 0x21, 0x0b, 0x90, 0xfa,
-	0x6b, 0x0f, 0xc2, 0x3d, 0xbd, 0x4f, 0x3c, 0x86, 0x25, 0xa6, 0xb8, 0x0c, 0x19, 0xa7, 0x5d, 0xdf,
-	0x86, 0xa6, 0x36, 0x0a, 0x9c, 0x76, 0xcd, 0x05, 0xd2, 0x2e, 0xc3, 0x0a, 0xd0, 0xfe, 0xfe, 0xb8,
-	0xa1, 0xf1, 0xda, 0x6c, 0xdf, 0x05, 0xd4, 0x22, 0x8c, 0xdf, 0x21, 0x9d, 0xba, 0xd0, 0x4f, 0xba,
-	0xe3, 0x51, 0xd6, 0xd4, 0xbe, 0xf4, 0x82, 0x46, 0x41, 0x1b, 0x7d, 0x3d, 0x70, 0xc2, 0x28, 0x74,
-	0x35, 0x6a, 0x9d, 0x97, 0xe2, 0xd6, 0xf9, 0x7c, 0x78, 0xdc, 0x46, 0xb3, 0xcf, 0xa6, 0xbe, 0xe7,
-	0x4e, 0x41, 0xdf, 0x7f, 0x03, 0x66, 0x77, 0xe2, 0x6f, 0xe1, 0xba, 0x7e, 0xe8, 0xcd, 0x11, 0x9f,
-	0xd2, 0xcb, 0x17, 0x0f, 0xc3, 0x07, 0xd4, 0xb0, 0x19, 0xf7, 0x33, 0x42, 0xae, 0x5f, 0x38, 0x2d,
-	0x93, 0x1b, 0x2a, 0x6f, 0x35, 0xf4, 0x99, 0x8b, 0xa5, 0x45, 0xe2, 0x25, 0xd3, 0x0a, 0x12, 0x47,
-	0x18, 0xc4, 0xce, 0x60, 0xfe, 0x24, 0xcf, 0x20, 0xba, 0x1a, 0xbc, 0x17, 0x89, 0xe9, 0xc8, 0xa0,
-	0x23, 0xdb, 0xf7, 0xd2, 0x23, 0x48, 0xd8, 0xec, 0x87, 0xbe, 0x67, 0xc1, 0x45, 0xa1, 0xac, 0x37,
-	0x1f, 0xd1, 0x5a, 0x4f, 0x48, 0xc5, 0xff, 0x3d, 0xc5, 0x7c, 0x51, 0x4a, 0x63, 0xc8, 0x32, 0xf2,
-	0x6a, 0x12, 0x44, 0x18, 0x41, 0x25, 0x92, 0x71, 0x32, 0x63, 0xf4, 0x50, 0xda, 0x0e, 0x4e, 0x65,
-	0x80, 0x7a, 0xfc, 0xec, 0x51, 0x41, 0xdb, 0x1d, 0xae, 0xec, 0x0e, 0xa7, 0xf6, 0x4f, 0xb2, 0xa6,
-	0xb9, 0x1a, 0x2e, 0xa7, 0xf5, 0x00, 0xc6, 0x38, 0x61, 0xbb, 0xfa, 0x14, 0xbc, 0x33, 0x42, 0x49,
-	0x6c, 0x78, 0x16, 0x64, 0xb4, 0x24, 0x9b, 0x24, 0x26, 0x5a, 0x80, 0x0c, 0x61, 0xf1, 0x17, 0x8e,
-	0x55, 0x86, 0x33, 0x84, 0xc9, 0xd7, 0x8f, 0x1d, 0x1d, 0xcb, 0x86, 0xaf, 0x1f, 0x3b, 0x38, 0xe3,
-	0xec, 0xa0, 0x0f, 0x20, 0xe7, 0x51, 0xee, 0xed, 0x6b, 0x6b, 0x7e, 0x6d, 0x04, 0xc3, 0x85, 0xc5,
-	0x78, 0x25, 0x22, 0xf9, 0x27, 0x56, 0x88, 0x81, 0xbd, 0xcd, 0x9f, 0x82, 0xbd, 0x0d, 0xd3, 0x83,
-	0xd9, 0x53, 0x4b, 0x0f, 0xfe, 0xd0, 0x32, 0x2e, 0xf8, 0x60, 0xa1, 0xe8, 0x3d, 0x18, 0xe7, 0x4e,
-	0x9b, 0xba, 0x3d, 0x9e, 0xce, 0x6b, 0x0a, 0x1e, 0x90, 0xa5, 0x19, 0xdb, 0x52, 0x10, 0xd8, 0xc7,
-	0x12, 0x31, 0x3f, 0xf5, 0x3c, 0xd7, 0xdb, 0x6a, 0x0a, 0xb3, 0xec, 0xb6, 0x94, 0x6b, 0x32, 0x15,
-	0xc6, 0xfc, 0x37, 0x23, 0x54, 0x1c, 0xeb, 0xed, 0x47, 0xb4, 0x3f, 0x2b, 0x35, 0xde, 0x3a, 0xa2,
-	0x3d, 0xd3, 0xe2, 0xee, 0x91, 0x23, 0xda, 0x23, 0xab, 0xba, 0x3f, 0x82, 0x67, 0x92, 0xcf, 0xf1,
-	0x89, 0xfc, 0x2a, 0xea, 0x1f, 0xe2, 0xb2, 0x92, 0xee, 0x93, 0x7f, 0xfc, 0xac, 0xd3, 0x74, 0x77,
-	0x32, 0x27, 0xed, 0xee, 0x78, 0xe6, 0x52, 0xf4, 0x6f, 0xc8, 0xd0, 0xc7, 0x5a, 0xcf, 0xac, 0x34,
-	0xbf, 0x4a, 0xea, 0x83, 0x19, 0xa8, 0x6b, 0xff, 0x6c, 0xc1, 0xc5, 0xc4, 0xde, 0x81, 0x0c, 0x33,
-	0xa7, 0x29, 0x43, 0xeb, 0xa4, 0x65, 0xf8, 0x83, 0x0c, 0xcc, 0x60, 0xda, 0x75, 0x23, 0xb9, 0xe8,
-	0x4d, 0xbf, 0xfa, 0x3e, 0x45, 0x4c, 0x10, 0x7b, 0xa6, 0x2d, 0x8f, 0x47, 0xca, 0xee, 0xc5, 0x71,
-	0x69, 0xfb, 0x0e, 0xe0, 0xd0, 0xc7, 0xbf, 0x2f, 0x4b, 0xae, 0x6e, 0x0e, 0x95, 0x6f, 0x57, 0x80,
-	0x02, 0x59, 0x96, 0x39, 0x69, 0xe3, 0xfe, 0x66, 0x8a, 0x82, 0xa9, 0x7e, 0x64, 0xd9, 0x8c, 0x15,
-	0xa0, 0xfd, 0x69, 0x06, 0x54, 0xfc, 0x70, 0x06, 0xd6, 0xf1, 0x97, 0x23, 0xd6, 0x71, 0x79, 0x58,
-	0x27, 0x48, 0x88, 0x67, 0x50, 0x8e, 0x20, 0x1e, 0xdb, 0x5d, 0x4e, 0x03, 0xfa, 0xe4, 0xfc, 0xc0,
-	0xdf, 0x59, 0x50, 0x90, 0xfd, 0xce, 0xc0, 0xd0, 0x6e, 0x46, 0x0d, 0xed, 0x2b, 0x29, 0x56, 0x31,
-	0xc0, 0xc0, 0xfe, 0x4f, 0x56, 0xcf, 0x3e, 0x88, 0x1c, 0x9b, 0xc4, 0xab, 0xeb, 0x90, 0x28, 0x3c,
-	0x25, 0xa2, 0x11, 0x2b, 0x5a, 0x70, 0xb6, 0xc7, 0x4f, 0xe1, 0x6c, 0xff, 0x9a, 0xaa, 0x36, 0xa3,
-	0x8c, 0xd3, 0xfa, 0xad, 0x20, 0xf6, 0xc9, 0xa6, 0x2e, 0x9b, 0xd3, 0xa5, 0x7d, 0xe1, 0x03, 0x09,
-	0x8e, 0xa1, 0xe2, 0x3e, 0x3e, 0x22, 0x1e, 0xea, 0xc6, 0x8d, 0x99, 0x8e, 0x13, 0xde, 0x1c, 0xd1,
-	0x72, 0xaa, 0x78, 0xa8, 0xaf, 0x19, 0xf7, 0x33, 0x42, 0x4d, 0x98, 0x34, 0x0b, 0x7e, 0xb5, 0x9e,
-	0xae, 0xa4, 0xaf, 0x2c, 0x56, 0xaf, 0xf8, 0x66, 0x0b, 0x8e, 0x20, 0xdb, 0x07, 0x79, 0x28, 0x1a,
-	0x8a, 0x1d, 0xcb, 0x65, 0x4e, 0x9d, 0x4e, 0x2e, 0x33, 0x39, 0xf2, 0x2e, 0x8e, 0x14, 0x79, 0x5f,
-	0x8e, 0x46, 0xde, 0xcf, 0xc6, 0x23, 0x6f, 0x90, 0xab, 0x8b, 0x44, 0xdd, 0x0c, 0xce, 0xeb, 0x10,
-	0xd4, 0xaf, 0xdc, 0x56, 0xc9, 0x8c, 0x91, 0x03, 0x5d, 0x24, 0x3c, 0xcb, 0x5b, 0x11, 0x48, 0x1c,
-	0x63, 0x21, 0x3c, 0x53, 0xdd, 0x52, 0xed, 0xb5, 0xdb, 0xc4, 0xdb, 0x9f, 0x9f, 0x94, 0x13, 0x0e,
-	0x3c, 0xd3, 0x5b, 0x11, 0x2a, 0x8e, 0xf5, 0x46, 0x9b, 0x90, 0x57, 0x11, 0xac, 0xae, 0x06, 0x7e,
-	0x35, 0x4d, 0x70, 0xac, 0x3c, 0x73, 0xf5, 0x37, 0xd6, 0x38, 0x66, 0xf2, 0xa1, 0x70, 0x44, 0xf2,
-	0xe1, 0x2e, 0x20, 0x77, 0x5b, 0xc6, 0x00, 0xf5, 0xdb, 0xea, 0x13, 0x09, 0x42, 0x2b, 0xf3, 0x32,
-	0xb2, 0x0d, 0x36, 0xec, 0x5e, 0x5f, 0x0f, 0x9c, 0x30, 0x4a, 0x9c, 0x6a, 0x1d, 0xf6, 0x06, 0x47,
-	0x41, 0x27, 0x1a, 0xd2, 0x86, 0x4d, 0x61, 0x1c, 0x27, 0xab, 0x49, 0x2b, 0x31, 0x54, 0xdc, 0xc7,
-	0x07, 0x7d, 0x02, 0x53, 0x42, 0x85, 0x42, 0xc6, 0x70, 0x4c, 0xc6, 0xb3, 0x87, 0x07, 0x4b, 0x53,
-	0x1b, 0x26, 0x24, 0x8e, 0x72, 0xb0, 0xff, 0x30, 0x0b, 0xc9, 0x41, 0x77, 0xf8, 0x43, 0x16, 0xeb,
-	0x09, 0x3f, 0x64, 0x79, 0x1f, 0x0a, 0x8c, 0x13, 0x4f, 0xfd, 0xa2, 0x28, 0x33, 0xda, 0x2f, 0x8a,
-	0xaa, 0x3e, 0x00, 0x0e, 0xb1, 0x62, 0x19, 0x90, 0xec, 0x89, 0x66, 0x40, 0x56, 0x00, 0x64, 0x5c,
-	0x55, 0x71, 0x7b, 0xfa, 0x75, 0x7a, 0x2a, 0xb4, 0x09, 0x37, 0x03, 0x0a, 0x36, 0x7a, 0xa1, 0x6b,
-	0xc1, 0xa5, 0xac, 0x9e, 0xa3, 0x2f, 0xf5, 0xd5, 0x00, 0xc5, 0x73, 0x68, 0x09, 0xdf, 0x01, 0x38,
-	0xa2, 0x66, 0xd0, 0xfe, 0xbf, 0x0c, 0x44, 0x8c, 0x21, 0xfa, 0xae, 0x05, 0xb3, 0x24, 0xf6, 0xb1,
-	0x05, 0xdf, 0x97, 0xfc, 0xa5, 0x74, 0x5f, 0xc0, 0xe8, 0xfb, 0x56, 0x43, 0xf8, 0x32, 0x16, 0xef,
-	0xc2, 0x70, 0x3f, 0x53, 0xf4, 0x7b, 0x16, 0x5c, 0x20, 0xfd, 0x5f, 0xd3, 0xd0, 0x9b, 0xfe, 0xd6,
-	0xc8, 0x9f, 0xe3, 0x28, 0x7f, 0xed, 0xf0, 0x60, 0x29, 0xe9, 0x3b, 0x23, 0x38, 0x89, 0x1d, 0xfa,
-	0x10, 0xc6, 0x88, 0xd7, 0xf0, 0x53, 0xb0, 0xe9, 0xd9, 0xfa, 0x1f, 0x49, 0x09, 0x6f, 0xf4, 0x55,
-	0xaf, 0xc1, 0xb0, 0x04, 0xb5, 0x7f, 0x9c, 0x85, 0x99, 0xf8, 0x0f, 0x5f, 0x74, 0x59, 0xe9, 0x58,
-	0x62, 0x59, 0xa9, 0x38, 0x23, 0x35, 0x1e, 0xd4, 0x78, 0x86, 0x67, 0x44, 0x34, 0x62, 0x45, 0x0b,
-	0xce, 0x88, 0x2c, 0x47, 0xcf, 0x1d, 0xe3, 0x8c, 0xc8, 0x1a, 0xf4, 0x10, 0x0b, 0x5d, 0x8b, 0xde,
-	0x2d, 0x76, 0xfc, 0x6e, 0x99, 0x35, 0xd7, 0x32, 0x6a, 0x62, 0xb7, 0x0d, 0x45, 0x63, 0x1f, 0xf4,
-	0x49, 0xbc, 0x9e, 0x5a, 0xee, 0xa1, 0xda, 0x4d, 0xab, 0x4a, 0xa4, 0x90, 0x62, 0xe2, 0x87, 0xe7,
-	0x5e, 0x4a, 0xeb, 0x58, 0x99, 0x4f, 0x29, 0x2e, 0x03, 0xcd, 0xfe, 0x0f, 0x0b, 0xa6, 0x22, 0xb5,
-	0xce, 0x82, 0x9b, 0x5f, 0x53, 0x3e, 0xfa, 0x97, 0x45, 0xee, 0x07, 0x08, 0xd8, 0x40, 0x43, 0xdf,
-	0x82, 0x62, 0xcb, 0xed, 0x34, 0x28, 0xe3, 0x55, 0x97, 0xec, 0xea, 0x73, 0x92, 0x36, 0x8d, 0x34,
-	0x7f, 0x78, 0xb0, 0x34, 0xb7, 0xa1, 0x60, 0x2a, 0x6e, 0xbb, 0xdb, 0xa2, 0x5c, 0xfd, 0xfa, 0x00,
-	0x9b, 0xe0, 0xf2, 0x75, 0xf4, 0x7d, 0xe2, 0xd1, 0xa6, 0xdb, 0x63, 0xf4, 0x69, 0x7d, 0x1d, 0x0d,
-	0x26, 0x78, 0xd2, 0xaf, 0xa3, 0x21, 0xf0, 0xd1, 0xaf, 0xa3, 0x41, 0xdf, 0xa7, 0xf6, 0x75, 0x34,
-	0x98, 0xe1, 0x80, 0x28, 0xe8, 0x7f, 0x33, 0xc6, 0x2a, 0xa2, 0x91, 0x50, 0xe6, 0x09, 0x91, 0xd0,
-	0x47, 0x30, 0xe1, 0x74, 0x38, 0xf5, 0xf6, 0x48, 0x4b, 0xa7, 0x81, 0xd3, 0xea, 0x62, 0xb0, 0xd4,
-	0x75, 0x8d, 0x83, 0x03, 0x44, 0xd4, 0x82, 0x8b, 0xfe, 0xb3, 0x89, 0x47, 0x49, 0xf8, 0x6e, 0xab,
-	0x8b, 0xd4, 0xde, 0xf0, 0xf3, 0xfb, 0xb7, 0x92, 0x3a, 0x3d, 0x1e, 0x44, 0xc0, 0xc9, 0xa0, 0x88,
-	0xc1, 0x14, 0x33, 0x52, 0x00, 0xfe, 0x8d, 0x38, 0xe4, 0x93, 0x53, 0x3c, 0x6b, 0x62, 0x54, 0xb6,
-	0x99, 0xa0, 0x38, 0xca, 0xc3, 0xfe, 0xd7, 0x2c, 0x4c, 0xc7, 0x34, 0x2d, 0x16, 0x8e, 0x14, 0xce,
-	0x32, 0x1c, 0xc9, 0x8f, 0x14, 0x8e, 0x24, 0x7b, 0xca, 0x63, 0x23, 0x79, 0xca, 0x6f, 0x2b, 0x6f,
-	0x55, 0xef, 0xdc, 0xfa, 0x9a, 0xfe, 0xf5, 0x42, 0x20, 0xcd, 0x0d, 0x93, 0x88, 0xa3, 0x7d, 0xa5,
-	0x3b, 0x51, 0xef, 0xff, 0x4a, 0x85, 0x76, 0xb5, 0xdf, 0x4a, 0x5b, 0xc9, 0x19, 0x00, 0x28, 0x77,
-	0x22, 0x81, 0x80, 0x93, 0xd8, 0x95, 0xef, 0x7e, 0xfe, 0xd5, 0xe2, 0xb9, 0x2f, 0xbe, 0x5a, 0x3c,
-	0xf7, 0xe5, 0x57, 0x8b, 0xe7, 0xbe, 0x73, 0xb8, 0x68, 0x7d, 0x7e, 0xb8, 0x68, 0x7d, 0x71, 0xb8,
-	0x68, 0x7d, 0x79, 0xb8, 0x68, 0xfd, 0xe4, 0x70, 0xd1, 0xfa, 0xde, 0x4f, 0x17, 0xcf, 0x3d, 0x78,
-	0x61, 0x98, 0x6f, 0xdb, 0xfd, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x4e, 0x36, 0x5e, 0x9a, 0x02,
-	0x4f, 0x00, 0x00,
+	// 4332 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5c, 0x4d, 0x6c, 0x1c, 0x47,
+	0x76, 0x56, 0xcf, 0x90, 0x43, 0xce, 0x1b, 0x92, 0x22, 0x4b, 0x94, 0xcd, 0xa5, 0x63, 0x51, 0x69,
+	0x1b, 0x86, 0x1d, 0xdb, 0xc3, 0x88, 0xb2, 0x6c, 0x59, 0x32, 0x14, 0x70, 0x86, 0xfa, 0xa1, 0x96,
+	0x6b, 0x31, 0x35, 0xb2, 0xbc, 0x96, 0x6d, 0x28, 0xc5, 0x99, 0xe2, 0x4c, 0x2f, 0x67, 0xba, 0xc7,
+	0x5d, 0x35, 0xb4, 0x98, 0x04, 0xc9, 0xe6, 0x17, 0x8b, 0x24, 0x08, 0x0c, 0x64, 0x03, 0xef, 0x21,
+	0x41, 0x82, 0xe4, 0x14, 0x2c, 0x90, 0x6b, 0x0e, 0x39, 0xf8, 0x90, 0x8b, 0x93, 0x6c, 0x02, 0x63,
+	0x73, 0x88, 0x03, 0x2c, 0x98, 0x35, 0x17, 0xc8, 0x39, 0x97, 0x5c, 0x04, 0x04, 0x08, 0xea, 0xa7,
+	0xbb, 0xab, 0x7b, 0x7a, 0xc4, 0xe9, 0x11, 0xa9, 0x08, 0xbe, 0x91, 0xf5, 0xaa, 0xbe, 0x57, 0x3f,
+	0xef, 0xbd, 0x7a, 0xef, 0xd5, 0xeb, 0x81, 0xd7, 0x9a, 0x0e, 0x6f, 0xf5, 0xb6, 0xca, 0x75, 0xaf,
+	0xb3, 0x4c, 0x76, 0x7a, 0x0e, 0xdf, 0x5b, 0xde, 0x21, 0x7e, 0xd3, 0x5b, 0x26, 0x5d, 0x67, 0x79,
+	0xf7, 0x1c, 0x69, 0x77, 0x5b, 0xe4, 0xdc, 0x72, 0x93, 0xba, 0xd4, 0x27, 0x9c, 0x36, 0xca, 0x5d,
+	0xdf, 0xe3, 0x1e, 0x7a, 0x3e, 0x1a, 0x55, 0x56, 0xa3, 0xca, 0x72, 0x54, 0x99, 0x74, 0x9d, 0x72,
+	0x30, 0x6a, 0xf1, 0x55, 0x03, 0xbb, 0xe9, 0x35, 0xbd, 0x65, 0x39, 0x78, 0xab, 0xb7, 0x2d, 0xff,
+	0x93, 0xff, 0xc8, 0xbf, 0x14, 0xe8, 0xe2, 0x8d, 0x9d, 0x8b, 0xac, 0xec, 0x48, 0xce, 0xf4, 0x3e,
+	0xa7, 0x2e, 0x73, 0x3c, 0x97, 0xbd, 0x4a, 0xba, 0x0e, 0xa3, 0xfe, 0x2e, 0xf5, 0x97, 0xbb, 0x3b,
+	0x4d, 0x41, 0x63, 0xf1, 0x0e, 0xcb, 0xbb, 0x7d, 0xd3, 0x5b, 0x7c, 0x2d, 0x42, 0xea, 0x90, 0x7a,
+	0xcb, 0x71, 0xa9, 0xbf, 0x17, 0x0d, 0xef, 0x50, 0x4e, 0xd2, 0x46, 0x2d, 0x0f, 0x1a, 0xe5, 0xf7,
+	0x5c, 0xee, 0x74, 0x68, 0xdf, 0x80, 0xd7, 0x0f, 0x1b, 0xc0, 0xea, 0x2d, 0xda, 0x21, 0xc9, 0x71,
+	0xf6, 0x07, 0x70, 0x6a, 0xd5, 0x25, 0xed, 0x3d, 0xe6, 0x30, 0xdc, 0x73, 0x57, 0xfd, 0x66, 0xaf,
+	0x43, 0x5d, 0x8e, 0xce, 0xc2, 0x98, 0x4b, 0x3a, 0x74, 0xc1, 0x3a, 0x6b, 0xbd, 0x58, 0xac, 0x4c,
+	0x7d, 0xbe, 0xbf, 0x74, 0xe2, 0x60, 0x7f, 0x69, 0xec, 0x6d, 0xd2, 0xa1, 0x58, 0x52, 0xd0, 0x73,
+	0x30, 0xbe, 0x4b, 0xda, 0x3d, 0xba, 0x90, 0x93, 0x5d, 0xa6, 0x75, 0x97, 0xf1, 0x3b, 0xa2, 0x11,
+	0x2b, 0x9a, 0xfd, 0x3b, 0xf9, 0x18, 0xfc, 0xb7, 0x28, 0x27, 0x0d, 0xc2, 0x09, 0xea, 0x40, 0xa1,
+	0x4d, 0xb6, 0x68, 0x9b, 0x2d, 0x58, 0x67, 0xf3, 0x2f, 0x96, 0x56, 0xae, 0x96, 0x87, 0x39, 0xc4,
+	0x72, 0x0a, 0x54, 0x79, 0x43, 0xe2, 0x5c, 0x75, 0xb9, 0xbf, 0x57, 0x99, 0xd1, 0x93, 0x28, 0xa8,
+	0x46, 0xac, 0x99, 0xa0, 0xdf, 0xb2, 0xa0, 0x44, 0x5c, 0xd7, 0xe3, 0x84, 0x8b, 0x63, 0x5a, 0xc8,
+	0x49, 0xa6, 0x37, 0x47, 0x67, 0xba, 0x1a, 0x81, 0x29, 0xce, 0xa7, 0x34, 0xe7, 0x92, 0x41, 0xc1,
+	0x26, 0xcf, 0xc5, 0x37, 0xa1, 0x64, 0x4c, 0x15, 0xcd, 0x42, 0x7e, 0x87, 0xee, 0xa9, 0xfd, 0xc5,
+	0xe2, 0x4f, 0x34, 0x1f, 0xdb, 0x50, 0xbd, 0x83, 0x97, 0x72, 0x17, 0xad, 0xc5, 0x2b, 0x30, 0x9b,
+	0x64, 0x98, 0x65, 0xbc, 0xfd, 0xc7, 0x16, 0xcc, 0x1b, 0xab, 0xc0, 0x74, 0x9b, 0xfa, 0xd4, 0xad,
+	0x53, 0xb4, 0x0c, 0x45, 0x71, 0x96, 0xac, 0x4b, 0xea, 0xc1, 0x51, 0xcf, 0xe9, 0x85, 0x14, 0xdf,
+	0x0e, 0x08, 0x38, 0xea, 0x13, 0x8a, 0x45, 0xee, 0x61, 0x62, 0xd1, 0x6d, 0x11, 0x46, 0x17, 0xf2,
+	0x71, 0xb1, 0xd8, 0x14, 0x8d, 0x58, 0xd1, 0xec, 0x7b, 0xf0, 0x8d, 0x60, 0x3e, 0xb7, 0x69, 0xa7,
+	0xdb, 0x26, 0x9c, 0x46, 0x93, 0x3a, 0x5c, 0xf4, 0xce, 0xc2, 0xd8, 0x8e, 0xe3, 0x36, 0x92, 0xb3,
+	0xf8, 0xa6, 0xe3, 0x36, 0xb0, 0xa4, 0xd8, 0x3b, 0x30, 0xbd, 0xda, 0xed, 0xfa, 0xde, 0x2e, 0x6d,
+	0xd4, 0x38, 0x69, 0x52, 0x74, 0x17, 0x80, 0xe8, 0x86, 0x55, 0x2e, 0xa1, 0x4b, 0x2b, 0xbf, 0x50,
+	0x56, 0x3a, 0x53, 0x36, 0x75, 0xa6, 0xdc, 0xdd, 0x69, 0x8a, 0x06, 0x56, 0x16, 0xaa, 0x59, 0xde,
+	0x3d, 0x57, 0xbe, 0xed, 0x74, 0x68, 0x65, 0xe6, 0x60, 0x7f, 0x09, 0x56, 0x43, 0x04, 0x6c, 0xa0,
+	0xd9, 0xbf, 0x6d, 0xc1, 0xe9, 0x55, 0xbf, 0xe9, 0x55, 0xd7, 0x56, 0xbb, 0xdd, 0x1b, 0x94, 0xb4,
+	0x79, 0xab, 0xc6, 0x09, 0xef, 0x31, 0x74, 0x05, 0x0a, 0x4c, 0xfe, 0xa5, 0x17, 0xf3, 0x42, 0x20,
+	0x9f, 0x8a, 0xfe, 0x60, 0x7f, 0x69, 0x3e, 0x65, 0x20, 0xc5, 0x7a, 0x14, 0x7a, 0x09, 0x26, 0x3a,
+	0x94, 0x31, 0xd2, 0x0c, 0x76, 0xfc, 0xa4, 0x06, 0x98, 0xf8, 0x96, 0x6a, 0xc6, 0x01, 0xdd, 0xfe,
+	0xa7, 0x1c, 0x9c, 0x0c, 0xb1, 0x34, 0xfb, 0x63, 0x38, 0xde, 0x1e, 0x4c, 0xb5, 0x8c, 0x15, 0xca,
+	0x53, 0x2e, 0xad, 0x5c, 0x1e, 0x52, 0x93, 0xd2, 0x36, 0xa9, 0x32, 0xaf, 0xd9, 0x4c, 0x99, 0xad,
+	0x38, 0xc6, 0x06, 0x75, 0x00, 0xd8, 0x9e, 0x5b, 0xd7, 0x4c, 0xc7, 0x24, 0xd3, 0x37, 0x33, 0x32,
+	0xad, 0x85, 0x00, 0x15, 0xa4, 0x59, 0x42, 0xd4, 0x86, 0x0d, 0x06, 0xf6, 0xdf, 0x5a, 0x70, 0x2a,
+	0x65, 0x1c, 0x7a, 0x2b, 0x71, 0x9e, 0xcf, 0xf7, 0x9d, 0x27, 0xea, 0x1b, 0x16, 0x9d, 0xe6, 0x2b,
+	0x30, 0xe9, 0xd3, 0x5d, 0x47, 0xdc, 0x14, 0x7a, 0x87, 0x67, 0xf5, 0xf8, 0x49, 0xac, 0xdb, 0x71,
+	0xd8, 0x03, 0xbd, 0x0c, 0xc5, 0xe0, 0x6f, 0xb1, 0xcd, 0x79, 0xa1, 0x4c, 0xe2, 0xe0, 0x82, 0xae,
+	0x0c, 0x47, 0x74, 0xfb, 0x37, 0x61, 0xbc, 0xda, 0x22, 0x3e, 0x17, 0x12, 0xe3, 0xd3, 0xae, 0xf7,
+	0x0e, 0xde, 0xd0, 0x53, 0x0c, 0x25, 0x06, 0xab, 0x66, 0x1c, 0xd0, 0x87, 0x38, 0xec, 0x97, 0x60,
+	0x62, 0x97, 0xfa, 0x72, 0xbe, 0xf9, 0x38, 0xd8, 0x1d, 0xd5, 0x8c, 0x03, 0xba, 0xfd, 0x6f, 0x16,
+	0xcc, 0xcb, 0x19, 0xac, 0x39, 0xac, 0xee, 0xed, 0x52, 0x7f, 0x0f, 0x53, 0xd6, 0x6b, 0x1f, 0xf1,
+	0x84, 0xd6, 0x60, 0x96, 0xd1, 0xce, 0x2e, 0xf5, 0xab, 0x9e, 0xcb, 0xb8, 0x4f, 0x1c, 0x97, 0xeb,
+	0x99, 0x2d, 0xe8, 0xde, 0xb3, 0xb5, 0x04, 0x1d, 0xf7, 0x8d, 0x40, 0x2f, 0xc2, 0xa4, 0x9e, 0xb6,
+	0x10, 0x25, 0xb1, 0xb1, 0x53, 0xe2, 0x0c, 0xf4, 0x9a, 0x18, 0x0e, 0xa9, 0xf6, 0x7f, 0x59, 0x30,
+	0x27, 0x57, 0x55, 0xeb, 0x6d, 0xb1, 0xba, 0xef, 0x74, 0x85, 0x01, 0x7e, 0x12, 0x97, 0x74, 0x05,
+	0x66, 0x1a, 0xc1, 0xc6, 0x6f, 0x38, 0x1d, 0x87, 0x4b, 0x1d, 0x19, 0xaf, 0x3c, 0xa5, 0x31, 0x66,
+	0xd6, 0x62, 0x54, 0x9c, 0xe8, 0xad, 0x8e, 0xaf, 0xdd, 0x63, 0x9c, 0xfa, 0x9b, 0xbe, 0xd7, 0xf1,
+	0xc4, 0x3a, 0x6f, 0x13, 0xb6, 0x83, 0x7e, 0x05, 0x26, 0x3b, 0xfa, 0xd2, 0xd3, 0x56, 0xf3, 0x17,
+	0x87, 0xb3, 0x9a, 0xb7, 0xb6, 0xbe, 0x43, 0xeb, 0x5c, 0x5c, 0x98, 0x91, 0xb6, 0x45, 0x6d, 0x38,
+	0x44, 0x45, 0xef, 0xc1, 0x18, 0xeb, 0xd2, 0xba, 0xdc, 0xa2, 0xd2, 0xca, 0x1b, 0xc3, 0x29, 0x75,
+	0x6c, 0x92, 0xb5, 0x2e, 0xad, 0x47, 0x7b, 0x2b, 0xfe, 0xc3, 0x12, 0xd2, 0xfe, 0x0f, 0x0b, 0x16,
+	0xd2, 0x56, 0xb5, 0xe1, 0x30, 0x8e, 0x3e, 0xe8, 0x5b, 0x59, 0x79, 0xb8, 0x95, 0x89, 0xd1, 0x72,
+	0x5d, 0xa1, 0xf6, 0x06, 0x2d, 0xc6, 0xaa, 0xee, 0xc1, 0xb8, 0xc3, 0x69, 0x27, 0x70, 0x35, 0x2e,
+	0x0d, 0xb7, 0xac, 0xb4, 0xc9, 0x46, 0x57, 0xe8, 0xba, 0x00, 0xc4, 0x0a, 0xd7, 0x7e, 0x1f, 0xa6,
+	0xaa, 0x3d, 0xdf, 0xa7, 0x2e, 0x57, 0x17, 0xdc, 0x37, 0x61, 0x9c, 0x39, 0xae, 0xb6, 0xf3, 0xd9,
+	0xee, 0xb6, 0xa2, 0x00, 0xaf, 0x89, 0xc1, 0x58, 0x61, 0xd8, 0x7f, 0x96, 0x87, 0x53, 0x81, 0xc4,
+	0xd0, 0xc6, 0xaa, 0xcf, 0x9d, 0x6d, 0x52, 0xe7, 0x0c, 0x35, 0x60, 0xaa, 0x11, 0x35, 0x73, 0x6d,
+	0x88, 0xb3, 0xf0, 0x0a, 0x8d, 0xbd, 0x01, 0xcf, 0x71, 0x0c, 0x15, 0xbd, 0x0b, 0xf9, 0xa6, 0xc3,
+	0xb5, 0x67, 0x78, 0x71, 0xb8, 0x9d, 0xbb, 0xee, 0x24, 0x2d, 0x4f, 0xa5, 0xa4, 0x59, 0xe5, 0xaf,
+	0x3b, 0x1c, 0x0b, 0x44, 0xb4, 0x05, 0x05, 0xa7, 0x43, 0x9a, 0x34, 0xe3, 0xa9, 0xac, 0x8b, 0x31,
+	0x49, 0xf4, 0xd0, 0xd5, 0x94, 0x54, 0x86, 0x35, 0xb2, 0xe0, 0x51, 0x17, 0x16, 0x43, 0xd9, 0xec,
+	0xe1, 0x4f, 0x3e, 0xc5, 0x76, 0x46, 0x3c, 0x24, 0x95, 0x61, 0x8d, 0x6c, 0x7f, 0x99, 0x83, 0xd9,
+	0x68, 0xff, 0xaa, 0x5e, 0xa7, 0xe3, 0x70, 0xb4, 0x08, 0x39, 0xa7, 0xa1, 0x0d, 0x12, 0xe8, 0x81,
+	0xb9, 0xf5, 0x35, 0x9c, 0x73, 0x1a, 0xe8, 0x05, 0x28, 0x6c, 0xf9, 0xc4, 0xad, 0xb7, 0xb4, 0x21,
+	0x0a, 0x81, 0x2b, 0xb2, 0x15, 0x6b, 0x2a, 0x7a, 0x16, 0xf2, 0x9c, 0x34, 0xb5, 0xfd, 0x09, 0xf7,
+	0xef, 0x36, 0x69, 0x62, 0xd1, 0x2e, 0x0c, 0x1f, 0xeb, 0x49, 0x1d, 0x96, 0x27, 0x6f, 0x18, 0xbe,
+	0x9a, 0x6a, 0xc6, 0x01, 0x5d, 0x70, 0x24, 0x3d, 0xde, 0xf2, 0xfc, 0x85, 0xf1, 0x38, 0xc7, 0x55,
+	0xd9, 0x8a, 0x35, 0x55, 0xb8, 0x28, 0x75, 0x39, 0x7f, 0x4e, 0xfd, 0x85, 0x42, 0xdc, 0x45, 0xa9,
+	0x06, 0x04, 0x1c, 0xf5, 0x41, 0x1f, 0x42, 0xa9, 0xee, 0x53, 0xc2, 0x3d, 0x7f, 0x8d, 0x70, 0xba,
+	0x30, 0x91, 0x59, 0x02, 0x4f, 0x0a, 0x2f, 0xbd, 0x1a, 0x41, 0x60, 0x13, 0xcf, 0xfe, 0xbb, 0x3c,
+	0x2c, 0x44, 0x5b, 0x2b, 0xcf, 0x36, 0xf2, 0x4c, 0xf5, 0xf6, 0x58, 0x03, 0xb6, 0xe7, 0x05, 0x28,
+	0x34, 0x9c, 0x26, 0x65, 0x3c, 0xb9, 0xcb, 0x6b, 0xb2, 0x15, 0x6b, 0x2a, 0xfa, 0xfd, 0x44, 0x34,
+	0x32, 0x2e, 0x05, 0xe5, 0xd6, 0x70, 0x82, 0x32, 0x68, 0x72, 0x23, 0x84, 0x24, 0x68, 0x05, 0xa0,
+	0xe9, 0x70, 0x7d, 0x69, 0xe9, 0x53, 0x0f, 0x8d, 0xf5, 0xf5, 0x90, 0x82, 0x8d, 0x5e, 0xe8, 0x5d,
+	0x28, 0xca, 0xfd, 0x1a, 0x51, 0xff, 0xa5, 0x0b, 0x53, 0x0d, 0x00, 0x70, 0x84, 0xf5, 0xc8, 0x41,
+	0xce, 0xfb, 0x80, 0xae, 0xde, 0xef, 0xfa, 0x94, 0x89, 0xab, 0xfb, 0x0e, 0xf1, 0x1d, 0xb2, 0xd5,
+	0xa6, 0x47, 0x15, 0xc7, 0x7e, 0x31, 0x06, 0x13, 0xd7, 0x7c, 0xea, 0x34, 0x5b, 0xfc, 0x31, 0x5c,
+	0x89, 0xcf, 0xc1, 0x38, 0x69, 0x3b, 0x84, 0x49, 0xe9, 0x36, 0xa6, 0xb4, 0x2a, 0x1a, 0xb1, 0xa2,
+	0xa1, 0xf7, 0xa1, 0xe0, 0xf9, 0x4e, 0xd3, 0x71, 0x17, 0x8a, 0x72, 0x12, 0xe7, 0x87, 0x93, 0x1f,
+	0xbd, 0x8a, 0x5b, 0x72, 0x68, 0x24, 0xa2, 0xea, 0x7f, 0xac, 0x21, 0xd1, 0x5d, 0x98, 0x50, 0x2a,
+	0x17, 0x98, 0xb1, 0xe5, 0xa1, 0xcd, 0xb0, 0xd2, 0xda, 0xc8, 0x34, 0xa8, 0xff, 0x19, 0x0e, 0x00,
+	0x51, 0x2d, 0xb4, 0xc2, 0x63, 0x12, 0xfa, 0xe5, 0x0c, 0x56, 0x78, 0xa0, 0xd9, 0xad, 0x85, 0x66,
+	0x77, 0x3c, 0x0b, 0xa8, 0x34, 0xac, 0x83, 0xec, 0xac, 0xd8, 0x62, 0xed, 0xee, 0x17, 0x46, 0xd8,
+	0x62, 0x1d, 0x6b, 0xcc, 0xc4, 0x63, 0x84, 0x20, 0x1a, 0xb0, 0xbf, 0x9f, 0x87, 0x39, 0xdd, 0xb3,
+	0xea, 0xb5, 0xdb, 0xb4, 0x2e, 0x7d, 0x4b, 0x65, 0xc5, 0xf3, 0xa9, 0x56, 0xdc, 0x09, 0x7c, 0x0a,
+	0x75, 0x33, 0x56, 0x32, 0xcd, 0x26, 0xe2, 0x51, 0x96, 0x7e, 0x84, 0xb2, 0x11, 0xe1, 0x29, 0xe9,
+	0x5e, 0xda, 0xbb, 0x40, 0xbf, 0x67, 0xc1, 0xa9, 0x5d, 0xea, 0x3b, 0xdb, 0x4e, 0x5d, 0xea, 0xe3,
+	0x0d, 0x87, 0x71, 0xcf, 0xdf, 0xd3, 0xf7, 0xe6, 0xeb, 0xc3, 0x71, 0xbe, 0x63, 0x00, 0xac, 0xbb,
+	0xdb, 0x5e, 0xe5, 0x19, 0xcd, 0xed, 0xd4, 0x9d, 0x7e, 0x68, 0x9c, 0xc6, 0x6f, 0xb1, 0x0b, 0x10,
+	0xcd, 0x36, 0xc5, 0x1c, 0x6c, 0x98, 0xca, 0x3b, 0xf4, 0xc4, 0x82, 0xc5, 0x06, 0xb6, 0xd3, 0x34,
+	0x23, 0x9f, 0x59, 0x50, 0xd2, 0xf4, 0xc7, 0xe0, 0x26, 0xe2, 0xb8, 0x9b, 0xf8, 0x6a, 0xa6, 0xf9,
+	0x0f, 0xf0, 0x0c, 0x7d, 0x98, 0x8e, 0x29, 0x39, 0xba, 0xa0, 0xd3, 0x25, 0xca, 0x06, 0xfe, 0xbc,
+	0x99, 0x2e, 0x79, 0xb0, 0xbf, 0x34, 0x17, 0xeb, 0x1c, 0xe5, 0x50, 0x0e, 0x8f, 0x5d, 0x2e, 0x4d,
+	0xfe, 0xe0, 0x2f, 0x97, 0x4e, 0x7c, 0xf7, 0x27, 0x67, 0x4f, 0xd8, 0x9f, 0xe6, 0x61, 0x36, 0xb9,
+	0xab, 0x43, 0xd8, 0xde, 0xc8, 0x86, 0x4d, 0x1e, 0xab, 0x0d, 0xcb, 0x1d, 0x9f, 0x0d, 0xcb, 0x1f,
+	0x87, 0x0d, 0x1b, 0x3b, 0x32, 0x1b, 0x66, 0xff, 0xab, 0x05, 0x33, 0xe1, 0xc9, 0x7c, 0xd4, 0x13,
+	0xfe, 0x47, 0xb4, 0xeb, 0xd6, 0xd1, 0xef, 0xfa, 0x3d, 0x98, 0x60, 0x5e, 0xcf, 0xaf, 0x4b, 0x27,
+	0x5b, 0xa0, 0xbf, 0x96, 0xcd, 0x68, 0xaa, 0xb1, 0x86, 0x67, 0xa9, 0x1a, 0x70, 0x80, 0x6a, 0x7f,
+	0x96, 0x0b, 0x17, 0xa4, 0x69, 0xca, 0xf1, 0xf2, 0x85, 0x5b, 0x2a, 0x16, 0x34, 0x69, 0x3a, 0x5e,
+	0xa2, 0x15, 0x6b, 0x2a, 0xb2, 0xa5, 0x3d, 0x0f, 0xfc, 0xff, 0x62, 0x05, 0xb4, 0x59, 0x96, 0x87,
+	0xa0, 0x28, 0xa8, 0x0b, 0xb3, 0x3e, 0xfd, 0xa8, 0xe7, 0xf8, 0xb4, 0x51, 0xf3, 0xc8, 0x8e, 0x70,
+	0x5a, 0x74, 0x92, 0x6b, 0x48, 0xbd, 0x5f, 0xeb, 0xf9, 0xd2, 0x84, 0x55, 0xe6, 0x45, 0xec, 0x8e,
+	0x13, 0x58, 0xb8, 0x0f, 0x1d, 0x79, 0x30, 0x4f, 0x76, 0x89, 0xd3, 0x26, 0x5b, 0x4e, 0xdb, 0xe1,
+	0x7b, 0x35, 0xee, 0x13, 0x4e, 0x9b, 0x7b, 0xda, 0xc5, 0xbe, 0xac, 0xd7, 0x32, 0xbf, 0x9a, 0xd2,
+	0xe7, 0xc1, 0xfe, 0xd2, 0x33, 0x7a, 0x2f, 0xd2, 0xc8, 0x38, 0x15, 0xd8, 0xfe, 0xcf, 0xf1, 0xd0,
+	0x42, 0xe8, 0xbc, 0xd6, 0xaf, 0x41, 0xa9, 0xae, 0x82, 0xc9, 0xf6, 0xde, 0xba, 0xab, 0x65, 0x7a,
+	0x6d, 0x84, 0xdb, 0xae, 0x5c, 0x8d, 0x60, 0x12, 0x5e, 0xa8, 0x41, 0xc1, 0x26, 0x37, 0xf4, 0x31,
+	0x80, 0x32, 0xfd, 0xb4, 0xb1, 0xee, 0xea, 0xbb, 0xad, 0x3a, 0x0a, 0xef, 0x3b, 0x21, 0x8a, 0x62,
+	0x1d, 0x3a, 0x59, 0x11, 0x01, 0x1b, 0xac, 0xc4, 0xaa, 0x83, 0x2c, 0xee, 0x35, 0xcf, 0xd7, 0x46,
+	0x62, 0xa4, 0x55, 0xaf, 0x46, 0x30, 0x49, 0xdf, 0x3b, 0xa2, 0x60, 0x93, 0xdb, 0xa2, 0x0f, 0xb3,
+	0xc9, 0xbd, 0x4a, 0xb9, 0xdf, 0x6e, 0xc4, 0xef, 0xb7, 0x95, 0x21, 0x2d, 0x82, 0x91, 0x18, 0x30,
+	0xdf, 0x11, 0x7c, 0x38, 0x99, 0xd8, 0xa3, 0x14, 0x96, 0xeb, 0x71, 0x96, 0xe7, 0xb3, 0xdc, 0xf5,
+	0x3a, 0xdb, 0x6e, 0xf2, 0x64, 0x30, 0x9b, 0xdc, 0x9d, 0x23, 0x63, 0x1a, 0x4b, 0xf1, 0x9b, 0x97,
+	0xf8, 0x9f, 0xe7, 0xa0, 0x18, 0x9a, 0xf1, 0x2c, 0xf9, 0x3a, 0xe5, 0x7e, 0xe5, 0x0e, 0x09, 0xa2,
+	0xf3, 0xc3, 0x04, 0xd1, 0x63, 0x83, 0x83, 0xe8, 0x20, 0xa7, 0x5f, 0x78, 0x78, 0x4e, 0xdf, 0x08,
+	0xa2, 0x27, 0x86, 0x0f, 0xa2, 0x27, 0x0f, 0x0f, 0xa2, 0xed, 0xbf, 0xb2, 0x00, 0xf5, 0x67, 0x4c,
+	0xb2, 0x6c, 0x14, 0x49, 0x5e, 0xae, 0xaf, 0x67, 0x0d, 0x5f, 0x0f, 0xbb, 0x63, 0xed, 0xcf, 0xc6,
+	0xe1, 0xe4, 0x75, 0x67, 0xe4, 0xd4, 0x2b, 0x87, 0xa7, 0x15, 0x52, 0x8d, 0x6a, 0xc7, 0x37, 0xb4,
+	0xac, 0xea, 0x7c, 0x2f, 0xe9, 0xa1, 0x4f, 0x57, 0xd3, 0xbb, 0x3d, 0x18, 0x4c, 0xc2, 0x83, 0xa0,
+	0x87, 0x16, 0x92, 0xcb, 0x30, 0xcd, 0xb8, 0xef, 0xd4, 0xb9, 0x4a, 0xee, 0xb2, 0x85, 0x92, 0xbc,
+	0xb9, 0x4e, 0xeb, 0xee, 0xd3, 0x35, 0x93, 0x88, 0xe3, 0x7d, 0x53, 0x73, 0xc6, 0x63, 0x99, 0x73,
+	0xc6, 0xcb, 0x50, 0x24, 0xed, 0xb6, 0xf7, 0xf1, 0x6d, 0xd2, 0x64, 0x3a, 0x4b, 0x13, 0x4a, 0xcd,
+	0x6a, 0x40, 0xc0, 0x51, 0x1f, 0x54, 0x06, 0x70, 0x9a, 0xae, 0xe7, 0x53, 0x39, 0xa2, 0x20, 0xaf,
+	0x50, 0xf9, 0x2e, 0xb6, 0x1e, 0xb6, 0x62, 0xa3, 0x07, 0xaa, 0xc1, 0x69, 0xc7, 0x65, 0xb4, 0xde,
+	0xf3, 0x69, 0x6d, 0xc7, 0xe9, 0xde, 0xde, 0xa8, 0x49, 0x2b, 0xb1, 0x27, 0xa5, 0x79, 0xb2, 0xf2,
+	0xac, 0x66, 0x76, 0x7a, 0x3d, 0xad, 0x13, 0x4e, 0x1f, 0x8b, 0x5e, 0x83, 0x29, 0xc7, 0xad, 0xb7,
+	0x7b, 0x0d, 0xba, 0x49, 0x78, 0x8b, 0x2d, 0x4c, 0xca, 0x69, 0xcc, 0x1e, 0xec, 0x2f, 0x4d, 0xad,
+	0x1b, 0xed, 0x38, 0xd6, 0x4b, 0x8c, 0xa2, 0xf7, 0x8d, 0x51, 0xc5, 0x68, 0xd4, 0xd5, 0xfb, 0xe6,
+	0x28, 0xb3, 0x57, 0x4a, 0x56, 0x1d, 0x32, 0x65, 0xd5, 0x7f, 0x98, 0x83, 0x82, 0x7a, 0xd4, 0x42,
+	0x17, 0x12, 0x2f, 0x47, 0xcf, 0xf6, 0xbd, 0x1c, 0x95, 0xd2, 0x1e, 0x00, 0x6d, 0x28, 0x38, 0x8c,
+	0xf5, 0xe2, 0x1e, 0xcb, 0xba, 0x6c, 0xc1, 0x9a, 0x22, 0x33, 0x8e, 0x9e, 0xbb, 0xed, 0x34, 0x75,
+	0x3a, 0xe6, 0x8a, 0xe1, 0xa7, 0x44, 0xa5, 0x09, 0xf7, 0xc2, 0xda, 0x85, 0xc8, 0x65, 0x89, 0x75,
+	0x10, 0xbe, 0xcb, 0xcd, 0xda, 0xad, 0xb7, 0x15, 0x8f, 0xaa, 0x44, 0xc4, 0x1a, 0x59, 0xf0, 0xf0,
+	0x7a, 0xbc, 0xdb, 0xe3, 0x52, 0x50, 0x8e, 0x88, 0xc7, 0x2d, 0x89, 0x88, 0x35, 0xb2, 0xfd, 0xa9,
+	0x05, 0x27, 0xd5, 0x1e, 0x54, 0x5b, 0xb4, 0xbe, 0x53, 0xe3, 0xb4, 0x2b, 0x42, 0x88, 0x1e, 0xa3,
+	0x2c, 0x19, 0x42, 0xbc, 0xc3, 0x28, 0xc3, 0x92, 0x62, 0xac, 0x3e, 0x77, 0x5c, 0xab, 0xb7, 0x2f,
+	0x82, 0x71, 0x38, 0xf2, 0x55, 0x56, 0x3d, 0x4e, 0xaa, 0x2b, 0x2c, 0x1f, 0x19, 0x21, 0xd5, 0x6b,
+	0x0f, 0x07, 0x74, 0xfb, 0x20, 0x07, 0xe3, 0xd2, 0xcb, 0xcf, 0x62, 0xb9, 0xe2, 0x69, 0xb9, 0xdc,
+	0x50, 0x69, 0xb9, 0x43, 0x32, 0xb7, 0x51, 0x6a, 0x72, 0xec, 0xa1, 0xa9, 0x49, 0x96, 0x96, 0x99,
+	0x7c, 0x2b, 0x43, 0x70, 0x33, 0x4a, 0x65, 0xc4, 0xa3, 0x66, 0xfe, 0x7e, 0x66, 0xc1, 0x7c, 0x5a,
+	0x8e, 0x3e, 0xcb, 0x9e, 0xbf, 0x02, 0x93, 0xdd, 0x36, 0xe1, 0xdb, 0x9e, 0xdf, 0x49, 0xbe, 0xcd,
+	0x6e, 0xea, 0x76, 0x1c, 0xf6, 0x40, 0x3e, 0x80, 0x1f, 0x84, 0xb9, 0x41, 0x08, 0x78, 0xe5, 0xd1,
+	0xf2, 0xb7, 0xd1, 0x09, 0x87, 0x4d, 0x0c, 0x1b, 0x5c, 0xec, 0x3f, 0x1c, 0x87, 0x39, 0x39, 0x64,
+	0xd4, 0x0b, 0x71, 0x14, 0xb1, 0xea, 0xc2, 0x53, 0x32, 0x38, 0xed, 0xbf, 0x43, 0x95, 0xa4, 0x5d,
+	0xd4, 0xe3, 0x9f, 0x5a, 0x4f, 0xed, 0xf5, 0x60, 0x20, 0x05, 0x0f, 0xc0, 0xed, 0xbf, 0x18, 0xe1,
+	0xeb, 0x77, 0x31, 0x9a, 0xc2, 0x36, 0x71, 0xa8, 0xb0, 0x0d, 0xbc, 0x46, 0x27, 0x1f, 0xe1, 0x1a,
+	0xed, 0xbf, 0xda, 0x8a, 0x99, 0xae, 0xb6, 0xbf, 0xc8, 0xc1, 0xc4, 0xa6, 0xef, 0xc9, 0xb7, 0x9e,
+	0xe3, 0x4f, 0x88, 0xbf, 0x33, 0xe2, 0x1b, 0xb1, 0x80, 0x52, 0xb6, 0x5c, 0xbe, 0x11, 0x4f, 0xc6,
+	0xdf, 0x87, 0x8d, 0xfc, 0x6e, 0x3e, 0x4b, 0xd8, 0xa1, 0x81, 0x0f, 0xc9, 0xef, 0xfe, 0xc8, 0x82,
+	0xe9, 0xd8, 0x14, 0x9e, 0xe0, 0xb7, 0xf4, 0xc4, 0x3e, 0xa5, 0xbd, 0xa5, 0xff, 0xb3, 0x05, 0x73,
+	0xb1, 0x9e, 0x8f, 0x21, 0x3b, 0xfa, 0xed, 0x78, 0x76, 0xf4, 0xfc, 0x08, 0xeb, 0x19, 0x90, 0x23,
+	0xfd, 0x93, 0xe4, 0x6a, 0xc4, 0x4a, 0xd1, 0x6f, 0xc0, 0x5c, 0x37, 0x78, 0x7a, 0xdf, 0xf4, 0xda,
+	0x4e, 0xdd, 0xa1, 0x41, 0xb2, 0xfd, 0x42, 0xc6, 0xba, 0x04, 0x39, 0x7c, 0xaf, 0xf2, 0x0d, 0xcd,
+	0x7d, 0x6e, 0x33, 0x89, 0x8b, 0xfb, 0x59, 0xc9, 0xdc, 0xb3, 0x9e, 0xd5, 0x13, 0x9b, 0x7b, 0xd6,
+	0xf3, 0x1b, 0xb0, 0xaf, 0x3f, 0xb6, 0x60, 0xca, 0x50, 0x0f, 0x86, 0x5a, 0x00, 0x1f, 0x13, 0x9f,
+	0xb6, 0xbc, 0xd0, 0x8d, 0x1b, 0x3a, 0x23, 0xf8, 0x6e, 0x30, 0x4e, 0x22, 0x45, 0x92, 0x1f, 0xb6,
+	0x33, 0x6c, 0x60, 0xa3, 0x6f, 0x1b, 0xc9, 0x3d, 0xa5, 0x5b, 0x43, 0x71, 0x91, 0xb9, 0x03, 0xc5,
+	0xc1, 0xd4, 0x64, 0x23, 0x25, 0x68, 0xff, 0x4d, 0x2e, 0xd4, 0x64, 0x9d, 0x2f, 0xab, 0x03, 0xd4,
+	0x3d, 0xb7, 0xe1, 0xf0, 0xb0, 0x38, 0xab, 0xb4, 0xb2, 0x3c, 0xdc, 0xd1, 0x54, 0x83, 0x71, 0xd1,
+	0x82, 0xc2, 0x26, 0x86, 0x0d, 0x58, 0x74, 0x3e, 0xa8, 0xa4, 0x8c, 0x47, 0x0c, 0xaa, 0x92, 0xf2,
+	0xc1, 0xfe, 0x52, 0xb0, 0xd1, 0x66, 0x65, 0x65, 0x86, 0x8a, 0x41, 0x54, 0x83, 0x71, 0x61, 0xaa,
+	0x82, 0x72, 0xba, 0x95, 0xcc, 0xc6, 0x8f, 0xe9, 0xca, 0x11, 0xf1, 0x27, 0x56, 0x58, 0xf6, 0x5f,
+	0xe7, 0xa0, 0x18, 0xca, 0xfa, 0x63, 0xbf, 0x19, 0xce, 0x67, 0xd4, 0xd2, 0x41, 0xd6, 0x0e, 0x7d,
+	0x98, 0xb8, 0x19, 0xb2, 0xaa, 0xff, 0x21, 0x77, 0xc3, 0x3f, 0xa8, 0xbb, 0x41, 0xf5, 0x7d, 0x0c,
+	0xaa, 0x7e, 0x3b, 0xae, 0xea, 0xcb, 0x19, 0x57, 0x33, 0x40, 0xd9, 0xbf, 0x67, 0xc1, 0xc9, 0x84,
+	0xc1, 0x43, 0xcf, 0x49, 0xa1, 0x6a, 0x06, 0x42, 0x1b, 0x0e, 0xd4, 0x29, 0x3a, 0x49, 0x43, 0x9b,
+	0x30, 0x4f, 0x7a, 0xdc, 0x0b, 0xc7, 0x5e, 0x75, 0xc9, 0x56, 0x9b, 0xaa, 0xbc, 0xdb, 0x64, 0xe5,
+	0xe7, 0xc2, 0x8c, 0x77, 0x4a, 0x1f, 0x9c, 0x3a, 0xd2, 0xfe, 0x51, 0x0e, 0x50, 0xd8, 0x98, 0xe5,
+	0x05, 0xea, 0x43, 0x98, 0xd8, 0x56, 0xa9, 0xdc, 0x47, 0x7b, 0x42, 0xac, 0x94, 0xcc, 0x57, 0xd4,
+	0x00, 0x13, 0xbd, 0x77, 0x34, 0x72, 0x04, 0xfd, 0x32, 0x84, 0xee, 0x02, 0x6c, 0x3b, 0xae, 0xc3,
+	0x5a, 0x23, 0x56, 0x62, 0x48, 0x07, 0xf5, 0x5a, 0x88, 0x80, 0x0d, 0x34, 0xfb, 0x4f, 0x73, 0x86,
+	0x7c, 0xca, 0xab, 0x71, 0xa8, 0x73, 0x7d, 0x29, 0xbe, 0x99, 0xc5, 0xfe, 0xe7, 0xe5, 0x70, 0x63,
+	0xee, 0xc2, 0xd8, 0x2e, 0xf1, 0x83, 0x97, 0xae, 0x21, 0x8b, 0xbc, 0xfa, 0xeb, 0x3b, 0xa2, 0x33,
+	0xbd, 0x43, 0x7c, 0x86, 0x25, 0xa6, 0x70, 0x1b, 0x18, 0xa7, 0xdd, 0xc0, 0x30, 0x67, 0x36, 0x0a,
+	0x9c, 0x76, 0xcd, 0x05, 0xd2, 0xae, 0xb4, 0x6e, 0xb4, 0xcb, 0xec, 0xef, 0x4f, 0x18, 0x12, 0xaf,
+	0xef, 0x82, 0x9b, 0x80, 0xda, 0x84, 0xf1, 0x1b, 0xc4, 0x6d, 0x08, 0xf9, 0xa4, 0xdb, 0x3e, 0x65,
+	0x2d, 0x1d, 0x75, 0x2c, 0x6a, 0x14, 0xb4, 0xd1, 0xd7, 0x03, 0xa7, 0x8c, 0x42, 0x17, 0xe2, 0x26,
+	0x7f, 0x29, 0x69, 0xf2, 0x67, 0x22, 0x75, 0x1b, 0xd1, 0xe8, 0x1b, 0xf2, 0x3e, 0x7e, 0x0c, 0xf2,
+	0xfe, 0xeb, 0x30, 0xb7, 0x9d, 0xac, 0x37, 0xd0, 0x35, 0x5a, 0x6f, 0x8c, 0x58, 0xae, 0x50, 0x39,
+	0x7d, 0x10, 0x3d, 0x52, 0x47, 0xcd, 0xb8, 0x9f, 0x11, 0xf2, 0x82, 0xe2, 0x74, 0x99, 0x40, 0x52,
+	0xb9, 0xc1, 0xa1, 0x75, 0x2e, 0x91, 0x7a, 0x4a, 0x96, 0xa5, 0x2b, 0x48, 0x1c, 0x63, 0x90, 0xd0,
+	0xc1, 0xc2, 0x51, 0xea, 0x20, 0xba, 0x10, 0xbe, 0xc9, 0x89, 0xe9, 0xc8, 0xf0, 0x2c, 0xdf, 0xf7,
+	0x9a, 0x26, 0x48, 0xd8, 0xec, 0x87, 0x3e, 0xb1, 0xe0, 0xb4, 0x10, 0xd6, 0xab, 0xf7, 0x69, 0xbd,
+	0x27, 0x76, 0x25, 0xf8, 0x66, 0x65, 0xa1, 0x24, 0x77, 0xe3, 0xf2, 0xb0, 0x6e, 0x51, 0x0a, 0x44,
+	0x14, 0x6b, 0xa6, 0x92, 0x71, 0x3a, 0x63, 0x74, 0x4f, 0x39, 0x1a, 0x54, 0x86, 0xf2, 0x8f, 0x9e,
+	0xa1, 0x0b, 0x9d, 0x0e, 0x65, 0x77, 0x38, 0xb5, 0x7f, 0x9a, 0x37, 0xcd, 0xd5, 0x70, 0x79, 0xc3,
+	0xbb, 0x30, 0xc6, 0x09, 0xdb, 0xd1, 0x5a, 0xf0, 0xd6, 0x08, 0x65, 0xc7, 0x91, 0x2e, 0xc8, 0xb8,
+	0x52, 0x36, 0x49, 0x4c, 0xb4, 0x08, 0x39, 0xc2, 0x92, 0xaf, 0x48, 0xab, 0x0c, 0xe7, 0x08, 0x93,
+	0x2f, 0x4c, 0xdb, 0x3a, 0xea, 0x8f, 0x5e, 0x98, 0xb6, 0x71, 0xce, 0xd9, 0x46, 0xef, 0xc1, 0xb8,
+	0x4f, 0xb9, 0xbf, 0xa7, 0xad, 0xf9, 0xc5, 0x11, 0x0c, 0x17, 0x16, 0xe3, 0xd5, 0x16, 0xc9, 0x3f,
+	0xb1, 0x42, 0x0c, 0xed, 0x6d, 0xe1, 0x18, 0xec, 0x6d, 0x94, 0x82, 0xcd, 0x1f, 0x5b, 0x0a, 0xf6,
+	0x87, 0x96, 0x71, 0xc1, 0x87, 0x0b, 0x45, 0xef, 0xc0, 0x04, 0x77, 0x3a, 0xd4, 0xeb, 0xf1, 0x6c,
+	0x5e, 0x53, 0xf8, 0x48, 0x2f, 0xcd, 0xd8, 0x6d, 0x05, 0x81, 0x03, 0x2c, 0x74, 0x05, 0x66, 0xa8,
+	0xef, 0x7b, 0xfe, 0xed, 0x96, 0x30, 0xcb, 0x5e, 0x5b, 0xb9, 0x26, 0xd3, 0x51, 0x76, 0xe4, 0x6a,
+	0x8c, 0x8a, 0x13, 0xbd, 0x83, 0xd8, 0xff, 0xeb, 0x52, 0x47, 0xaf, 0x63, 0xff, 0xc7, 0x5a, 0x40,
+	0x3f, 0x72, 0xec, 0x7f, 0x68, 0xe5, 0xfc, 0x07, 0xf0, 0x54, 0xba, 0x1e, 0x1f, 0xc9, 0x97, 0x67,
+	0xff, 0x98, 0xdc, 0x2b, 0xe9, 0x3e, 0x05, 0xea, 0x67, 0x1d, 0xa7, 0xbb, 0x93, 0x3b, 0x6a, 0x77,
+	0xc7, 0x37, 0x97, 0xa2, 0xbf, 0xd3, 0x43, 0x1f, 0x6a, 0x39, 0xb3, 0xb2, 0x7c, 0xf9, 0xd5, 0x07,
+	0x33, 0x50, 0xd6, 0xfe, 0xc5, 0x82, 0xd3, 0xa9, 0xbd, 0xc3, 0x3d, 0xcc, 0x1d, 0xe7, 0x1e, 0x5a,
+	0x47, 0xbd, 0x87, 0x3f, 0xc8, 0xc1, 0x2c, 0xa6, 0x5d, 0x2f, 0x96, 0xb5, 0xdf, 0x0c, 0xbe, 0x70,
+	0xc8, 0x10, 0x13, 0x24, 0x9e, 0xc2, 0x2b, 0x13, 0xb1, 0x4f, 0x1b, 0x84, 0xba, 0x74, 0x02, 0x07,
+	0x70, 0x68, 0xf5, 0xef, 0x7b, 0x4f, 0x50, 0x37, 0x87, 0x7a, 0x99, 0x50, 0x80, 0x02, 0x59, 0x96,
+	0x92, 0x69, 0xe3, 0xfe, 0x46, 0x86, 0xa2, 0xb4, 0x7e, 0x64, 0xd9, 0x8c, 0x15, 0xa0, 0xfd, 0x69,
+	0x0e, 0x54, 0xfc, 0xf0, 0x18, 0xac, 0xe3, 0x2f, 0xc7, 0xac, 0xe3, 0x72, 0x96, 0xdc, 0xd0, 0xa0,
+	0x1c, 0x41, 0x32, 0xb6, 0x3b, 0x97, 0x31, 0xe1, 0xf4, 0x90, 0xfc, 0xc0, 0xdf, 0x5b, 0x50, 0x94,
+	0xfd, 0x1e, 0x83, 0xa1, 0xdd, 0x8c, 0x1b, 0xda, 0x97, 0x33, 0xac, 0x62, 0x80, 0x81, 0xfd, 0xef,
+	0xbc, 0x9e, 0x7d, 0x18, 0x39, 0xb6, 0x88, 0xdf, 0xd0, 0x21, 0x51, 0xa4, 0x25, 0xa2, 0x11, 0x2b,
+	0x5a, 0xa8, 0xdb, 0x13, 0xc7, 0xa0, 0xdb, 0xbf, 0xaa, 0x2a, 0xfa, 0x28, 0xe3, 0xb4, 0x71, 0x2d,
+	0x8c, 0x7d, 0xf2, 0x99, 0x4b, 0x13, 0x75, 0xf9, 0x64, 0xf4, 0x94, 0x84, 0x13, 0xa8, 0xb8, 0x8f,
+	0x8f, 0x88, 0x87, 0xba, 0x49, 0x63, 0xa6, 0xe3, 0x84, 0x37, 0x46, 0xb4, 0x9c, 0x2a, 0x1e, 0xea,
+	0x6b, 0xc6, 0xfd, 0x8c, 0x50, 0x0b, 0xa6, 0xcc, 0xa2, 0x6a, 0x2d, 0xa7, 0x2b, 0xd9, 0xab, 0xb7,
+	0x55, 0xa5, 0x84, 0xd9, 0x82, 0x63, 0xc8, 0xf6, 0x1f, 0x59, 0x00, 0x51, 0x26, 0x55, 0x9c, 0x79,
+	0xdd, 0xeb, 0xb9, 0x2a, 0x0d, 0x90, 0x8f, 0xce, 0xbc, 0x2a, 0x1a, 0xb1, 0xa2, 0x09, 0xfd, 0x51,
+	0xc1, 0x94, 0x16, 0xea, 0x73, 0x59, 0xe2, 0xb4, 0x44, 0xc6, 0x56, 0x35, 0x62, 0x0d, 0x68, 0xef,
+	0x17, 0xa0, 0x64, 0xe8, 0x59, 0x22, 0x5f, 0x3b, 0x7d, 0x3c, 0xf9, 0xda, 0xf4, 0x44, 0x40, 0x69,
+	0xa4, 0x44, 0xc0, 0xb9, 0x78, 0x22, 0xe0, 0x99, 0x64, 0x22, 0x40, 0x6d, 0x76, 0x2c, 0x09, 0xc0,
+	0x60, 0x46, 0x47, 0xc4, 0x41, 0xb1, 0xbe, 0xca, 0xad, 0x8c, 0x1c, 0x77, 0x23, 0xe1, 0xe8, 0x5e,
+	0x8b, 0x41, 0xe2, 0x04, 0x0b, 0xe1, 0x28, 0xeb, 0x96, 0x5a, 0xaf, 0xd3, 0x21, 0xfe, 0xde, 0xc2,
+	0x94, 0x9c, 0x70, 0xe8, 0x28, 0x5f, 0x8b, 0x51, 0x71, 0xa2, 0x37, 0xda, 0x0c, 0x65, 0x40, 0x15,
+	0x80, 0xbf, 0x92, 0x45, 0x06, 0x54, 0xa0, 0x10, 0x3f, 0x7a, 0x33, 0x17, 0x52, 0x3c, 0x24, 0x17,
+	0x72, 0x13, 0x90, 0xb7, 0x25, 0x43, 0x92, 0xc6, 0x75, 0xf5, 0xab, 0x18, 0x42, 0x49, 0x0a, 0x52,
+	0x64, 0xc3, 0x03, 0xbb, 0xd5, 0xd7, 0x03, 0xa7, 0x8c, 0x12, 0x46, 0x46, 0x47, 0xe1, 0xa1, 0x66,
+	0xea, 0xbc, 0x47, 0xd6, 0x28, 0x2e, 0x0a, 0x2b, 0x65, 0x01, 0x71, 0x35, 0x81, 0x8a, 0xfb, 0xf8,
+	0xa0, 0x8f, 0x60, 0x5a, 0x88, 0x50, 0xc4, 0x18, 0x1e, 0x91, 0xf1, 0xdc, 0xc1, 0xfe, 0xd2, 0xf4,
+	0x86, 0x09, 0x89, 0xe3, 0x1c, 0xec, 0x3f, 0xc8, 0x43, 0x7a, 0x0e, 0x20, 0xfa, 0x76, 0xc9, 0x7a,
+	0xc8, 0xb7, 0x4b, 0xef, 0x42, 0x91, 0x71, 0xe2, 0xab, 0x8f, 0xc8, 0x72, 0xa3, 0x7d, 0x44, 0x56,
+	0x0b, 0x00, 0x70, 0x84, 0x95, 0x48, 0xc8, 0xe4, 0x8f, 0x34, 0x21, 0xb3, 0x02, 0x20, 0xc3, 0x3c,
+	0x69, 0xc4, 0xe4, 0x6d, 0x36, 0x1d, 0xd9, 0x84, 0xab, 0x21, 0x05, 0x1b, 0xbd, 0xd0, 0xc5, 0xd0,
+	0x47, 0x50, 0x75, 0x04, 0x67, 0xfb, 0xca, 0xbe, 0x92, 0x29, 0xbd, 0x94, 0x9f, 0x7e, 0x38, 0xa4,
+	0x4c, 0xd4, 0xfe, 0xdf, 0x1c, 0xc4, 0x6c, 0x33, 0xfa, 0x9e, 0x05, 0x73, 0x24, 0xf1, 0xfb, 0x1a,
+	0x81, 0x6b, 0xfb, 0x4b, 0xd9, 0x7e, 0xf4, 0xa4, 0xef, 0xe7, 0x39, 0xa2, 0x27, 0xcd, 0x64, 0x17,
+	0x86, 0xfb, 0x99, 0xa2, 0xdf, 0xb5, 0xe0, 0x14, 0xe9, 0xff, 0x01, 0x15, 0x7d, 0xe8, 0x6f, 0x8e,
+	0xfc, 0x0b, 0x2c, 0x95, 0xa7, 0x0f, 0xf6, 0x97, 0xd2, 0x7e, 0x5a, 0x06, 0xa7, 0xb1, 0x43, 0xef,
+	0xc3, 0x18, 0xf1, 0x9b, 0x41, 0x46, 0x38, 0x3b, 0xdb, 0xe0, 0x77, 0x71, 0x22, 0x07, 0x63, 0xd5,
+	0x6f, 0x32, 0x2c, 0x41, 0xed, 0x9f, 0xe4, 0x61, 0x36, 0xf9, 0xad, 0x93, 0xae, 0x24, 0x1e, 0x4b,
+	0xad, 0x24, 0x16, 0x3a, 0x52, 0xe7, 0x61, 0x59, 0x6f, 0xa4, 0x23, 0xa2, 0x11, 0x2b, 0x5a, 0xa8,
+	0x23, 0xf2, 0x0b, 0x84, 0xf1, 0x47, 0xd0, 0x11, 0xf9, 0xd9, 0x41, 0x84, 0x85, 0x2e, 0xc6, 0xef,
+	0x16, 0x3b, 0x79, 0xb7, 0xcc, 0x99, 0x6b, 0x19, 0x35, 0xcf, 0xdc, 0x81, 0x92, 0x71, 0x0e, 0x5a,
+	0x13, 0x2f, 0x65, 0xde, 0xf7, 0x48, 0xec, 0x4e, 0xaa, 0x12, 0xb2, 0x88, 0x62, 0xe2, 0x47, 0x7a,
+	0x2f, 0x77, 0xeb, 0x91, 0x12, 0xb1, 0x72, 0xbb, 0x0c, 0x34, 0xfb, 0xdf, 0x2d, 0x98, 0x8e, 0x95,
+	0xb7, 0x0b, 0x6e, 0xc1, 0x67, 0x04, 0xa3, 0xff, 0x98, 0xcc, 0x9d, 0x10, 0x01, 0x1b, 0x68, 0xe8,
+	0x3b, 0x50, 0x6a, 0x7b, 0x6e, 0x93, 0x32, 0x5e, 0xf3, 0xc8, 0x8e, 0xd6, 0x93, 0xac, 0x59, 0xad,
+	0x85, 0x83, 0xfd, 0xa5, 0xf9, 0x0d, 0x05, 0x53, 0xf5, 0x3a, 0xdd, 0x36, 0xe5, 0xea, 0x83, 0x13,
+	0x6c, 0x82, 0xcb, 0xc7, 0xda, 0xf0, 0x35, 0xfd, 0x49, 0x7d, 0xac, 0x8d, 0xca, 0x00, 0x8e, 0xf8,
+	0xb1, 0x36, 0x56, 0x5f, 0x70, 0xc8, 0x63, 0x6d, 0xd8, 0xf7, 0x89, 0x7d, 0xac, 0x0d, 0x67, 0x38,
+	0x20, 0x28, 0xfb, 0x9f, 0x9c, 0xb1, 0x8a, 0x78, 0x60, 0x96, 0x7b, 0x48, 0x60, 0xf6, 0x01, 0x4c,
+	0x3a, 0x2e, 0xa7, 0xfe, 0x2e, 0x69, 0xeb, 0xac, 0x74, 0x56, 0x59, 0x0c, 0x97, 0xba, 0xae, 0x71,
+	0x70, 0x88, 0x88, 0xda, 0x70, 0x3a, 0x78, 0xc5, 0xf1, 0x29, 0x89, 0x9e, 0x91, 0x75, 0x75, 0xe1,
+	0xeb, 0xc1, 0x73, 0xc3, 0xb5, 0xb4, 0x4e, 0x0f, 0x06, 0x11, 0x70, 0x3a, 0x28, 0x62, 0x30, 0xcd,
+	0x8c, 0x8c, 0x44, 0x70, 0x23, 0x0e, 0xf9, 0x02, 0x96, 0x4c, 0xe2, 0x18, 0x25, 0x89, 0x26, 0x28,
+	0x8e, 0xf3, 0xb0, 0x3f, 0xb1, 0x60, 0x26, 0x5e, 0xc9, 0xf2, 0xff, 0x1e, 0x1d, 0xfd, 0x38, 0x0f,
+	0x27, 0x13, 0xc2, 0x9f, 0x88, 0x90, 0x8a, 0x8f, 0x33, 0x42, 0x2a, 0x8c, 0x14, 0x21, 0xa5, 0x3b,
+	0xef, 0x63, 0x23, 0x39, 0xef, 0x97, 0x95, 0x03, 0xad, 0x85, 0x69, 0x7d, 0x4d, 0x7f, 0x43, 0x13,
+	0x1e, 0xf0, 0x86, 0x49, 0xc4, 0xf1, 0xbe, 0xd2, 0xc3, 0x69, 0xf4, 0xff, 0x56, 0x8a, 0xf6, 0xfe,
+	0xdf, 0xcc, 0x5a, 0x15, 0x1c, 0x02, 0x28, 0x0f, 0x27, 0x85, 0x80, 0xd3, 0xd8, 0x55, 0x6e, 0x7e,
+	0xfe, 0xd5, 0x99, 0x13, 0x5f, 0x7c, 0x75, 0xe6, 0xc4, 0x97, 0x5f, 0x9d, 0x39, 0xf1, 0xdd, 0x83,
+	0x33, 0xd6, 0xe7, 0x07, 0x67, 0xac, 0x2f, 0x0e, 0xce, 0x58, 0x5f, 0x1e, 0x9c, 0xb1, 0x7e, 0x7a,
+	0x70, 0xc6, 0xfa, 0xe4, 0x67, 0x67, 0x4e, 0xdc, 0x7d, 0x7e, 0x98, 0x5f, 0x58, 0xfc, 0xbf, 0x00,
+	0x00, 0x00, 0xff, 0xff, 0x1d, 0x3c, 0x3d, 0x3f, 0x88, 0x51, 0x00, 0x00,
 }
 
 func (m *AnalysisRunArgument) Marshal() (dAtA []byte, err error) {
@@ -3927,6 +4051,32 @@ func (m *HealthCheckStep) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *HealthStats) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *HealthStats) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *HealthStats) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	i = encodeVarintGenerated(dAtA, i, uint64(m.Healthy))
+	i--
+	dAtA[i] = 0x8
+	return len(dAtA) - i, nil
+}
+
 func (m *Image) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
@@ -4351,6 +4501,49 @@ func (m *ProjectList) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *ProjectStats) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *ProjectStats) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *ProjectStats) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	{
+		size, err := m.Stages.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintGenerated(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0x12
+	{
+		size, err := m.Warehouses.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintGenerated(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
 func (m *ProjectStatus) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
@@ -4371,6 +4564,18 @@ func (m *ProjectStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if m.Stats != nil {
+		{
+			size, err := m.Stats.MarshalToSizedBuffer(dAtA[:i])
+			if err != nil {
+				return 0, err
+			}
+			i -= size
+			i = encodeVarintGenerated(dAtA, i, uint64(size))
+		}
+		i--
+		dAtA[i] = 0x22
+	}
 	if len(m.Conditions) > 0 {
 		for iNdEx := len(m.Conditions) - 1; iNdEx >= 0; iNdEx-- {
 			{
@@ -5399,6 +5604,42 @@ func (m *StageSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *StageStats) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *StageStats) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *StageStats) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	i = encodeVarintGenerated(dAtA, i, uint64(m.Count))
+	i--
+	dAtA[i] = 0x10
+	{
+		size, err := m.Health.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintGenerated(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
 func (m *StageStatus) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
@@ -5920,7 +6161,7 @@ func (m *WarehouseSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
-func (m *WarehouseStatus) Marshal() (dAtA []byte, err error) {
+func (m *WarehouseStats) Marshal() (dAtA []byte, err error) {
 	size := m.Size()
 	dAtA = make([]byte, size)
 	n, err := m.MarshalToSizedBuffer(dAtA[:size])
@@ -5930,32 +6171,68 @@ func (m *WarehouseStatus) Marshal() (dAtA []byte, err error) {
 	return dAtA[:n], nil
 }
 
-func (m *WarehouseStatus) MarshalTo(dAtA []byte) (int, error) {
+func (m *WarehouseStats) MarshalTo(dAtA []byte) (int, error) {
 	size := m.Size()
 	return m.MarshalToSizedBuffer(dAtA[:size])
 }
 
-func (m *WarehouseStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+func (m *WarehouseStats) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	i := len(dAtA)
 	_ = i
 	var l int
 	_ = l
-	if len(m.Conditions) > 0 {
-		for iNdEx := len(m.Conditions) - 1; iNdEx >= 0; iNdEx-- {
-			{
-				size, err := m.Conditions[iNdEx].MarshalToSizedBuffer(dAtA[:i])
-				if err != nil {
-					return 0, err
-				}
-				i -= size
-				i = encodeVarintGenerated(dAtA, i, uint64(size))
-			}
-			i--
-			dAtA[i] = 0x4a
-		}
-	}
-	i -= len(m.LastFreightID)
-	copy(dAtA[i:], m.LastFreightID)
+	i = encodeVarintGenerated(dAtA, i, uint64(m.Count))
+	i--
+	dAtA[i] = 0x10
+	{
+		size, err := m.Health.MarshalToSizedBuffer(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = encodeVarintGenerated(dAtA, i, uint64(size))
+	}
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
+func (m *WarehouseStatus) Marshal() (dAtA []byte, err error) {
+	size := m.Size()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *WarehouseStatus) MarshalTo(dAtA []byte) (int, error) {
+	size := m.Size()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *WarehouseStatus) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.Conditions) > 0 {
+		for iNdEx := len(m.Conditions) - 1; iNdEx >= 0; iNdEx-- {
+			{
+				size, err := m.Conditions[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+				if err != nil {
+					return 0, err
+				}
+				i -= size
+				i = encodeVarintGenerated(dAtA, i, uint64(size))
+			}
+			i--
+			dAtA[i] = 0x4a
+		}
+	}
+	i -= len(m.LastFreightID)
+	copy(dAtA[i:], m.LastFreightID)
 	i = encodeVarintGenerated(dAtA, i, uint64(len(m.LastFreightID)))
 	i--
 	dAtA[i] = 0x42
@@ -6623,6 +6900,16 @@ func (m *HealthCheckStep) Size() (n int) {
 	return n
 }
 
+func (m *HealthStats) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	n += 1 + sovGenerated(uint64(m.Healthy))
+	return n
+}
+
 func (m *Image) Size() (n int) {
 	if m == nil {
 		return 0
@@ -6776,6 +7063,19 @@ func (m *ProjectList) Size() (n int) {
 	return n
 }
 
+func (m *ProjectStats) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = m.Warehouses.Size()
+	n += 1 + l + sovGenerated(uint64(l))
+	l = m.Stages.Size()
+	n += 1 + l + sovGenerated(uint64(l))
+	return n
+}
+
 func (m *ProjectStatus) Size() (n int) {
 	if m == nil {
 		return 0
@@ -6792,6 +7092,10 @@ func (m *ProjectStatus) Size() (n int) {
 			n += 1 + l + sovGenerated(uint64(l))
 		}
 	}
+	if m.Stats != nil {
+		l = m.Stats.Size()
+		n += 1 + l + sovGenerated(uint64(l))
+	}
 	return n
 }
 
@@ -7158,6 +7462,18 @@ func (m *StageSpec) Size() (n int) {
 	return n
 }
 
+func (m *StageStats) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = m.Health.Size()
+	n += 1 + l + sovGenerated(uint64(l))
+	n += 1 + sovGenerated(uint64(m.Count))
+	return n
+}
+
 func (m *StageStatus) Size() (n int) {
 	if m == nil {
 		return 0
@@ -7348,6 +7664,18 @@ func (m *WarehouseSpec) Size() (n int) {
 	return n
 }
 
+func (m *WarehouseStats) Size() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = m.Health.Size()
+	n += 1 + l + sovGenerated(uint64(l))
+	n += 1 + sovGenerated(uint64(m.Count))
+	return n
+}
+
 func (m *WarehouseStatus) Size() (n int) {
 	if m == nil {
 		return 0
@@ -7888,6 +8216,16 @@ func (this *HealthCheckStep) String() string {
 	}, "")
 	return s
 }
+func (this *HealthStats) String() string {
+	if this == nil {
+		return "nil"
+	}
+	s := strings.Join([]string{`&HealthStats{`,
+		`Healthy:` + fmt.Sprintf("%v", this.Healthy) + `,`,
+		`}`,
+	}, "")
+	return s
+}
 func (this *Image) String() string {
 	if this == nil {
 		return "nil"
@@ -8018,6 +8356,17 @@ func (this *ProjectList) String() string {
 	}, "")
 	return s
 }
+func (this *ProjectStats) String() string {
+	if this == nil {
+		return "nil"
+	}
+	s := strings.Join([]string{`&ProjectStats{`,
+		`Warehouses:` + strings.Replace(strings.Replace(this.Warehouses.String(), "WarehouseStats", "WarehouseStats", 1), `&`, ``, 1) + `,`,
+		`Stages:` + strings.Replace(strings.Replace(this.Stages.String(), "StageStats", "StageStats", 1), `&`, ``, 1) + `,`,
+		`}`,
+	}, "")
+	return s
+}
 func (this *ProjectStatus) String() string {
 	if this == nil {
 		return "nil"
@@ -8031,6 +8380,7 @@ func (this *ProjectStatus) String() string {
 		`Phase:` + fmt.Sprintf("%v", this.Phase) + `,`,
 		`Message:` + fmt.Sprintf("%v", this.Message) + `,`,
 		`Conditions:` + repeatedStringForConditions + `,`,
+		`Stats:` + strings.Replace(this.Stats.String(), "ProjectStats", "ProjectStats", 1) + `,`,
 		`}`,
 	}, "")
 	return s
@@ -8325,6 +8675,17 @@ func (this *StageSpec) String() string {
 	}, "")
 	return s
 }
+func (this *StageStats) String() string {
+	if this == nil {
+		return "nil"
+	}
+	s := strings.Join([]string{`&StageStats{`,
+		`Health:` + strings.Replace(strings.Replace(this.Health.String(), "HealthStats", "HealthStats", 1), `&`, ``, 1) + `,`,
+		`Count:` + fmt.Sprintf("%v", this.Count) + `,`,
+		`}`,
+	}, "")
+	return s
+}
 func (this *StageStatus) String() string {
 	if this == nil {
 		return "nil"
@@ -8464,6 +8825,17 @@ func (this *WarehouseSpec) String() string {
 	}, "")
 	return s
 }
+func (this *WarehouseStats) String() string {
+	if this == nil {
+		return "nil"
+	}
+	s := strings.Join([]string{`&WarehouseStats{`,
+		`Health:` + strings.Replace(strings.Replace(this.Health.String(), "HealthStats", "HealthStats", 1), `&`, ``, 1) + `,`,
+		`Count:` + fmt.Sprintf("%v", this.Count) + `,`,
+		`}`,
+	}, "")
+	return s
+}
 func (this *WarehouseStatus) String() string {
 	if this == nil {
 		return "nil"
@@ -14147,6 +14519,75 @@ func (m *HealthCheckStep) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *HealthStats) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowGenerated
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: HealthStats: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: HealthStats: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Healthy", wireType)
+			}
+			m.Healthy = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Healthy |= int64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		default:
+			iNdEx = preIndex
+			skippy, err := skipGenerated(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func (m *Image) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
@@ -15519,7 +15960,7 @@ func (m *ProjectList) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *ProjectStatus) Unmarshal(dAtA []byte) error {
+func (m *ProjectStats) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -15542,17 +15983,17 @@ func (m *ProjectStatus) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: ProjectStatus: wiretype end group for non-group")
+			return fmt.Errorf("proto: ProjectStats: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: ProjectStatus: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: ProjectStats: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
 		case 1:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Phase", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field Warehouses", wireType)
 			}
-			var stringLen uint64
+			var msglen int
 			for shift := uint(0); ; shift += 7 {
 				if shift >= 64 {
 					return ErrIntOverflowGenerated
@@ -15562,59 +16003,28 @@ func (m *ProjectStatus) Unmarshal(dAtA []byte) error {
 				}
 				b := dAtA[iNdEx]
 				iNdEx++
-				stringLen |= uint64(b&0x7F) << shift
+				msglen |= int(b&0x7F) << shift
 				if b < 0x80 {
 					break
 				}
 			}
-			intStringLen := int(stringLen)
-			if intStringLen < 0 {
+			if msglen < 0 {
 				return ErrInvalidLengthGenerated
 			}
-			postIndex := iNdEx + intStringLen
+			postIndex := iNdEx + msglen
 			if postIndex < 0 {
 				return ErrInvalidLengthGenerated
 			}
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Phase = ProjectPhase(dAtA[iNdEx:postIndex])
-			iNdEx = postIndex
-		case 2:
-			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Message", wireType)
-			}
-			var stringLen uint64
-			for shift := uint(0); ; shift += 7 {
-				if shift >= 64 {
-					return ErrIntOverflowGenerated
-				}
-				if iNdEx >= l {
-					return io.ErrUnexpectedEOF
-				}
-				b := dAtA[iNdEx]
-				iNdEx++
-				stringLen |= uint64(b&0x7F) << shift
-				if b < 0x80 {
-					break
-				}
-			}
-			intStringLen := int(stringLen)
-			if intStringLen < 0 {
-				return ErrInvalidLengthGenerated
-			}
-			postIndex := iNdEx + intStringLen
-			if postIndex < 0 {
-				return ErrInvalidLengthGenerated
-			}
-			if postIndex > l {
-				return io.ErrUnexpectedEOF
+			if err := m.Warehouses.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
 			}
-			m.Message = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
-		case 3:
+		case 2:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Conditions", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field Stages", wireType)
 			}
 			var msglen int
 			for shift := uint(0); ; shift += 7 {
@@ -15641,8 +16051,7 @@ func (m *ProjectStatus) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.Conditions = append(m.Conditions, v1.Condition{})
-			if err := m.Conditions[len(m.Conditions)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+			if err := m.Stages.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
 			iNdEx = postIndex
@@ -15667,7 +16076,7 @@ func (m *ProjectStatus) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
-func (m *Promotion) Unmarshal(dAtA []byte) error {
+func (m *ProjectStatus) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
 	for iNdEx < l {
@@ -15690,17 +16099,17 @@ func (m *Promotion) Unmarshal(dAtA []byte) error {
 		fieldNum := int32(wire >> 3)
 		wireType := int(wire & 0x7)
 		if wireType == 4 {
-			return fmt.Errorf("proto: Promotion: wiretype end group for non-group")
+			return fmt.Errorf("proto: ProjectStatus: wiretype end group for non-group")
 		}
 		if fieldNum <= 0 {
-			return fmt.Errorf("proto: Promotion: illegal tag %d (wire type %d)", fieldNum, wire)
+			return fmt.Errorf("proto: ProjectStatus: illegal tag %d (wire type %d)", fieldNum, wire)
 		}
 		switch fieldNum {
 		case 1:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field ObjectMeta", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field Phase", wireType)
 			}
-			var msglen int
+			var stringLen uint64
 			for shift := uint(0); ; shift += 7 {
 				if shift >= 64 {
 					return ErrIntOverflowGenerated
@@ -15710,30 +16119,214 @@ func (m *Promotion) Unmarshal(dAtA []byte) error {
 				}
 				b := dAtA[iNdEx]
 				iNdEx++
-				msglen |= int(b&0x7F) << shift
+				stringLen |= uint64(b&0x7F) << shift
 				if b < 0x80 {
 					break
 				}
 			}
-			if msglen < 0 {
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
 				return ErrInvalidLengthGenerated
 			}
-			postIndex := iNdEx + msglen
+			postIndex := iNdEx + intStringLen
 			if postIndex < 0 {
 				return ErrInvalidLengthGenerated
 			}
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			if err := m.ObjectMeta.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
-				return err
-			}
+			m.Phase = ProjectPhase(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
 		case 2:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field Spec", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field Message", wireType)
 			}
-			var msglen int
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Message = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Conditions", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Conditions = append(m.Conditions, v1.Condition{})
+			if err := m.Conditions[len(m.Conditions)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 4:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Stats", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.Stats == nil {
+				m.Stats = &ProjectStats{}
+			}
+			if err := m.Stats.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipGenerated(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *Promotion) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowGenerated
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: Promotion: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: Promotion: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field ObjectMeta", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := m.ObjectMeta.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Spec", wireType)
+			}
+			var msglen int
 			for shift := uint(0); ; shift += 7 {
 				if shift >= 64 {
 					return ErrIntOverflowGenerated
@@ -18489,6 +19082,108 @@ func (m *StageSpec) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *StageStats) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowGenerated
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: StageStats: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: StageStats: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Health", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := m.Health.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Count", wireType)
+			}
+			m.Count = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Count |= int64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		default:
+			iNdEx = preIndex
+			skippy, err := skipGenerated(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func (m *StageStatus) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
@@ -20108,6 +20803,108 @@ func (m *WarehouseSpec) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *WarehouseStats) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowGenerated
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: WarehouseStats: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: WarehouseStats: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Health", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := m.Health.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Count", wireType)
+			}
+			m.Count = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowGenerated
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Count |= int64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		default:
+			iNdEx = preIndex
+			skippy, err := skipGenerated(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return ErrInvalidLengthGenerated
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func (m *WarehouseStatus) Unmarshal(dAtA []byte) error {
 	l := len(dAtA)
 	iNdEx := 0
diff --git a/api/v1alpha1/generated.proto b/api/v1alpha1/generated.proto
index e8a8ecdb..600882f4 100644
--- a/api/v1alpha1/generated.proto
+++ b/api/v1alpha1/generated.proto
@@ -672,6 +672,13 @@ message HealthCheckStep {
   optional .k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1.JSON config = 2;
 }
 
+// HealthStats contains a summary of the collective health of a some resource
+// type.
+message HealthStats {
+  // Healthy contains the number of resources that are explicitly healthy.
+  optional int64 healthy = 1;
+}
+
 // Image describes a specific version of a container image.
 message Image {
   // RepoURL describes the repository in which the image can be found.
@@ -866,6 +873,17 @@ message ProjectList {
   repeated Project items = 2;
 }
 
+// ProjectStats contains a summary of the of the collective state of a
+// Project's constituent resources.
+message ProjectStats {
+  // Warehouses contains a summary of the collective state of the Project's
+  // Warehouses.
+  optional WarehouseStats warehouses = 1;
+
+  // Stages contains a summary of the collective state of the Project's Stages.
+  optional StageStats stages = 2;
+}
+
 // ProjectStatus describes a Project's current status.
 message ProjectStatus {
   // Conditions contains the last observations of the Project's current
@@ -887,6 +905,10 @@ message ProjectStatus {
   //
   // Deprecated: Use the Conditions field instead.
   optional string message = 2;
+
+  // Stats contains a summary of the of the collective state of a Project's
+  // constituent resources.
+  optional ProjectStats stats = 4;
 }
 
 // Promotion represents a request to transition a particular Stage into a
@@ -1248,6 +1270,16 @@ message StageSpec {
   optional Verification verification = 3;
 }
 
+// StageStats contains a summary of the collective state of the a Project's
+// Stages.
+message StageStats {
+  // Count contains the total number of Stages in the Project.
+  optional int64 count = 2;
+
+  // Health contains a summary of the collective health of a Project's Stages.
+  optional HealthStats health = 1;
+}
+
 // StageStatus describes a Stages's current and recent Freight, health, and
 // more.
 message StageStatus {
@@ -1447,6 +1479,17 @@ message WarehouseSpec {
   repeated RepoSubscription subscriptions = 1;
 }
 
+// WarehouseStats contains a summary of the collective state of the a Project's
+// Warehouses.
+message WarehouseStats {
+  // Count contains the total number of Warehouses in the Project.
+  optional int64 count = 2;
+
+  // Health contains a summary of the collective health of a Project's
+  // Warehouses.
+  optional HealthStats health = 1;
+}
+
 // WarehouseStatus describes a Warehouse's most recently observed state.
 message WarehouseStatus {
   // Conditions contains the last observations of the Warehouse's current
diff --git a/api/v1alpha1/project_types.go b/api/v1alpha1/project_types.go
index 83469b32..2de3c5b1 100644
--- a/api/v1alpha1/project_types.go
+++ b/api/v1alpha1/project_types.go
@@ -78,16 +78,57 @@ type ProjectStatus struct {
 	//
 	// Deprecated: Use the Conditions field instead.
 	Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"`
+	// Stats contains a summary of the of the collective state of a Project's
+	// constituent resources.
+	Stats *ProjectStats `json:"stats,omitempty" protobuf:"bytes,4,opt,name=stats"`
 }
 
+// GetConditions implements the conditions.Getter interface.
 func (w *ProjectStatus) GetConditions() []metav1.Condition {
 	return w.Conditions
 }
 
+// SetConditions implements the conditions.Setter interface.
 func (w *ProjectStatus) SetConditions(conditions []metav1.Condition) {
 	w.Conditions = conditions
 }
 
+// ProjectStats contains a summary of the of the collective state of a
+// Project's constituent resources.
+type ProjectStats struct {
+	// Warehouses contains a summary of the collective state of the Project's
+	// Warehouses.
+	Warehouses WarehouseStats `json:"warehouses,omitempty" protobuf:"bytes,1,opt,name=warehouses"`
+	// Stages contains a summary of the collective state of the Project's Stages.
+	Stages StageStats `json:"stages,omitempty" protobuf:"bytes,2,opt,name=stages"`
+}
+
+// WarehouseStats contains a summary of the collective state of the a Project's
+// Warehouses.
+type WarehouseStats struct {
+	// Count contains the total number of Warehouses in the Project.
+	Count int64 `json:"count,omitempty" protobuf:"varint,2,opt,name=count"`
+	// Health contains a summary of the collective health of a Project's
+	// Warehouses.
+	Health HealthStats `json:"health,omitempty" protobuf:"bytes,1,opt,name=health"`
+}
+
+// StageStats contains a summary of the collective state of the a Project's
+// Stages.
+type StageStats struct {
+	// Count contains the total number of Stages in the Project.
+	Count int64 `json:"count,omitempty" protobuf:"varint,2,opt,name=count"`
+	// Health contains a summary of the collective health of a Project's Stages.
+	Health HealthStats `json:"health,omitempty" protobuf:"bytes,1,opt,name=health"`
+}
+
+// HealthStats contains a summary of the collective health of a some resource
+// type.
+type HealthStats struct {
+	// Healthy contains the number of resources that are explicitly healthy.
+	Healthy int64 `json:"healthy,omitempty" protobuf:"varint,1,opt,name=healthy"`
+}
+
 // +kubebuilder:object:root=true
 
 // ProjectList is a list of Project resources.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index f2863950..4c0e9b08 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -752,6 +752,21 @@ func (in *HealthCheckStep) DeepCopy() *HealthCheckStep {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HealthStats) DeepCopyInto(out *HealthStats) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthStats.
+func (in *HealthStats) DeepCopy() *HealthStats {
+	if in == nil {
+		return nil
+	}
+	out := new(HealthStats)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Image) DeepCopyInto(out *Image) {
 	*out = *in
@@ -957,6 +972,23 @@ func (in *ProjectList) DeepCopyObject() runtime.Object {
 	return nil
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ProjectStats) DeepCopyInto(out *ProjectStats) {
+	*out = *in
+	out.Warehouses = in.Warehouses
+	out.Stages = in.Stages
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectStats.
+func (in *ProjectStats) DeepCopy() *ProjectStats {
+	if in == nil {
+		return nil
+	}
+	out := new(ProjectStats)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ProjectStatus) DeepCopyInto(out *ProjectStatus) {
 	*out = *in
@@ -967,6 +999,11 @@ func (in *ProjectStatus) DeepCopyInto(out *ProjectStatus) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.Stats != nil {
+		in, out := &in.Stats, &out.Stats
+		*out = new(ProjectStats)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectStatus.
@@ -1481,6 +1518,22 @@ func (in *StageSpec) DeepCopy() *StageSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StageStats) DeepCopyInto(out *StageStats) {
+	*out = *in
+	out.Health = in.Health
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StageStats.
+func (in *StageStats) DeepCopy() *StageStats {
+	if in == nil {
+		return nil
+	}
+	out := new(StageStats)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *StageStatus) DeepCopyInto(out *StageStatus) {
 	*out = *in
@@ -1758,6 +1811,22 @@ func (in *WarehouseSpec) DeepCopy() *WarehouseSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WarehouseStats) DeepCopyInto(out *WarehouseStats) {
+	*out = *in
+	out.Health = in.Health
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WarehouseStats.
+func (in *WarehouseStats) DeepCopy() *WarehouseStats {
+	if in == nil {
+		return nil
+	}
+	out := new(WarehouseStats)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *WarehouseStatus) DeepCopyInto(out *WarehouseStatus) {
 	*out = *in
diff --git a/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml b/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml
index 4f09a30a..4b6fc40b 100644
--- a/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml
+++ b/charts/kargo/resources/crds/kargo.akuity.io_projects.yaml
@@ -163,6 +163,54 @@ spec:
 
                   Deprecated: Use the Conditions field instead.
                 type: string
+              stats:
+                description: |-
+                  Stats contains a summary of the of the collective state of a Project's
+                  constituent resources.
+                properties:
+                  stages:
+                    description: Stages contains a summary of the collective state
+                      of the Project's Stages.
+                    properties:
+                      count:
+                        description: Count contains the total number of Stages in
+                          the Project.
+                        format: int64
+                        type: integer
+                      health:
+                        description: Health contains a summary of the collective health
+                          of a Project's Stages.
+                        properties:
+                          healthy:
+                            description: Healthy contains the number of resources
+                              that are explicitly healthy.
+                            format: int64
+                            type: integer
+                        type: object
+                    type: object
+                  warehouses:
+                    description: |-
+                      Warehouses contains a summary of the collective state of the Project's
+                      Warehouses.
+                    properties:
+                      count:
+                        description: Count contains the total number of Warehouses
+                          in the Project.
+                        format: int64
+                        type: integer
+                      health:
+                        description: |-
+                          Health contains a summary of the collective health of a Project's
+                          Warehouses.
+                        properties:
+                          healthy:
+                            description: Healthy contains the number of resources
+                              that are explicitly healthy.
+                            format: int64
+                            type: integer
+                        type: object
+                    type: object
+                type: object
             type: object
         type: object
     served: true
diff --git a/charts/kargo/templates/management-controller/cluster-roles.yaml b/charts/kargo/templates/management-controller/cluster-roles.yaml
index d3e8a086..e1cde1c3 100644
--- a/charts/kargo/templates/management-controller/cluster-roles.yaml
+++ b/charts/kargo/templates/management-controller/cluster-roles.yaml
@@ -54,4 +54,13 @@ rules:
   - projectconfigs
   verbs:
   - create
+- apiGroups:
+  - kargo.akuity.io
+  resources:
+  - stages
+  - warehouses
+  verbs:
+  - get
+  - list
+  - watch
 {{- end }}  
diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-open-pr.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-open-pr.md
index 5c7730f9..80dafe37 100644
--- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-open-pr.md
+++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-open-pr.md
@@ -18,7 +18,7 @@ GitLab pull/merge requests.
 | Name | Type | Required | Description |
 |------|------|----------|-------------|
 | `repoURL` | `string` | Y | The URL of a remote Git repository. |
-| `provider` | `string` | N | The name of the Git provider to use. Currently only `github`, `gitlab`, `azure`, and `gitea` are supported. Kargo will try to infer the provider if it is not explicitly specified. |
+| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. |
 | `insecureSkipTLSVerify` | `boolean` | N | Indicates whether to bypass TLS certificate verification when interfacing with the Git provider. Setting this to `true` is highly discouraged in production. |
 | `sourceBranch` | `string` | Y | Specifies the source branch for the pull request. |
 | `targetBranch` | `string` | N | The branch to which the changes should be merged. |
diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-wait-for-pr.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-wait-for-pr.md
index a5d359cc..b15fa9a5 100644
--- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-wait-for-pr.md
+++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-wait-for-pr.md
@@ -14,7 +14,7 @@ and is commonly followed by an `argocd-update` step.
 | Name | Type | Required | Description |
 |------|------|----------|-------------|
 | `repoURL` | `string` | Y | The URL of a remote Git repository. |
-| `provider` | `string` | N | The name of the Git provider to use. Currently only `github`, `gitlab`, `azure`, and `gitea` are supported. Kargo will try to infer the provider if it is not explicitly specified. |
+| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. |
 | `insecureSkipTLSVerify` | `boolean` | N | Indicates whether to bypass TLS certificate verification when interfacing with the Git provider. Setting this to `true` is highly discouraged in production. |
 | `prNumber` | `string` | Y | The number of the pull request to wait for. |
 
diff --git a/docs/old-docs/10-overview.md b/docs/old-docs/10-overview.md
deleted file mode 100644
index 61d02dfd..00000000
--- a/docs/old-docs/10-overview.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-sidebar_label: Overview
-description: Find out more about Kargo - a next-generation continuous delivery and application lifecycle orchestration platform for Kubernetes
----
-
-# What is Kargo?
-
-Kargo is a next-generation continuous delivery and application lifecycle
-orchestration platform for Kubernetes. It builds upon
-[GitOps](https://opengitops.dev/) principles and integrates with existing
-technologies, like [Argo CD](https://argoproj.github.io/cd/), to streamline and
-automate the progressive rollout of changes across the many stages of an
-application's lifecycle.
-
-![Screenshot](static/img/kargo-dashboard.png)
-
-Kargo's goal is to provide an intuitive and flexible layer "above" existing GitOps tooling, wherein you can describe the relationships between various application instances deployed to different environments as well as procedures for progressing changes from one application instance's source of truth to the next.
-
-:::info
-Watch the *Multi-Stage Deployment Pipelines the GitOps Way* talk by Jesse Suen & Kent Rancourt of Akuity at GitOpsCon EU 2024.
-
-<center>
-<div style={{position: "relative", width: "100%", "padding-top": "56.25%"}}>
-  <iframe style={{position: "absolute", top: 0, left: 0, width: "100%", height: "100%"}} src="https://www.youtube.com/embed/0B_JODxyK0w" title="Kargo - Multi-Stage Deployment Pipelines using GitOps - Jesse Suen / Kent Rancourt" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen/>
-</div>
-</center>
-:::
-
-:::info
-Join the Akuity Community [Discord server](https://discord.gg/dHJBZw6ewT)!
-:::
-
-## Next Steps
-
-To learn more about Kargo, consider checking out our
-[concepts doc](./concepts) or get hands-on right away with our
-[quickstart](./quickstart)!
diff --git a/docs/old-docs/15-concepts.md b/docs/old-docs/15-concepts.md
deleted file mode 100644
index 5ec5a6a0..00000000
--- a/docs/old-docs/15-concepts.md
+++ /dev/null
@@ -1,399 +0,0 @@
----
-sidebar_label: Key Concepts
-description: Find out more about key Kargo concepts - stages, freight, warehouses, promotions, and more
----
-# Key Kargo Concepts
-
-:::note
-We're currently reorganizing and updating the documentation.
-During this process, you may encounter sections that are incomplete,
-repetitive, or fragmented. Please bear with us as we work to make improvements.
-:::
-
-## The Basics
-
-### What is a Project
-
-A **project** is a collection of related Kargo resources that describe one or
-more delivery pipelines and is the basic unit of organization and tenancy in
-Kargo.
-
-RBAC rules are also defined at the project level and project administrators
-may use projects to define policies, such as whether a **stage** is eligible
-for automatic promotions of new **freight**.
-
-:::note
-For technical information on the corresponding `Project` resource
-type, refer to [Working with Projects](./30-how-to-guides/11-working-with-projects.md).
-:::
-
-### What is a Stage?
-
-When you hear the term “environment”, what you envision will depend
-significantly on your perspective. A developer, for example, may think of
-an "environment" as a specific _instance_ of an application they work on,
-while a DevOps engineer, may think of an "environment" as a particular segment
-of the infrastructure they maintain.
-
-To eliminate confusion, Kargo avoids the term "environment" altogether in favor of **stage**.
-The important feature of a stage is that its name ("test" or "prod," for instance)
-denotes an application instance's _purpose_ and not necessarily its _location_.
-[This blog post](https://akuity.io/blog/kargo-stage-not-environment/) discusses
-the rationale behind this choice.
-
-_Stages are Kargo's most important concept._ They can be linked together in a
-directed acyclic graph to describe a delivery pipeline. Typically, such a
-pipeline may feature a "test" or "dev" stage as its starting point, with one or
-more "prod" stages at the end.
-
-:::note
-For technical details of the corresponding `Stage` resource type,
-refer to [Working with Stages](./30-how-to-guides/14-working-with-stages.md).
-:::
-
-### What is Freight?
-
-**Freight** is Kargo's second most important concept. A single "piece of
-freight" is a set of references to one or more versioned artifacts, which may
-include one or more:
-
-* Container images (from image repositories)
-
-* Kubernetes manifests (from Git repositories)
-
-* Helm charts (from chart repositories)
-
-Freight can therefore be thought of as a sort of meta-artifact. Freight is what
-Kargo seeks to progress from one stage to another.
-For detailed guidance on working with Freight, refer to
-[this guide](./30-how-to-guides/50-working-with-freight.md).
-
-### What is a Warehouse?
-
-A **warehouse** is a _source_ of freight. A warehouse subscribes to one or more:
-
-* Container image repositories
-
-* Git repositories
-
-* Helm charts repositories
-
-Anytime something new is discovered in any repository to which a warehouse
-subscribes, the warehouse produces a new piece of freight.
-
-### What is a Promotion?
-
-A **promotion** is a request to move a piece of freight into a specified stage.
-
-## Corresponding Resource Types
-
-Each of Kargo's fundamental concepts maps directly onto a custom Kubernetes
-resource type.
-
-### `Freight` Resources
-
-Each piece of Kargo freight is represented by a Kubernetes resource of type
-`Freight`. `Freight` resources are immutable except for their `alias` field
-and `status` subresource (mutable only by the Kargo controller).
-
-A single `Freight` resource references one or more versioned artifacts, such as:
-
-* Container images (from image repositories)
-
-* Kubernetes manifests (from Git repositories)
-
-* Helm charts (from chart repositories)
-
-A `Freight` resource's `metadata.name` field is a SHA1 hash of a canonical
-representation of the artifacts referenced by the `Freight` resource. (This is
-enforced by an admission webhook.) The `metadata.name` field is therefore a
-"fingerprint", deterministically derived from the `Freight`'s contents.
-
-To provide a human-readable identifier for a `Freight` resource, a `Freight`
-resource has an `alias` field. This alias is
-a human-readable string that is unique within the `Project` to which the
-`Freight` belongs. Kargo automatically generates unique aliases for all
-`Freight` resources, but users may update them to be more meaningful.
-
-:::tip
-Assigning meaningful and recognizable aliases to important pieces of `Freight`
-traversing your pipeline(s) can make it easier to track their progress from one
-`Stage` to another.
-:::
-
-:::note
-For more information on aliases, refer to the [aliases](./30-how-to-guides/50-working-with-freight.md#aliases)
-and [updating aliases](./30-how-to-guides/50-working-with-freight.md#updating-aliases)
-sections of the "Working with Freight" how-to guide.
-:::
-
-A `Freight` resource's `status` field records a list of `Stage` resources in
-which the `Freight` has been _verified_ and a separate list of `Stage` resources
-for which the `Freight` has been manually _approved_.
-
-`Freight` resources look similar to the following:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Freight
-metadata:
-  name: 47b33c0c92b54439e5eb7fb80ecc83f8626fe390
-  namespace: kargo-demo
-  labels:
-    kargo.akuity.io/alias: fruitful-ferret
-alias: fruitful-ferret
-images:
-- digest: sha256:b2487a28589657b318e0d63110056e11564e73b9fd3ec4c4afba5542f9d07d46
-  repoURL: public.ecr.aws/nginx/nginx
-  tag: 1.27.0
-commits:
-- repoURL: https://github.com/example/kargo-demo.git
-  id: 1234abc
-warehouse: my-warehouse
-status:
-  verifiedIn:
-    test: {}
-  approvedFor:
-    prod: {}
-```
-
-### `Warehouse` Resources
-
-Each Kargo warehouse is represented by a Kubernetes resource of type
-`Warehouse`.
-
-A `Warehouse` resource's most important field is its `spec.subscriptions` field,
-which is used to subscribe to one or more:
-
-* Container image repositories
-
-* Git repositories
-
-* Helm charts repositories
-
-The following example shows a `Warehouse` resource that subscribes to a
-container image repository and a Git repository:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Warehouse
-metadata:
-  name: my-warehouse
-  namespace: kargo-demo
-spec:
-  subscriptions:
-  - image:
-      repoURL: public.ecr.aws/nginx/nginx
-      semverConstraint: ^1.26.0
-  - git:
-      repoURL: https://github.com/example/kargo-demo.git
-```
-:::info
-Kargo uses [semver](https://github.com/masterminds/semver#checking-version-constraints) to handle semantic versioning constraints.
-:::
-
-#### Git Subscription Path Filtering
-
-In some cases, it may be necessary to constrain the paths within a Git
-repository that a `Warehouse` will consider as triggers for `Freight`
-production. This is especially useful for GitOps repositories that are
-"monorepos" containing configuration for multiple applications.
-
-The paths that may or must not trigger `Freight` production may be specified
-using a combination of the `includePaths` and `excludePaths` fields of a Git
-repository subscription.
-
-The following example demonstrates a `Warehouse` with a Git repository
-subscription that will only produce new `Freight` when the latest commit
-(selected by the applicable commit selection strategy) contains changes in the
-`apps/guestbook` directory since the last piece of `Freight` produced by the
-`Warehouse`:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Warehouse
-metadata:
-  name: my-warehouse
-  namespace: kargo-demo
-spec:
-  subscriptions:
-  - git:
-      repoURL: https://github.com/example/kargo-demo.git
-      includePaths:
-      - apps/guestbook
-```
-
-The next example demonstrates the opposite: a `Warehouse` with a Git repository
-subscription that will only produce new `Freight` when the latest commit
-(selected by the applicable commit selection strategy) contains changes to paths
-_other than_ the repository's `docs/` directory:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Warehouse
-metadata:
-  name: my-warehouse
-  namespace: kargo-demo
-spec:
-  subscriptions:
-  - git:
-      repoURL: https://github.com/example/kargo-demo.git
-      excludePaths:
-      - docs
-```
-
-`includePaths` and `excludePaths` may be combined to include a broad set of
-paths and then exclude a subset of those. The following example demonstrates a
-`Warehouse` with a Git repository subscription that will only produce new
-`Freight` when the latest commit (selected by the applicable commit selection
-strategy) contains changes _within_ the `apps/guestbook` directory _other than_
-the `apps/guestbook/README.md`:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Warehouse
-metadata:
-  name: my-warehouse
-  namespace: kargo-demo
-spec:
-  subscriptions:
-  - git:
-      repoURL: https://github.com/example/kargo-demo.git
-      includePaths:
-      - apps/guestbook
-      excludePaths:
-      - apps/guestbook/README.md
-```
-
-:::note
-It is important to understand that new `Freight` will be produced when the
-latest commit (selected by the applicable commit selection strategy) contains
-_even a single change_ that is:
-
-1. Implicitly included via undefined `includePaths`.
-
-   &nbsp;&nbsp;&nbsp;&nbsp;OR
-
-   Explicitly included via `includePaths`.
-
-   AND
-
-2. Not explicitly excluded via `excludePaths`.
-:::
-
-:::info
-By default, the strings in the `includePaths` and `excludePaths` fields are
-treated as exact paths to files or directories. (Selecting a directory will
-implicitly select all paths within that directory.)
-
-Paths may _also_ be specified using glob patterns (by prefixing the string with
-`glob:`) or regular expressions (by prefixing the string with `regex:` or
-`regexp:`).
-:::
-
-### `Promotion` Resources
-
-Each Kargo promotion is represented by a Kubernetes resource of type
-`Promotion`.
-
-A `Promotion` resource's two most important fields are its `spec.freight` and
-`spec.stage` fields, which respectively identify a piece of `Freight` and a
-target `Stage` to which that `Freight` should be promoted.
-
-`Promotions` are, in some cases, created automatically by Kargo. In other cases,
-they are created manually by users. In either case, a `Promotion` resource
-resembles the following:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Promotion
-metadata:
-  name: 47b33c0c92b54439e5eb7fb80ecc83f8626fe390-to-test
-  namespace: kargo-demo
-spec:
-  stage: test
-  freight: 47b33c0c92b54439e5eb7fb80ecc83f8626fe390
-```
-
-:::info
-The name in a `Promotion`'s `metadata.name` field is inconsequential. Only
-the `spec` matters.
-:::
-
-When a `Promotion` has concluded -- whether successfully or unsuccessfully --
-the `Promotion`'s `status` field is updated to reflect the outcome. For example:
-
-```yaml
-status:
-  phase: Succeeded
-```
-
-## Role-Based Access Control
-
-As with all resource types in Kubernetes, permissions to perform various actions
-on resources of different types are governed by
-[RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/).
-
-For all Kargo resource types, Kubernetes RBAC functions exactly as one would
-expect, with one notable exception.
-
-Often, it is necessary to grant a user permission to create `Promotion`
-resources that reference certain `Stage` resources, but not others. To address
-this, Kargo utilizes an admission control webhook that conducts access reviews
-to determine if a user creating a `Promotion` resource has the virtual `promote`
-verb for the `Stage` referenced by the `Promotion` resource.
-
-:::info
-[This blog post](https://blog.aquasec.com/kubernetes-verbs) is an excellent
-primer on virtual verbs in Kubernetes RBAC.
-:::
-
-The following `Role` resource describes permissions to create `Promotion`
-references that reference the `uat` `Stage` only:
-
-```yaml
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  name: uat-promoter
-  namespace: kargo-demo
-rules:
-- apiGroups:
-  - kargo.akuity.io
-  resources:
-  - promotions
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - kargo.akuity.io
-  resources:
-  - stages
-  resourceNames:
-  - uat
-  verbs:
-  - promote
-```
-
-To grant a fictional user `alice`, in the QA department, the ability to promote
-to `uat` only, create a corresponding `RoleBinding`:
-
-```yaml
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  name: alice-uat-promoter
-  namespace: kargo-demo
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: uat-promoter
-subjects:
-- kind: User
-  name: alice
-```
diff --git a/docs/old-docs/20-quickstart.md b/docs/old-docs/20-quickstart.md
deleted file mode 100644
index ac1e0e78..00000000
--- a/docs/old-docs/20-quickstart.md
+++ /dev/null
@@ -1,974 +0,0 @@
----
-description: Learn about Kargo by progressing a change through multiple stages in a local Kubernetes cluster
-sidebar_label: Quickstart
----
-
-# Kargo Quickstart
-
-This guide presents a basic introduction to Kargo. Together, we will:
-
-1. Install Kargo and its dependencies into an existing, local Kubernetes
-   cluster.
-
-    OR
-
-    Create a new local Kubernetes cluster with Kargo and its dependencies
-    already installed.
-
-1. Demonstrate how Kargo can progress changes through multiple stages by
-   interacting with your GitOps repository and Argo CD `Application` resources.
-
-1. Clean up.
-
-:::info
-If you're looking to contribute to Kargo, you may wish to consult the
-[contributor guide](./contributor-guide) instead.
-:::
-
-## Starting a Local Cluster
-
-Any of the following approaches require [Helm](https://helm.sh/docs/) v3.13.1 or
-greater to be installed.
-
-<Tabs groupId="cluster-start">
-<TabItem value="docker-desktop" label="Docker Desktop">
-
-If you are a
-[Docker Desktop](https://www.docker.com/products/docker-desktop/)
-user, you can follow
-[these instructions](https://docs.docker.com/desktop/kubernetes/) to enable
-its built-in Kubernetes support.
-
-:::info
-Although this is one of the fastest paths to a local Kubernetes cluster, be
-aware that Docker Desktop supports only a _single_ Kubernetes cluster. If
-that cluster reaches a state you are dissatisfied with, resetting it will
-remove not just Kargo-related resources, but _all_ your workloads and data.
-:::
-
-```shell
-curl -L https://raw.githubusercontent.com/akuity/kargo/main/hack/quickstart/install.sh | sh
-```
-
-</TabItem>
-<TabItem value="orbstack" label="OrbStack">
-
-[OrbStack](https://orbstack.dev/) is a fast, lightweight, drop-in replacement
-for Docker Desktop for macOS only. You can follow
-[these instructions](https://docs.orbstack.dev/kubernetes/) to enable its
-built-in Kubernetes support.
-
-:::info
-Although this is one of the fastest paths to a local Kubernetes cluster, be
-aware that OrbStack supports only a _single_ Kubernetes cluster. If
-that cluster reaches a state you are dissatisfied with, resetting it will
-remove not just Kargo-related resources, but _all_ your workloads and data.
-:::
-
-```shell
-curl -L https://raw.githubusercontent.com/akuity/kargo/main/hack/quickstart/install.sh | sh
-```
-
-</TabItem>
-<TabItem value="kind" label="kind">
-
-If you have any Docker-compatible container runtime installed (including native
-Docker, Docker Desktop, or OrbStack), you can easily launch a disposable cluster
-just for this quickstart using
-[kind](https://kind.sigs.k8s.io/#installation-and-usage).
-
-```shell
-curl -L https://raw.githubusercontent.com/akuity/kargo/main/hack/quickstart/kind.sh | sh
-```
-
-:::info
-While this option is a bit more complex than using Docker Desktop or OrbStack
-directly, it offers the advantage of being fully-disposable. If your cluster
-reaches a state you are dissatisfied with, you can simply destroy it and
-launch a new one.
-:::
-
-</TabItem>
-<TabItem value="k3d" label="k3d">
-
-If you have any Docker-compatible container runtime installed (including native
-Docker, Docker Desktop, or OrbStack), you can easily launch a disposable cluster
-just for this quickstart using [k3d](https://k3d.io).
-
-```shell
-curl -L https://raw.githubusercontent.com/akuity/kargo/main/hack/quickstart/k3d.sh | sh
-```
-
-:::info
-While this option is a bit more complex than using Docker Desktop or OrbStack
-directly, it offers the advantage of being fully-disposable. If your cluster
-reaches a state you are dissatisfied with, you can simply destroy it and
-launch a new one.
-:::
-
-</TabItem>
-<TabItem value="more-info" label="More Info">
-
-:::info
-If you are averse to piping a downloaded script directly into a shell, please
-feel free to download the applicable script and inspect its contents prior to
-execution.
-
-Any approach you select should only:
-
-1. Launch a new, local Kubernetes cluster, if applicable
-1. Install cert-manager
-1. Install Argo CD
-1. Install Argo Rollouts
-1. Install Kargo
-
-:::
-
-</TabItem>
-</Tabs>
-
-:::note
-If Kargo installation fails with a `401`, verify that you are using Helm v3.13.1
-or greater.
-
-If Kargo installation fails with a `403`, it is likely that Docker is configured
-to authenticate to `ghcr.io` with an expired token. The Kargo chart and images
-are accessible anonymously, so this issue can be resolved simply by logging out:
-
-```shell
-docker logout ghcr.io
-```
-
-:::
-
-At the end of this process:
-
-* The Argo CD dashboard will be accessible at [localhost:31443](https://localhost:31443).
-
-  The username and password are both `admin`.
-
-* The Kargo dashboard will be accessible at [localhost:31444](https://localhost:31444).
-
-  The admin password is `admin`.
-
-* You can safely ignore all cert errors for both of the above.
-
-## Create a GitOps Repository
-
-Let's begin by creating a repository on GitHub to house variations of our
-application manifests for three different stages of a sample application: test,
-UAT, and production.
-
-1. Visit https://github.com/akuity/kargo-demo and fork the repository into your
-   own GitHub account.
-
-1. You can explore the repository and see that the `main` branch contains common
-   configuration in a `base/` directory as well as stage-specific overlays in
-   paths of the form `stages/<stage name>/`.
-
-    :::note
-    This layout is typical of a GitOps repository using
-    [Kustomize](https://kustomize.io/) for configuration management and is not
-    at all Kargo-specific.
-
-    Kargo also works just as well with [Helm](https://helm.sh).
-    :::
-
-1. We'll be using it later, so save the location of your GitOps repository in an
-   environment variable:
-
-   ```shell
-   export GITOPS_REPO_URL=<your repo URL, starting with https://>
-   ```
-
-## Create Argo CD Application Resources
-
-In this step, we will use an Argo CD `ApplicationSet` resource to create and
-manage three Argo CD `Application` resources that deploy the sample application
-at three different stages of its lifecycle, with three slightly different
-configurations, to three different namespaces in our local cluster:
-
-```shell
-cat <<EOF | kubectl apply -f -
-apiVersion: argoproj.io/v1alpha1
-kind: ApplicationSet
-metadata:
-  name: kargo-demo
-  namespace: argocd
-spec:
-  generators:
-  - list:
-      elements:
-      - stage: test
-      - stage: uat
-      - stage: prod
-  template:
-    metadata:
-      name: kargo-demo-{{stage}}
-      annotations:
-        kargo.akuity.io/authorized-stage: kargo-demo:{{stage}}
-    spec:
-      project: default
-      source:
-        repoURL: ${GITOPS_REPO_URL}
-        targetRevision: stage/{{stage}}
-        path: .
-      destination:
-        server: https://kubernetes.default.svc
-        namespace: kargo-demo-{{stage}}
-      syncPolicy:
-        syncOptions:
-        - CreateNamespace=true
-EOF
-```
-
-If you visit [your Argo CD dashboard](https://localhost:31443), you will notice
-all three Argo CD `Application`s have not yet synced because they're not
-configured to do so automatically, and in fact, the branches referenced by their
-`targetRevision` fields do not even exist yet.
-
-![Argo-dashboard-screenshot](static/img/argo-dashboard.png)
-
-## Your First Kargo Project
-
-Up to this point, we haven't done anything with Kargo -- in fact everything
-we've done thus far should be familiar to anyone who's already using Argo CD and
-Kustomize. Now it's time to see what Kargo can do!
-
-To get started, you will need a GitHub
-[personal access token](https://github.com/settings/tokens)
-with adequate permissions to read from and write to the repository you forked in
-the previous section.
-
-1. Save your GitHub handle and your personal access token in environment
-   variables:
-
-   ```shell
-   export GITHUB_USERNAME=<your github handle>
-   export GITHUB_PAT=<your personal access token>
-   ```
-
-1. Next, we'll create several Kargo resources:
-
-    * A `Project`, which, when reconciled, will effect all boilerplate project
-      initialization, including the creation of a specially-labeled `Namespace`
-      with the same name as the `Project`
-
-    * A `Secret` containing credentials for our GitOps repository
-
-    * A `Warehouse` that subscribes to a container image repository
-
-    * Three `Stage` resources -- `test`, `uat`, and `prod`
-
-    Although we will use Kargo's UI throughout most of this quickstart, with the
-    amount of configuration we're about to create, it is easiest to do it
-    declaratively using either `kubectl` or the `kargo` CLI.
-
-    :::info
-    For demo purposes, using `kubectl` is the quickest way to declaratively
-    define your first `Project`. The `kargo` CLI, however, does offer
-    Kargo-specific functionality, and for Kargo users who lack direct access to
-    the underlying cluster, it also offers authentication via OpenID Connect.
-    You might consider choosing this option below if you wish to become more
-    familiar with it.
-    :::
-
-    <Tabs groupId="login-method">
-    <TabItem value="kubectl" label="Using kubectl" default>
-
-    To create Kargo resources, use the following command:
-
-    ```shell
-    cat <<EOF | kubectl apply -f -
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Project
-    metadata:
-      name: kargo-demo
-    ---
-    apiVersion: v1
-    kind: Secret
-    type: Opaque
-    metadata:
-      name: kargo-demo-repo
-      namespace: kargo-demo
-      labels:
-        kargo.akuity.io/cred-type: git
-    stringData:
-      repoURL: ${GITOPS_REPO_URL}
-      username: ${GITHUB_USERNAME}
-      password: ${GITHUB_PAT}
-    ---
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Warehouse
-    metadata:
-      name: kargo-demo
-      namespace: kargo-demo
-    spec:
-      subscriptions:
-      - image:
-          repoURL: public.ecr.aws/nginx/nginx
-          semverConstraint: ^1.26.0
-          discoveryLimit: 5
-    ---
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Stage
-    metadata:
-      name: test
-      namespace: kargo-demo
-    spec:
-      requestedFreight:
-      - origin:
-          kind: Warehouse
-          name: kargo-demo
-        sources:
-          direct: true
-      promotionTemplate:
-        spec:
-          vars:
-          - name: gitopsRepo
-            value: ${GITOPS_REPO_URL}
-          - name: imageRepo
-            value: public.ecr.aws/nginx/nginx
-          - name: srcPath
-            value: ./src
-          - name: outPath
-            value: ./out
-          steps:
-          - uses: git-clone
-            config:
-              repoURL: \${{ vars.gitopsRepo }}
-              checkout:
-              - branch: main
-                path: \${{ vars.srcPath }}
-              - branch: stage/\${{ ctx.stage }}
-                create: true
-                path: \${{ vars.outPath }}
-          - uses: git-clear
-            config:
-              path: \${{ vars.outPath }}
-          - uses: kustomize-set-image
-            as: update
-            config:
-              path: \${{ vars.srcPath }}/base
-              images:
-              - image: \${{ vars.imageRepo }}
-                tag: \${{ imageFrom(vars.imageRepo).Tag }}
-          - uses: kustomize-build
-            config:
-              path: \${{ vars.srcPath }}/stages/\${{ ctx.stage }}
-              outPath: \${{ vars.outPath }}/manifests.yaml
-          - uses: git-commit
-            as: commit
-            config:
-              path: \${{ vars.outPath }}
-              message: \${{ outputs.update.commitMessage }}
-          - uses: git-push
-            config:
-              path: \${{ vars.outPath }}
-          - uses: argocd-update
-            config:
-              apps:
-              - name: kargo-demo-\${{ ctx.stage }}
-                sources:
-                - repoURL: \${{ vars.gitopsRepo }}
-                  desiredRevision: \${{ outputs.commit.commit }}
-    ---
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Stage
-    metadata:
-      name: uat
-      namespace: kargo-demo
-    spec:
-      requestedFreight:
-      - origin:
-          kind: Warehouse
-          name: kargo-demo
-        sources:
-          stages:
-          - test
-      promotionTemplate:
-        spec:
-          vars:
-          - name: gitopsRepo
-            value: ${GITOPS_REPO_URL}
-          - name: imageRepo
-            value: public.ecr.aws/nginx/nginx
-          - name: srcPath
-            value: ./src
-          - name: outPath
-            value: ./out
-          steps:
-          - uses: git-clone
-            config:
-              repoURL: \${{ vars.gitopsRepo }}
-              checkout:
-              - branch: main
-                path: \${{ vars.srcPath }}
-              - branch: stage/\${{ ctx.stage }}
-                create: true
-                path: \${{ vars.outPath }}
-          - uses: git-clear
-            config:
-              path: \${{ vars.outPath }}
-          - uses: kustomize-set-image
-            as: update
-            config:
-              path: \${{ vars.srcPath }}/base
-              images:
-              - image: \${{ vars.imageRepo }}
-                tag: \${{ imageFrom(vars.imageRepo).Tag }}
-          - uses: kustomize-build
-            config:
-              path: \${{ vars.srcPath }}/stages/\${{ ctx.stage }}
-              outPath: \${{ vars.outPath }}/manifests.yaml
-          - uses: git-commit
-            as: commit
-            config:
-              path: \${{ vars.outPath }}
-              message: \${{ outputs.update.commitMessage }}
-          - uses: git-push
-            config:
-              path: \${{ vars.outPath }}
-          - uses: argocd-update
-            config:
-              apps:
-              - name: kargo-demo-\${{ ctx.stage }}
-                sources:
-                - repoURL: \${{ vars.gitopsRepo }}
-                  desiredRevision: \${{ outputs.commit.commit }}
-    ---
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Stage
-    metadata:
-      name: prod
-      namespace: kargo-demo
-    spec:
-      requestedFreight:
-      - origin:
-          kind: Warehouse
-          name: kargo-demo
-        sources:
-          stages:
-          - uat
-      promotionTemplate:
-        spec:
-          vars:
-          - name: gitopsRepo
-            value: ${GITOPS_REPO_URL}
-          - name: imageRepo
-            value: public.ecr.aws/nginx/nginx
-          - name: srcPath
-            value: ./src
-          - name: outPath
-            value: ./out
-          steps:
-          - uses: git-clone
-            config:
-              repoURL: \${{ vars.gitopsRepo }}
-              checkout:
-              - branch: main
-                path: \${{ vars.srcPath }}
-              - branch: stage/\${{ ctx.stage }}
-                create: true
-                path: \${{ vars.outPath }}
-          - uses: git-clear
-            config:
-              path: \${{ vars.outPath }}
-          - uses: kustomize-set-image
-            as: update
-            config:
-              path: \${{ vars.srcPath }}/base
-              images:
-              - image: \${{ vars.imageRepo }}
-                tag: \${{ imageFrom(vars.imageRepo).Tag }}
-          - uses: kustomize-build
-            config:
-              path: \${{ vars.srcPath }}/stages/\${{ ctx.stage }}
-              outPath: \${{ vars.outPath }}/manifests.yaml
-          - uses: git-commit
-            as: commit
-            config:
-              path: \${{ vars.outPath }}
-              message: \${{ outputs.update.commitMessage }}
-          - uses: git-push
-            config:
-              path: \${{ vars.outPath }}
-          - uses: argocd-update
-            config:
-              apps:
-              - name: kargo-demo-\${{ ctx.stage }}
-                sources:
-                - repoURL: \${{ vars.gitopsRepo }}
-                  desiredRevision: \${{ outputs.commit.commit }}
-    EOF
-    ```
-
-    </TabItem>
-
-    <TabItem value="kargo-cli" label="Using the Kargo CLI">
-
-    Install the Kargo CLI:
-
-    <Tabs groupId="os">
-    <TabItem value="general" label="Mac, Linux, or WSL" default>
-
-    ```shell
-    arch=$(uname -m)
-    [ "$arch" = "x86_64" ] && arch=amd64
-    curl -L -o kargo https://github.com/akuity/kargo/releases/latest/download/kargo-"$(uname -s | tr '[:upper:]' '[:lower:]')-${arch}"
-    chmod +x kargo
-    ```
-
-    Then move `kargo` to a location in your file system that is included in the
-    value of your `PATH` environment variable.
-
-    </TabItem>
-    <TabItem value="windows" label="Windows Powershell">
-
-    ```shell
-    Invoke-WebRequest -URI https://github.com/akuity/kargo/releases/latest/download/kargo-windows-amd64.exe -OutFile kargo.exe
-    ```
-
-    Then move `kargo.exe` to a location in your file system that is included in the value
-    of your `PATH` environment variable.
-
-    </TabItem>
-    </Tabs>
-
-    Log in:
-
-    ```shell
-    kargo login https://localhost:31444 \
-      --admin \
-      --password admin \
-      --insecure-skip-tls-verify
-    ```
-
-    To create Kargo resources, use the following command:
-
-    ```shell
-    cat <<EOF | kargo apply -f -
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Project
-    metadata:
-      name: kargo-demo
-    ---
-    apiVersion: v1
-    kind: Secret
-    type: Opaque
-    metadata:
-      name: kargo-demo-repo
-      namespace: kargo-demo
-      labels:
-        kargo.akuity.io/cred-type: git
-    stringData:
-      repoURL: ${GITOPS_REPO_URL}
-      username: ${GITHUB_USERNAME}
-      password: ${GITHUB_PAT}
-    ---
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Warehouse
-    metadata:
-      name: kargo-demo
-      namespace: kargo-demo
-    spec:
-      subscriptions:
-      - image:
-          repoURL: public.ecr.aws/nginx/nginx
-          semverConstraint: ^1.26.0
-          discoveryLimit: 5
-    ---
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Stage
-    metadata:
-      name: test
-      namespace: kargo-demo
-    spec:
-      requestedFreight:
-      - origin:
-          kind: Warehouse
-          name: kargo-demo
-        sources:
-          direct: true
-      promotionTemplate:
-        spec:
-          vars:
-          - name: gitopsRepo
-            value: ${GITOPS_REPO_URL}
-          - name: imageRepo
-            value: public.ecr.aws/nginx/nginx
-          - name: srcPath
-            value: ./src
-          - name: outPath
-            value: ./out
-          steps:
-          - uses: git-clone
-            config:
-              repoURL: \${{ vars.gitopsRepo }}
-              checkout:
-              - branch: main
-                path: \${{ vars.srcPath }}
-              - branch: stage/\${{ ctx.stage }}
-                create: true
-                path: \${{ vars.outPath }}
-          - uses: git-clear
-            config:
-              path: \${{ vars.outPath }}
-          - uses: kustomize-set-image
-            as: update
-            config:
-              path: \${{ vars.srcPath }}/base
-              images:
-              - image: \${{ vars.imageRepo }}
-                tag: \${{ imageFrom(vars.imageRepo).Tag }}
-          - uses: kustomize-build
-            config:
-              path: \${{ vars.srcPath }}/stages/\${{ ctx.stage }}
-              outPath: \${{ vars.outPath }}/manifests.yaml
-          - uses: git-commit
-            as: commit
-            config:
-              path: \${{ vars.outPath }}
-              message: \${{ outputs.update.commitMessage }}
-          - uses: git-push
-            config:
-              path: \${{ vars.outPath }}
-          - uses: argocd-update
-            config:
-              apps:
-              - name: kargo-demo-\${{ ctx.stage }}
-                sources:
-                - repoURL: \${{ vars.gitopsRepo }}
-                  desiredRevision: \${{ outputs.commit.commit }}
-    ---
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Stage
-    metadata:
-      name: uat
-      namespace: kargo-demo
-    spec:
-      requestedFreight:
-      - origin:
-          kind: Warehouse
-          name: kargo-demo
-        sources:
-          stages:
-          - test
-      promotionTemplate:
-        spec:
-          vars:
-          - name: gitopsRepo
-            value: ${GITOPS_REPO_URL}
-          - name: imageRepo
-            value: public.ecr.aws/nginx/nginx
-          - name: srcPath
-            value: ./src
-          - name: outPath
-            value: ./out
-          steps:
-          - uses: git-clone
-            config:
-              repoURL: \${{ vars.gitopsRepo }}
-              checkout:
-              - branch: main
-                path: \${{ vars.srcPath }}
-              - branch: stage/\${{ ctx.stage }}
-                create: true
-                path: \${{ vars.outPath }}
-          - uses: git-clear
-            config:
-              path: \${{ vars.outPath }}
-          - uses: kustomize-set-image
-            as: update
-            config:
-              path: \${{ vars.srcPath }}/base
-              images:
-              - image: \${{ vars.imageRepo }}
-                tag: \${{ imageFrom(vars.imageRepo).Tag }}
-          - uses: kustomize-build
-            config:
-              path: \${{ vars.srcPath }}/stages/\${{ ctx.stage }}
-              outPath: \${{ vars.outPath }}/manifests.yaml
-          - uses: git-commit
-            as: commit
-            config:
-              path: \${{ vars.outPath }}
-              message: \${{ outputs.update.commitMessage }}
-          - uses: git-push
-            config:
-              path: \${{ vars.outPath }}
-          - uses: argocd-update
-            config:
-              apps:
-              - name: kargo-demo-\${{ ctx.stage }}
-                sources:
-                - repoURL: \${{ vars.gitopsRepo }}
-                  desiredRevision: \${{ outputs.commit.commit }}
-    ---
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Stage
-    metadata:
-      name: prod
-      namespace: kargo-demo
-    spec:
-      requestedFreight:
-      - origin:
-          kind: Warehouse
-          name: kargo-demo
-        sources:
-          stages:
-          - uat
-      promotionTemplate:
-        spec:
-          vars:
-          - name: gitopsRepo
-            value: ${GITOPS_REPO_URL}
-          - name: imageRepo
-            value: public.ecr.aws/nginx/nginx
-          - name: srcPath
-            value: ./src
-          - name: outPath
-            value: ./out
-          steps:
-          - uses: git-clone
-            config:
-              repoURL: \${{ vars.gitopsRepo }}
-              checkout:
-              - branch: main
-                path: \${{ vars.srcPath }}
-              - branch: stage/\${{ ctx.stage }}
-                create: true
-                path: \${{ vars.outPath }}
-          - uses: git-clear
-            config:
-              path: \${{ vars.outPath }}
-          - uses: kustomize-set-image
-            as: update
-            config:
-              path: \${{ vars.srcPath }}/base
-              images:
-              - image: \${{ vars.imageRepo }}
-                tag: \${{ imageFrom(vars.imageRepo).Tag }}
-          - uses: kustomize-build
-            config:
-              path: \${{ vars.srcPath }}/stages/\${{ ctx.stage }}
-              outPath: \${{ vars.outPath }}/manifests.yaml
-          - uses: git-commit
-            as: commit
-            config:
-              path: \${{ vars.outPath }}
-              message: \${{ outputs.update.commitMessage }}
-          - uses: git-push
-            config:
-              path: \${{ vars.outPath }}
-          - uses: argocd-update
-            config:
-              apps:
-              - name: kargo-demo-\${{ ctx.stage }}
-                sources:
-                - repoURL: \${{ vars.gitopsRepo }}
-                  desiredRevision: \${{ outputs.commit.commit }}
-    EOF
-    ```
-
-    </TabItem>
-    </Tabs>
-
-1. Navigate to the [Kargo Dashboard](https://localhost:31444/):
-
-   1. Log in using the password `admin`.
-
-      This will take you to a list of `Project`s.  It currently includes only
-      the one created in the previous step.
-
-      ![Kargo-dashboard](static/img/kargo-projects.png)
-
-   1. Select <Hlt>kargo-demo</Hlt>:
-
-      Here you can see a detailed overview of the `Project` we previously
-      created. It includes:
-
-      * An interactive, visual representation of your pipeline, composed of:
-        * A container image repository.
-        * A `Warehouse` that discovers new images as they are pushed to the
-          repository.
-        * Three `Stage`s representing distinct instances of our demo
-          application.
-
-        ![Kargo-dashboard-screenshot](static/img/kargo-dashboard-projects.png)
-
-      * An interactive <Hlt>Freight Timeline</Hlt> with `Freight` ordered
-        chronologically, with newer `Freight` to the left and older `Freight` to
-        the right.
-
-        ![Kargo-Freight-Timeline](static/img/kargo-frieght-timeline.png)
-
-1. After a few seconds, a piece of `Freight` should appear in the <Hlt>Freight
-   Timeline</Hlt>, if it isn't there already.
-
-    :::note
-    Note that the timeline _may_ not refresh automatically and you may need to
-    refresh the page to see new `Freight`.
-
-    This inconvenience will be addressed in a forthcoming update.
-    :::
-
-    :::info
-    `Freight` is a set of references to one or more versioned artifacts, which
-    may include:
-
-      * Container images (from image repositories)
-
-      * Kubernetes manifests (from Git repositories)
-
-      * Helm charts (from chart repositories)
-
-    This introductory example has `Freight` that references only a specific
-    version of the `public.ecr.aws/nginx/nginx` container image.
-    :::
-
-## Your First Promotion
-
-1. To promote `Freight` to the `test` `Stage`, select the target icon on the
-   left border of <Hlt>test</Hlt>:
-
-    ![Kargo-Promote](static/img/kargo-promote-option.png)
-
-    Next, select the `Freight` from the <Hlt>Freight Timeline</Hlt> and confirm
-    the promotion by selecting <Hlt>Yes</Hlt>:
-
-    ![Kargo-Promote](static/img/kargo-promote-option-2.png)
-
-    When promotion process is complete, you'll see a check mark next to
-    <Hlt>test</Hlt>, indicating that the promotion was successful.
-
-    ![Kargo-dashboard-screenshot](static/img/kargo-dashboard-promotion.png)
-
-    Following the promotion, health checks will run periodically. When a `Stage`
-    is in a healthy state, this will be reflected with a heart icon. You can
-    also verify the status by visiting the test instance of the demo application
-    at [localhost:30081](http://localhost:30081).
-
-    The <Hlt>Freight Timeline</Hlt> will also automatically update following the
-    promotion. It is color-coded to indicate which `Stage`s are actively using
-    each piece of `Freight`.
-
-1. Select the <Hlt>test</Hlt> to reveal additional details about the `Stage`
-   including its status, current `Freight`, and history.
-
-1. Select the `Freight` from the <Hlt>Freight Timeline</Hlt> to reveal
-   additional details. Importantly, you can see that (by virtue of the `test`
-   `Stage` having achieved a healthy state) the `Freight` is now _verified_ in
-   `test`, which designates it as eligible for promotion to the next `Stage` --
-   in our case, `uat`.
-
-   ![Kargo-Freight-Verified](static/img/kargo-freight-verified.png)
-
-    :::note
-    Although this example does not demonstrate it, it is also possible to verify
-    the `Freight` in a `Stage` using user-defined processes. See the
-    [relevant section](./30-how-to-guides/14-working-with-stages.md#verifications)
-    to learn more.
-    :::
-
-## Behind the Scenes
-
-So what has Kargo done behind the scenes?
-
-Visiting our fork of https://github.com/akuity/kargo-demo, we will see that
-Kargo has recently created a `stage/test` branch for us. It has taken the latest
-manifests from the `main` branch as a starting point, run `kustomize edit set
-image` and `kustomize build` within the `stages/test/` directory, and written
-the resulting manifests to a stage-specific branch -- the same branch referenced
-by the `test` Argo CD `Application`'s `targetRevision` field.
-
-:::info
-Although not required for all uses cases, using stage-specific branches is a
-practice highly recommended by the Kargo team.
-:::
-
-## Promote to UAT and then Production
-
-Unlike our `test` `Stage`, which subscribes directly to an image repository,
-our `uat` and `prod` `Stage`s both subscribe to other, _upstream_ `Stage`s,
-thereby forming a _pipeline_:
-
-1. `uat` subscribes to `test`
-1. `prod` subscribes to `uat`.
-
-We leave it as an exercise to the reader to use the dashboard to progress the
-`Freight` from `test` to `uat` and again from `uat` to `prod`.
-
-:::info
-The `uat` and `prod` instances of our site should be accessible at:
-
-* `uat`: [localhost:30082](http://localhost:30082)
-* `prod`: [localhost:30083](http://localhost:30083)
-:::
-
-:::info
-It is possible to automate promotion of new, qualified `Freight` for designated
-`Stage`s and also possible to used RBAC to limit who can trigger manual
-promotions for each `Stage`, however, both these topics are beyond the scope of
-this introduction.
-:::
-
-## Cleaning up
-
-Congratulations! You've just gotten hands on with Kargo for the first time!
-
-Now let's clean up!
-
-<Tabs groupId="cluster-start">
-<TabItem value="docker-desktop" label="Docker Desktop">
-
-Docker Desktop supports only a _single_ Kubernetes cluster. If you are
-comfortable deleting not just Kargo-related resources, but _all_ your
-workloads and data, the cluster can be reset from the Docker Desktop
-Dashboard.
-
-If, instead, you wish to preserve non-Kargo-related workloads and data, you
-will need to manually uninstall Kargo and its prerequisites:
-
-```shell
-curl -L https://raw.githubusercontent.com/akuity/kargo/main/hack/quickstart/uninstall.sh | sh
-```
-
-</TabItem>
-<TabItem value="orbstack" label="OrbStack">
-
-OrbStack supports only a _single_ Kubernetes cluster. If you are
-comfortable deleting not just Kargo-related resources, but _all_ your
-workloads and data, you can destroy the cluster with:
-
-```shell
-orb delete k8s
-```
-
-If, instead, you wish to preserve non-Kargo-related workloads and data, you
-will need to manually uninstall Kargo and its prerequisites:
-
-```shell
-curl -L https://raw.githubusercontent.com/akuity/kargo/main/hack/quickstart/uninstall.sh | sh
-```
-
-</TabItem>
-<TabItem value="kind" label="kind">
-
-Simply destroy the cluster:
-
-```shell
-kind delete cluster --name kargo-quickstart
-```
-
-</TabItem>
-<TabItem value="k3d" label="k3d">
-
-Simply destroy the cluster:
-
-```shell
-k3d cluster delete kargo-quickstart
-```
-
-</TabItem>
-</Tabs>
diff --git a/docs/old-docs/30-how-to-guides/10-installing-kargo.md b/docs/old-docs/30-how-to-guides/10-installing-kargo.md
deleted file mode 100644
index 47fc77fe..00000000
--- a/docs/old-docs/30-how-to-guides/10-installing-kargo.md
+++ /dev/null
@@ -1,90 +0,0 @@
----
-description: Learn how to install Kargo using this step-by-step guide
-sidebar_label: Installing Kargo
----
-
-# Installing Kargo
-
-## Basic Installation
-
-Installing Kargo with default configuration is quick and easy.
-
-You will need:
-
-* [Helm](https://helm.sh/docs/): These instructions were tested with v3.13.1.
-* A Kubernetes cluster with [cert-manager](https://cert-manager.io/),
-  [Argo CD](https://argo-cd.readthedocs.io), and
-  [Argo Rollouts](https://argoproj.github.io/argo-rollouts/)
-  pre-installed. These instructions were tested with:
-    * Kubernetes: v1.27.4
-    * cert-manager: v1.11.5
-    * Argo CD: v2.9.3
-    * Argo Rollouts: v1.6.4
-
-:::info
-`cert-manager` is used for self-signing a certificate used to identify Kargo's
-webhook server to the Kubernetes API server. If you do not wish to use
-`cert-manager` for this purpose, you may provision your own certificate through
-other means. Refer to the advanced installation section for more information.
-:::
-
-:::info
-We are working toward transitioning Argo CD and Argo Rollouts from required
-dependencies to _suggested_ dependencies.
-:::
-
-:::note
-If your Argo CD control plane manages multiple Kubernetes clusters, be advised
-that Kargo is intended to be installed into the same cluster as the Argo CD
-control plane and _not_ into the individual clusters that Argo CD is managing.
-:::
-
-The following command will install Kargo with default configuration and a
-user-specified admin password:
-
-```shell
-helm install kargo \
-  oci://ghcr.io/akuity/kargo-charts/kargo \
-  --namespace kargo \
-  --create-namespace \
-  --set api.adminAccount.passwordHash='$2a$10$Zrhhie4vLz5ygtVSaif6o.qN36jgs6vjtMBdM6yrU1FOeiAAMMxOm' \
-  --set api.adminAccount.tokenSigningKey=iwishtowashmyirishwristwatch \
-  --wait
-```
-
-:::caution
-If deploying to an internet-facing cluster, be certain to do one of the
-following:
-
-* Disable the admin account with `--set api.adminAccount.enabled=false`
-
-* Choose your own strong password and signing key. 
-:::
-
-## Advanced Installation
-
-1. Extract the default values from the Helm chart and save it to a convenient
-   location. In the example below, we save it to `~/kargo-values.yaml`
-
-   ```shell
-   helm inspect values \
-     oci://ghcr.io/akuity/kargo-charts/kargo > ~/kargo-values.yaml
-   ```
-
-1. Edit and save the values.
-
-   :::info
-   You will find this configuration file contains helpful comments for every
-   option, so specific options are not covered in detail here.
-   :::
-
-1. Proceed with installation, using your modified values:
-
-   ```shell
-   helm install kargo \
-     oci://ghcr.io/akuity/kargo-charts/kargo \
-     --namespace kargo \
-     --create-namespace \
-     --values ~/kargo-values.yaml \
-     --wait
-   ```
diff --git a/docs/old-docs/30-how-to-guides/11-working-with-projects.md b/docs/old-docs/30-how-to-guides/11-working-with-projects.md
deleted file mode 100644
index 5971032c..00000000
--- a/docs/old-docs/30-how-to-guides/11-working-with-projects.md
+++ /dev/null
@@ -1,116 +0,0 @@
----
-description: Learn how to work effectively with Projects
-sidebar_label: Working with Projects
----
-
-# Working with Projects
-
-Each Kargo project is represented by a cluster-scoped Kubernetes resource of
-type `Project`. Reconciliation of such a resource effects all boilerplate
-project initialization, including the creation of a specially-labeled
-`Namespace` with the same name as the `Project`. All resources belonging to a
-given `Project` should be grouped together in that `Namespace`.
-
-A minimal `Project` resource looks like the following:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Project
-metadata:
-  name: example
-```
-
-:::note
-Deletion of a `Project` resource results in the deletion of the corresponding
-`Namespace`. For convenience, the inverse is also true -- deletion of a
-project's `Namespace` results in the deletion of the corresponding `Project`
-resource.
-:::
-
-:::info
-There are compelling advantages to using `Project` resources instead of
-permitting users to create `Namespace` resources directly:
-
-* The required label indicating a `Namespace` is a Kargo project cannot be
-forgotten or misapplied.
-
-* Users can be granted permission to indirectly create `Namespace` resources for
-Kargo projects _only_ without being granted more general permissions to create
-_any_ new `Namespace` directly.
-
-* Boilerplate configuration is automatically created at the time of `Project`
-creation. This includes things such as project-level RBAC resources and
-`ServiceAccount` resources.
-:::
-
-## Promotion Policies
-
-A `ProjectConfig` resource defines project-level configuration for an associated
-`Project`. At present, this only includes **promotion policies** that describe
-which `Stage`s are eligible for automatic promotion of newly available `Freight`.
-
-The `ProjectConfig` resource must have the same name as its associated `Project`
-and be created in the `Namespace` of the `Project`. This separation of
-configuration from the `Project` resource enables more granular RBAC control.
-Users can be granted permission to modify project configurations via
-`ProjectConfig` resources without necessarily having broader access to `Project`
-resources themselves.
-
-In the example below, the `test` and `uat` `Stage`s are eligible for automatic
-promotion of newly available `Freight`, but any other `Stage`s in the `Project`
-are not:
-
-```yaml
----
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Project
-metadata:
-   name: example
----
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Project
-metadata:
-  name: example
-  namespace: example
-spec:
-  promotionPolicies:
-  - stage: test
-    autoPromotionEnabled: true
-  - stage: uat
-    autoPromotionEnabled: true
-```
-
-## Namespace Adoption
-
-At times, `Namespace`s may require specific configuration to
-comply with regulatory or organizational requirements. To
-account for this, Kargo supports the adoption of pre-existing
-`Namespace`s that are labeled with `kargo.akuity.io/project: "true"`.
-This enables pre-configuring such `Namespace`s according to your
-own requirements.
-
-:::info
-Requiring a `Namespace` to have the `kargo.akuity.io/project: "true"` label to
-be eligible for adoption by a new `Project` is intended to prevent accidental or
-willful hijacking of an existing `Namespace`.
-:::
-
-The following example demonstrates adoption of a `Namespace` that's been
-pre-configured with a label unrelated to Kargo:
-
-```yaml
-apiVersion: v1
-kind: Namespace
-metadata:
-  name: example
-labels:
-  kargo.akuity.io/project: "true"
-  example.com/org: platform-eng
----
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Project
-metadata:
-  name: example
-spec:
-  # ...
-```
diff --git a/docs/old-docs/30-how-to-guides/14-working-with-stages.md b/docs/old-docs/30-how-to-guides/14-working-with-stages.md
deleted file mode 100644
index f3f4787b..00000000
--- a/docs/old-docs/30-how-to-guides/14-working-with-stages.md
+++ /dev/null
@@ -1,590 +0,0 @@
----
-description: Learn how to work effectively with Stages
-sidebar_label: Working with Stages
----
-
-# Working with Stages
-
-Each Kargo Stage is represented by a Kubernetes resource of type `Stage`.
-
-## The `Stage` Resource Type
-
-Like most Kubernetes resources, a `Stage` is composed of a user-defined `spec` field
-and a system-populated `status` field.
-
-A `Stage` resource's `spec` field is itself composed of three main areas of concern:
-
-* Requested Freight
-
-* Promotion template
-
-* Verification
-
-The following sections will explore each of these `spec` sections as well as `status` in greater detail.
-
-### Requested Freight
-
-The `spec.requestedFreight` field is used to describe one or more "types" of
-`Freight`, as specified by an `origin`, that the `Stage`'s promotion process, as
-specified by `spec.promotionTemplate`, will operate on, and the acceptable
-sources from which to obtain that `Freight`. Those sources may include the
-origin itself (e.g. a `Warehouse`) and/or any number of "upstream" `Stage`
-resources.
-
-:::info
-`Warehouse`s are the only type of origin at present, but it is anticipated that
-future versions of Kargo will introduce additional origin types. This is why
-"types" of `Freight` are described by an `origin` field having `kind` and `name`
-subfields instead of being described only by the name of a `Warehouse`.
-:::
-
-For each `Stage`, the Kargo controller will periodically check for `Freight`
-resources that are newly available for promotion to that `Stage`.
-
-When a `Stage` accepts `Freight` directly from its origin, _all_ new `Freight`
-created by that origin (e.g. a `Warehouse` ) are immediately available for
-promotion to that `Stage`.
-
-When a `Stage` accepts `Freight` from one or more "upstream" `Stage` resources,
-`Freight` is considered available for promotion to that `Stage` only after being
-_verified_ in at least one of the upstream `Stage`s. Alternatively, users with
-adequate permissions may manually _approve_ `Freight` for promotion to any given
-`Stage` without requiring upstream verification.
-
-:::tip
-Explicit approvals are a useful method for applying the occasional "hotfix"
-without waiting for a `Freight` resource to traverse the entirety of a pipeline.
-:::
-
-In the following example, the `test` `Stage` requests `Freight` that has
-originated from the `my-warehouse` `Warehouse` and indicates that it will accept
-new `Freight` _directly_ from that origin:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Stage
-metadata:
-  name: test
-  namespace: kargo-demo
-spec:
-  requestedFreight:
-  - origin:
-      kind: Warehouse
-      name: my-warehouse
-    sources:
-      direct: true
-  # ...
-# ...
-```
-
-In this example, the `uat` `Stage` requests `Freight` that has originated from
-the `my-warehouse` `Warehouse`, but indicates that it will accept such `Freight`
-only after it has been _verified_ in the `test` `Stage`:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Stage
-metadata:
-  name: uat
-  namespace: kargo-demo
-spec:
-  requestedFreight:
-  - origin:
-      kind: Warehouse
-      name: my-warehouse
-    sources:
-      stages:
-      - test
-  # ...
-```
-
-Stages may also request `Freight` from multiple sources. The following example
-illustrates a `Stage` that requests `Freight` from both a `microservice-a` and
-`microservice-b` `Warehouse`:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Stage
-metadata:
-  name: test
-  namespace: kargo-demo
-spec:
-  requestedFreight:
-  - origin:
-      kind: Warehouse
-      name: microservice-a
-    sources:
-      direct: true
-  - origin:
-      kind: Warehouse
-      name: microservice-b
-    sources:
-      direct: true
-  # ...
-```
-
-:::tip
-By requesting `Freight` from multiple sources, a `Stage` can effectively
-participate in _multiple pipelines_ that may each deliver different collections
-of artifacts independently of the others.
-
-__This is _advanced_ configuration. If you're very new to Kargo
-and requesting `Freight` from multiple origins, you probably
-didn't mean to.__
-:::
-
-### Promotion Templates
-
-The `spec.promotionTemplate` field is used to describe _how_ to transition
-`Freight` into the `Stage`. The `spec.promotionTemplate.steps` field describes
-the discrete steps of a promotion process in detail.
-
-In the following, very common example, the `promotionTemplate` describes steps
-to:
-
-1. Clone a Git repository containing Kubernetes manifests and Kustomize
-   configuration, checking out two different branches to two different
-   directories.
-
-1. Clears the contents of one working tree, with intentions to fully replace its
-   contents.
-
-1. Runs the equivalent of `kustomize edit set image` to update a
-   `kustomization.yaml` file with a reference to an updated
-   `public.ecr.aws/nginx/nginx` container image.
-
-1. Renders the updated manifests using the equivalent of `kustomize build`.
-
-1. Commits the updated manifests and pushes them to the `stage/test` of the
-   remote repository.
-
-1. Forces Argo CD to sync the `kargo-demo-test` application to the latest commit
-   of the `stage/test` branch.
-
-```yaml
-promotionTemplate:
-  spec:
-    vars:
-    - name: gitopsRepo
-      value: https://github.com/example/repo.git
-    - name: imageRepo
-      value: public.ecr.aws/nginx/nginx
-    - name: srcPath
-      value: ./src
-    - name: outPath
-      value: ./out
-    - name: targetBranch
-      value: stage/${{ ctx.stage }}
-    steps:
-    - uses: git-clone
-      config:
-        repoURL: ${{ vars.gitopsRepo }}
-        checkout:
-        - branch: main
-          path: ${{ vars.srcPath }}
-        - branch: stage/${{ ctx.stage }}
-          create: true
-          path: ${{ vars.outPath }}
-    - uses: git-clear
-      config:
-        path: ${{ vars.outPath }}
-    - uses: kustomize-set-image
-      as: update-image
-      config:
-        path: ${{ vars.srcPath }}/base
-        images:
-        - image: ${{ vars.imageRepo }}
-    - uses: kustomize-build
-      config:
-        path: ${{ vars.srcPath }}/stages/${{ ctx.stage }}
-        outPath: ${{ vars.outPath }}/manifests.yaml
-    - uses: git-commit
-      as: commit
-      config:
-        path: ${{ vars.outPath }}
-        message: ${{ outputs['update-image'].commitMessage }}
-    - uses: git-push
-      config:
-        path: ${{ vars.outPath }}
-        branch: ${{ vars.targetBranch }}
-    - uses: argocd-update
-      config:
-        apps:
-        - name: kargo-demo-${{ ctx.stage }}
-          sources:
-          - repoURL: ${{ vars.gitopsRepo }}
-            desiredRevision: ${{ outputs.commit.commit }}
-```
-
-:::info
-For complete documentation of all Kargo's built-in promotion steps, refer
-to the [Promotion Steps Reference](../35-references/10-promotion-steps.md).
-:::
-
-### Verifications
-
-The `spec.verification` field is used to describe optional verification
-processes that should be executed after a `Promotion` has successfully deployed
-`Freight` to a `Stage`, and if applicable, after the `Stage` has reached a
-healthy state.
-
-Verification processes are defined through _references_ to one or more
-[Argo Rollouts `AnalysisTemplate` resources](https://argoproj.github.io/argo-rollouts/features/analysis/)
-that reside in the same `Project`/`Namespace` as the `Stage` resource.
-
-:::info
-Argo Rollouts `AnalysisTemplate` resources (and the `AnalysisRun` resources that
-are spawned from them) were intentionally built to be re-usable in contexts
-other than Argo Rollouts. Re-using this resource type to define verification
-processes means those processes benefit from this rich and battle-tested feature
-of Argo Rollouts.
-:::
-
-The following example depicts a `Stage` resource that references an
-`AnalysisTemplate` named `kargo-demo` to validate the `test` `Stage` after any
-successful `Promotion`:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Stage
-metadata:
-  name: test
-  namespace: kargo-demo
-spec:
-  # ...
-  verification:
-    analysisTemplates:
-    - name: kargo-demo
-```
-
-It is also possible to specify additional labels, annotations, and arguments
-that should be applied to `AnalysisRun` resources spawned from the referenced
-`AnalysisTemplate`:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Stage
-metadata:
-  name: test
-  namespace: kargo-demo
-spec:
-  # ...
-  verification:
-    analysisTemplates:
-    - name: kargo-demo
-    analysisRunMetadata:
-      labels:
-        foo: bar
-      annotations:
-        bat: baz
-    args:
-    - name: foo
-      value: bar
-```
-
-An `AnalysisTemplate` could be as simple as the following, which merely executes
-a Kubernetes `Job` that is defined inline:
-
-```yaml
-apiVersion: argoproj.io/v1alpha1
-kind: AnalysisTemplate
-metadata:
-  name: kargo-demo
-  namespace: kargo-demo
-spec:
-  metrics:
-  - name: test
-    provider:
-      job:
-        metadata:
-        spec:
-          backoffLimit: 1
-          template:
-            spec:
-              containers:
-              - name: test
-                image: alpine:latest
-                command:
-                - sleep
-                - "10"
-              restartPolicy: Never
-```
-
-:::note
-Please consult the
-[relevant sections](https://argoproj.github.io/argo-rollouts/features/analysis/)
-of the Argo Rollouts documentation for comprehensive coverage of the full range
-of `AnalysisTemplate` capabilities.
-:::
-
-### Status
-
-The `status` field of a `Stage` resource records:
-
-  * Conditions containing the last observations of the `Stage`'s current state.
-
-  * The current phase of the `Stage`'s lifecycle (distilled from the conditions).
-
-  * Details about the last `Promotion` and any in-progress `Promotion`.
-
-  * History of `Freight` that has been deployed to the `Stage` (from most to
-    least recent) along with the results of any associated verification processes.
-
-  * The health status of related Argo CD `Application` resources.
-
-For example:
-
-```yaml
-status:
-  freightHistory:
-  - id: 101bca5b0e18ca7913978a1da956308d2544f741
-    items:
-      Warehouse/my-warehouse:
-        commits:
-        - healthCheckCommit: 111eaf55aa41f21bb9bb707ba1baa748b83ec51e
-          id: 961cfaedbc53aacdb65110028839a2c1c281290d
-          repoURL: https://github.com/example/kargo-demo.git
-        images:
-        - digest: sha256:b2487a28589657b318e0d63110056e11564e73b9fd3ec4c4afba5542f9d07d46
-          repoURL: public.ecr.aws/nginx/nginx
-          tag: 1.27.0
-        name: 666209fd9755a1e48bec6b27f5f447747410dd9e
-        origin:
-          kind: Warehouse
-          name: my-warehouse
-    verificationHistory:
-    - analysisRun:
-        name: test.01j2w7aknhf3j7jteyqs72hnbg.101bca5
-        namespace: kargo-demo
-        phase: Successful
-      finishTime: "2024-07-15T22:13:57Z"
-      id: 5535a484-bbd0-4f12-8cf4-be2c8e0041c9
-      phase: Successful
-      startTime: "2024-07-15T22:13:34Z"
-  health:
-    argoCDApps:
-    - healthStatus:
-        status: Healthy
-      name: kargo-demo-test
-      namespace: argocd
-      syncStatus:
-        revision: 111eaf55aa41f21bb9bb707ba1baa748b83ec51e
-        status: Synced
-    status: Healthy
-  lastPromotion:
-    finishedAt: "2024-07-15T22:13:25Z"
-    freight:
-      commits:
-      - healthCheckCommit: 111eaf55aa41f21bb9bb707ba1baa748b83ec51e
-        id: 961cfaedbc53aacdb65110028839a2c1c281290d
-        repoURL: https://github.com/example/kargo-demo.git
-      name: 666209fd9755a1e48bec6b27f5f447747410dd9e
-      origin:
-        kind: Warehouse
-        name: kargo-demo
-    name: test.01j2w7a15cxjjgejresfyw6ysp.666209f
-    status:
-      finishedAt: "2024-07-15T22:13:25Z"
-      freight:
-        commits:
-        - healthCheckCommit: 111eaf55aa41f21bb9bb707ba1baa748b83ec51e
-          id: 961cfaedbc53aacdb65110028839a2c1c281290d
-          repoURL: https://github.com/example/kargo-demo.git
-        name: 666209fd9755a1e48bec6b27f5f447747410dd9e
-        origin:
-          kind: Warehouse
-          name: kargo-demo
-      freightCollection:
-        id: 101bca5b0e18ca7913978a1da956308d2544f741
-        items:
-          Warehouse/kargo-demo:
-            commits:
-            - healthCheckCommit: 111eaf55aa41f21bb9bb707ba1baa748b83ec51e
-              id: 961cfaedbc53aacdb65110028839a2c1c281290d
-              repoURL: https://github.com/example/kargo-demo.git
-            name: 666209fd9755a1e48bec6b27f5f447747410dd9e
-            origin:
-              kind: Warehouse
-              name: kargo-demo
-        verificationHistory:
-        - analysisRun:
-            name: test.01j2w7aknhf3j7jteyqs72hnbg.101bca5
-            namespace: kargo-demo
-            phase: ""
-          id: 5535a484-bbd0-4f12-8cf4-be2c8e0041c9
-          phase: Pending
-          startTime: "2024-07-15T22:13:34Z"
-      phase: Succeeded
-  observedGeneration: 1
-  phase: Steady
-```
-
-## Interacting with Stages
-
-Kargo provides tools to manage Stages using either its UI or
-CLI. This section explains how to handle Stages effectively through both interfaces.
-
-:::info
-Users with credentials for and sufficient permissions within the Kargo control plane's Kubernetes cluster can also manage `Stage` resources using `kubectl`.
-:::
-
-### Creating a Stage
-
-<Tabs groupId="create-stage">
-<TabItem value="ui" label="Using the UI" default>
-
-1. Navigate to your Project in the Kargo UI and locate the action menu in the upper right corner of the pipeline:
-
-   ![create-stage](../static/img/create-stage.png)
-
-1. Click the magic wand icon to open the dropdown, then select <Hlt>Create Stage</Hlt>.
-
-   A form will appear to input details for a new `Stage`:
-
-   ![create-stage](../static/img/create-stage-2.png)
-
-1. Complete the form with the necessary details and submit it.
-
-   The new `Stage` will be added to the pipeline, connected to other
-   `Stage`s based on your configuration:
-
-   ![create-stage](../static/img/create-stage-3.png)
-
-</TabItem>
-
-<TabItem value="cli" label="Using the CLI">
-
-1. Define the `Stage` in a YAML file, for example:
-
-    ```yaml
-    apiVersion: kargo.akuity.io/v1alpha1
-    kind: Stage
-    metadata:
-      name: <stage>
-      namespace: <project>
-    spec:
-      ### Add your Stage specifications here
-    ```
-
-1. Save the file and run:
-
-    ```shell
-    kargo create -f <filename>
-    ```
-
-1. Verify the creation by listing `Stage`s:
-
-    ```shell
-    kargo get stage <stage> --project <project>
-    ```
-
-</TabItem>
-</Tabs>
-
-### Promoting Freight to a Stage
-
-:::note
-For comprehensive guidance on promoting `Freight` to a `Stage`, refer to [Working with Promotions](../15-concepts.md#promotion-resources).
-:::
-
-### Deleting a Stage
-
-<Tabs groupId="delete-stage">
-<TabItem value="ui" label="Using the UI" default>
-
-1. Select the `Stage` you want to remove.
-
-1. Click <Hlt>Delete</Hlt> in the upper right corner of the pop-up window:
-
-   ![delete-stage](../static/img/delete-stage.png)
-
-</TabItem>
-
-<TabItem value="cli" label="Using the CLI">
-
-To delete a `Stage` using the CLI, run:
-
-```shell
-kargo delete stage <stage> --project <project>
-```
-
-</TabItem>
-</Tabs>
-
-### Refreshing a Stage
-
-Refreshing a `Stage` triggers its reconciliation process, which
-includes checking for any newly-completed Promotions, queueing
-up the next, pending Promotion, when applicable, and executing
-any applicable health check processes.
-
-<Tabs groupId="refresh-stage">
-<TabItem value="ui" label="Using the UI" default>
-
-1. Select the `Stage` you want to refresh.
-
-1. Click <Hlt>Refresh</Hlt> in the top-right corner of the pop-up window:
-
-   ![refresh-stage](../static/img/refresh-stage.png)
-
-</TabItem>
-
-<TabItem value="cli" label="Using the CLI">
-
-To refresh a `Stage`, run:
-
-```shell
-kargo refresh stage <stage> --project <project>
-```
-
-</TabItem>
-</Tabs>
-
-### Reverifying a Stage's Current Freight
-
-Verification processes, which run automatically following each successful Promotion,
-can also be re-run on-demand. This is useful for re-attempting a failed verification
-process or just to validate that applications within the `Stage` are performing
-as desired.
-
-<Tabs groupId="verify-stage">
-<TabItem value="ui" label="Using the UI" default>
-
-1. Select the `Stage` you want to reverify and click <Hlt>Reverify</Hlt> at the top of the menu:
-
-    ![verify-stage](../static/img/reverify-freight.png)
-
-    :::note
-    If you wish to stop the in-progress verification, you can click <Hlt>Abort Verification</Hlt>.
-    :::
-
-1. To check the `Stage`s where the `Freight` has been successfully verified, return to 
-    the <Hlt>Freight Timeline</Hlt> and select the `Freight`. Verified `Stage` names will appear under <Hlt>VERIFIED IN</Hlt>:
-
-    ![verify-stage](../static/img/verified-in.png)
-
-</TabItem>
-
-<TabItem value="cli" label="Using the CLI">
-
-1. To rerun verification using the CLI run:
-
-    ```shell
-    kargo verify stage <stage> --project <project> 
-    ```
-
-    If you want to *stop* this ongoing verification process, use:
-
-    ```shell
-    kargo verify stage <stage> --project <project> --abort
-    ```
-1. To check the `Stage`s where the `Freight` has been successfully verified, run:
-
-    ```shell
-    kargo get freight \
-    --project <project> \
-    --output jsonpath-as-json={.status}
-    ```
-
-</TabItem>
-</Tabs>
diff --git a/docs/old-docs/30-how-to-guides/15-working-with-freight.md b/docs/old-docs/30-how-to-guides/15-working-with-freight.md
deleted file mode 100644
index ac0c86a3..00000000
--- a/docs/old-docs/30-how-to-guides/15-working-with-freight.md
+++ /dev/null
@@ -1,209 +0,0 @@
----
-description: Learn how to work effectively with freight
-sidebar_label: Working with Freight
----
-
-# Working With Freight
-
-**Freight** is an important Kargo concept. A single "piece of freight" is a set
-of references to one or more versioned artifacts, which may include one or more:
-
-* Container images (from image repositories)
-
-* Kubernetes manifests (from Git repositories)
-
-* Helm charts (from chart repositories)
-
-Freight can therefore be thought of as a sort of meta-artifact. Freight is what
-Kargo seeks to progress from one stage to another.
-
-:::info
-To learn the fundamentals of freight and the warehouses that produce freight,
-visit the [concepts doc](../concepts).
-:::
-
-The remainder of this page describes features of freight that will enable you
-to work more effectively.
-
-## Names
-
-Like all Kubernetes resources, Kargo `Freight` resources have a `metadata.name`
-field, which uniquely identifies each resource of that type within a given Kargo
-project (a specially labeled Kubernetes namespace). When a `Warehouse` produces
-a new `Freight` resource, it will compute a canonical representation of the
-artifacts referenced by that resource and use that, in turn, to compute a SHA-1
-hash. This becomes the value of the `metadata.name` field. The deterministic
-method of computing this value makes it a unique "fingerprint" of the
-collection of artifacts referenced by the `Freight` resource.
-
-## Aliases
-
-While the `metadata.name` field contains a predictably computed SHA-1 hash,
-such identifiers are, unarguably, not very user-friendly.
-To make `Freight` resources easier for human users to identify, `Warehouse`s
-automatically generate a human-friendly alias for every `Freight` resource they
-produce and apply it as the value of the `Freight` resource's
-`kargo.akuity.io/alias`
-[label](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/).
-
-:::info
-Generating aliases of the form `<adjective>-<animal>` is a strategy borrowed
-from Docker, which generates similar names for containers not explicitly named
-by users.
-:::
-
-:::info
-Why a label?
-
-Kubernetes enforces the immutability of the `metadata.name` field for all
-resources.
-
-Kubernetes
-[labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/),
-by contrast, are both mutable and indexed, which makes them ideal for use as
-secondary identifiers.
-:::
-
-When using the Kargo CLI to query for `Freight` resources, the `alias` field is
-always displayed:
-
-```shell
-kargo get freight --project kargo-demo
-```
-
-Sample output:
-
-```shell
-NAME                                       ALIAS              AGE
-f5f87aa23c9e97f43eb83dd63768ee41f5ba3766   mortal-dragonfly   35s
-```
-
-It is also displayed when using `kubectl` to query for `Freight` resources:
-
-```shell
-kubectl get freight --namespace kargo-demo
-```
-
-Sample output:
-
-```shell
-NAME                                       ALIAS              AGE
-f5f87aa23c9e97f43eb83dd63768ee41f5ba3766   mortal-dragonfly   35s
-```
-
-:::info
-The Kargo UI, to make efficient use of screen real estate, displays aliases
-only, but a `Freight` resource's `name` can always be discovered by hovering
-over its alias.
-:::
-
-:::note
-Kargo CLI commands will accept `Freight` aliases as an alternative to a
-`Freight` name. Refer to the help text for the `kargo` command for more
-information.
-:::
-
-### Updating Aliases
-
-While every `Freight` resource is automatically assigned an alias, users may
-sometimes wish to override that alias with one of their own choosing. This can
-make it easier to identify a particularly important (or problematic) `Freight`
-resource as it progresses through the `Stage`s of a pipeline.
-
-This is conveniently accomplished via the Kargo CLI:
-
-```shell
-kargo update freight \
-  --project=kargo-demo \
-  --name=f5f87aa23c9e97f43eb83dd63768ee41f5ba3766 \
-  --new-alias=frozen-tauntaun
-```
-Alternatively, you can reference the `Freight` to which you want to assign a new alias using its existing alias:
-
-```shell
-kargo update freight \
-  --project=kargo-demo \
-  --old-alias=mortal-dragonfly \
-  --new-alias=frozen-tauntaun
-```
-
-This can also be accomplished via `kubectl` commands `apply`, `edit`, `patch`,
-etc. by updating the `alias` field of the `Freight` resource.
-
-:::info
-The `alias` field is a convenient way to update the `Freight` resource's
-`kargo.akuity.io/alias` label, which causes a webhook to sync the field value
-to the label value. The precedence rules for syncing between the field and
-label values are as follows:
-
-- If the field has a non-empty value, the label will assume the field's value.
-- If the field has an empty value, the field will assume the label's value.
-
-It's worth noting that removing an alias entirely requires clearing both the
-field and label values, but this is expected to be a rare occurrence.
-:::
-
-## Manual Approvals
-
-The [concepts doc](../concepts#verifications) describes the
-usual process by which `Freight` resources are _verified_ at each `Stage` in a
-pipeline before becoming available to the next `Stage` or `Stage`s. In brief, it
-typically requires the `Stage` to reach a healthy state _and_, if applicable,
-any user-defined verification processes to complete with favorable results.
-
-This is suitable for the average case wherein a new `Freight` resource is
-expected to traverse the entirety of a pipeline on its way to production,
-however, it is nearly inevitable that the occasional need for a "hotfix" will
-arise, in which case it may sometimes be desirable to bypass one or more
-`Stage`s in the pipeline.
-
-To enable this, Kargo provides the ability to manually approve a `Freight`
-resource for promotion to any given `Stage`. This is conveniently accomplished
-via the Kargo CLI:
-
-```shell
-kargo approve \
-  --freight f5f87aa23c9e97f43eb83dd63768ee41f5ba3766 \
-  --stage prod \
-  --project kargo-demo
-```
-
-:::note
-Manual approvals cannot be granted via `kubectl` due to technical factors
-preventing `kubectl` from updating `status` subresources of Kargo resources.
-:::
-
-:::note
-Manually granting approval for a `Freight` resource to be promoted to any given
-`Stage` requires the same level of permissions as would be required to carry out
-that promotion, although, granting manual approval does _not_ automatically
-create a corresponding `Promotion` resource.
-:::
-
-After successfully granting manual approval for a `Freight` resource to be
-promoted to a given `Stage`, the `Freight` resource's `status` field will
-reflect that approval.
-
-The following depicts a `Freight` resource that has been verified in a `test`
-`Stage` through the usual process, but has been manually approved for promotion
-to the `prod` `Stage`. i.e. Any `Stage`s between `test` and `prod` may be
-bypassed.
-
-```shell
-kargo get freight \
-  --project kargo-demo \
-  --output jsonpath-as-json={.status}
-```
-
-```shell
-[
-    {
-        "approvedFor": {
-            "prod": {}
-        },
-        "verifiedIn": {
-            "test": {}
-        }
-    }
-]
-```
diff --git a/docs/old-docs/30-how-to-guides/20-managing-credentials.md b/docs/old-docs/30-how-to-guides/20-managing-credentials.md
deleted file mode 100644
index 37b86491..00000000
--- a/docs/old-docs/30-how-to-guides/20-managing-credentials.md
+++ /dev/null
@@ -1,630 +0,0 @@
----
-description: Find out how to manage repository credentials for use by Kargo
-sidebar_label: Managing Credentials
----
-
-# Managing Credentials
-
-To manage the progression of Freight from Stage to Stage, Kargo will often
-require read/write permissions on private GitOps repositories and read-only
-permissions on private container image and/or Helm chart repositories.
-
-This section presents an overview of how these credentials can be managed.
-
-## Credentials as Kubernetes `Secret` Resources
-
-:::caution
-Kargo formerly borrowed its general credential-management approach from Argo CD,
-but has since diverged.
-:::
-
-Kargo expects any credentials it requires to have been stored as specially
-labeled Kubernetes `Secret` resources containing specially-formatted data. These
-`Secret`s take the following form:
-
-```yaml
-apiVersion: v1
-kind: Secret
-metadata:
-  name: <name>
-  namespace: <project namespace>
-  labels:
-    kargo.akuity.io/cred-type: image
-stringData:
-  repoURL: <repo url>
-  username: <username>
-  password: <password>
-```
-
-The `name` of such a `Secret` is inconsequential and may follow any convention
-preferred by the user.
-
-:::info
-Kargo uses Kubernetes `Namespace`s to mark project boundaries. `Secret`s
-representing credentials will typically exist in the same `Namespace` as the
-`Stage` resources that will require them. There are exceptions to this, which
-are covered in the next section.
-:::
-
-The label key `kargo.akuity.io/cred-type` and its value, one of `git`, `helm`,
-or `image`, is important, as it designates the `Secret` as representing
-credentials for a Git repository, a Helm chart repository, or a container image
-repository, respectively.
-
-The `Secret`'s `data` field (set above using plaintext in the `stringData`
-field), MUST contain the following keys:
-
-* `repoURL`: The full URL of the repository the credentials are for.
-
-* Either:
-
-  * `username`: The username to use when authenticating to the repository.
-
-  * `password`: A password or personal access token.
-
-    :::info
-    If the value of the `password` key is a personal access token, the value of
-    the `username` field may be inconsequential. You should consult your
-    repository's documentation for more information.
-    :::
-
-  OR:
-
-  * `sshPrivateKey`: A PEM-encoded SSH private key. Applicable to Git
-    repositories only.
-    
-
-Optionally, the following keys may also be included:
-
-* `repoURLIsRegex`: Set this to `true` if the value of the `repoURL` key
-  is a regular expression. Any other value of this key or the absence of this
-  key is interpreted as `false`.
-
-:::note
-When Kargo searches for repository credentials in a project `Namespace`, it
-_first_ checks all appropriately labeled `Secret`s for a `repoURL` value
-matching the repository URL exactly. Only if no `Secret` is an exact match does
-it check all appropriately labeled `Secret`s for a `repoURL` value containing a
-regular expression matching the repository URL.
-
-When searching for an exact match, and again when searching for a pattern match,
-appropriately labeled `Secret`s are considered in lexical order by name.
-:::
-
-:::caution
-Only username/password (or personal access token) authentication is
-supported at this time. Others are likely to be added in the future.
-:::
-
-## Global Credentials
-
-In cases where one or more sets of credentials are needed widely across _all_
-Kargo projects, the administrator/operator installing Kargo may opt-in to
-designating one or more `Namespace`s as homes for "global" credentials using the
-`controller.globalCredentials.namespaces` setting in Kargo's Helm chart.
-Refer to
-[the advanced section of the installation guide](./10-installing-kargo.md#advanced-installation)
-for more details.
-
-:::note
-Operators must manually ensure Kargo controllers receive read-only access
-to `Secret`s in the designated namespaces. For example, if `kargo-global-creds`
-is designated as a global credentials namespace, the following `RoleBinding`
-should be created within that `Namespace`:
-
-```yaml
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-    name: kargo-controller-read-secrets
-    namespace: kargo-global-creds
-subjects:
-    - kind: ServiceAccount
-      name: kargo-controller
-      namespace: kargo
-roleRef:
-    kind: ClusterRole
-    name: kargo-controller-read-secrets
-    apiGroup: rbac.authorization.k8s.io
-```
-:::
-
-:::info
-By default, Kargo controllers lack cluster-wide permissions on `Secret`
-resources. Instead, the Kargo _management controller_ dynamically expands
-controller access to `Secret`s on a namespace-by-namespace basis as new
-`Project`s are created.
-
-_It is because this process does not account for "global" credential namespaces
-that these bindings must be created manually by an operator._
-:::
-
-:::warning
-Setting `controller.serviceAccount.clusterWideSecretReadingEnabled` setting to
-`true` during Kargo installation will grant Kargo controllers cluster-wide read
-permission on `Secret` resources.
-
-__This is highly discouraged, especially in sharded environments where this
-permission would have the undesirable effect of granting remote Kargo
-controllers read permissions on all `Secret`s throughout the Kargo control
-plane's cluster -- including `Secret`s having nothing to do with Kargo.__
-:::
-
-:::note
-Any matching credentials (exact match _or_ pattern match) found in a project's
-own `Namespace` take precedence over those found in any global credentials
-`Namespace`.
-
-When Kargo searches for repository credentials in global credentials
-`Namespace`s, it _first_ checks all appropriately labeled `Secret`s for a
-`repoURL` value matching the repository URL exactly. Only if no `Secret` is an
-exact match does it check all appropriately labeled `Secret`s for a
-`repoURL` value containing a regular expression matching the repository URL.
-
-When searching for an exact match, and again when searching for a pattern match,
-appropriately labeled `Secret`s are considered in lexical order by name.
-
-When Kargo is configured with multiple global credentials `Namespace`s, they are
-searched in lexical order by name. Only after no exact match _and_ no pattern
-match is found in one global credentials `Namespace` does Kargo search the next.
-:::
-
-## Managing Credentials with the CLI
-
-The Kargo CLI can be used to manage credentials in a project's `Namespace.`
-
-The following example creates credentials for a Git repository:
-
-```shell
-kargo create credentials --project kargo-demo my-credentials \
-  --git --repo-url https://github.com/example/kargo-demo.git \
-  --username my-username --password my-my-personal-access-token
-```
-
-```shell
-secret/my-credentials created
-```
-
-:::caution
-If you do not wish for your password or personal access token to be stored
-in your shell history, you may wish to omit the `--password` flag, in which
-case the CLI will prompt you to enter the password interactively.
-:::
-
-Credentials can be listed or viewed with `kargo get credentials`:
-
-```shell
-kargo get credentials --project kargo-demo my-credentials
-```
-
-```shell
-NAME             TYPE   REGEX   REPO                                        AGE
-my-credentials   git    false   https://github.com/example/kargo-demo.git   8m25s
-```
-
-If requesting output as YAML or JSON, passwords and other potentially sensitive
-information will be redacted.
-
-```shell
-kargo get credentials --project kargo-demo my-credentials -o yaml
-```
-
-```shell
-apiVersion: v1
-kind: Secret
-metadata:
-  creationTimestamp: "2024-05-30T20:02:46Z"
-  labels:
-    kargo.akuity.io/cred-type: git
-  name: my-credentials
-  namespace: kargo-demo
-  resourceVersion: "17614"
-  uid: ca2660e4-867d-4709-b1a7-57fbb93fc6dc
-stringData:
-  password: '*** REDACTED ***'
-  repoURL: https://github.com/example/kargo-demo.git
-  username: my-username
-type: Opaque
-```
-
-Credentials can be updated using the `kargo update credentials` command and
-the flags corresponding to attributes of the credential that you wish to modify.
-Other attributes of the credentials will remain unchanged.
-
-The following example updates `my-credentials` with a regular expression for the
-repository URL:
-
-```shell
-kargo update credentials --project kargo-demo my-credentials \
-  --repo-url '^https://github.com/' --regex
-```
-
-```shell
-secret/my-credentials updated
-```
-
-And credentials can, of course, be deleted with `kargo delete credentials`:
-
-```shell
-kargo delete credentials --project kargo-demo my-credentials
-```
-
-```shell
-secret/my-credentials deleted
-```
-
-:::note
-While the CLI may be a fine way of managing project-level credentials whilst
-getting to know Kargo, it is unquestionably more secure to use other means to
-ensure the existence of these specially-formatted `Secret`s in the appropriate
-project `Namespace`s.
-:::
-
-## Git Provider-Specific Authentication Options
-
-This section provides Git provider-specific guidance on credential management.
-
-### GitHub
-
-#### Personal Access Token
-
-GitHub supports authentication using a
-[personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens),
-which can be used in place of a password. The corresponding username must be
-the GitHub handle of the user who created the token. These can be stored in
-the `username` and `password` fields of a `Secret` resource as described
-[in the first section](#credentials-as-kubernetes-secret-resources) of this
-document.
-
-:::info
-This method of authentication may be best when wishing to rigorously enforce
-the principle of least privilege, as personal access tokens can be scoped to
-specific permissions on specific repositories.
-
-A drawback to this method, however, is that the token is owned by a specific
-GitHub user, and if that user should lose their own access to the repositories
-in question, Kargo will also lose access.
-:::
-
-#### GitHub App Authentication
-
-[GitHub Apps](https://docs.github.com/en/apps) can be used
-[as an authentication method](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app).
-
-1. [Create a GitHub App](https://github.com/settings/apps/new):
-
-    1. In the <Hlt>GitHub App name</Hlt> field, specify a unique
-       name.
-    1. Set the <Hlt>Homepage URL</Hlt> to any URL you like.
-    1. Under <Hlt>Webhook</Hlt>, de-select
-       <Hlt>Active</Hlt>.
-    1. Under <Hlt>Permissions</Hlt> → <Hlt>Repository
-       permissions</Hlt> → <Hlt>Contents</Hlt>, select whether
-       the App will require <Hlt>Read-only</Hlt> or <Hlt>Read
-       and write</Hlt> permissions. _The App will receive these
-       permissions on all repositories into which it is installed._
-    1. Under <Hlt>Where can this GitHub App be installed?</Hlt>,
-       leave <Hlt>Only on this account</Hlt> selected.
-    1. Click <Hlt>Create GitHub App</Hlt>.
-    1. Take note of the <Hlt>App ID</Hlt>.
-    1. Scroll to the bottom of the page and click <Hlt>Generate a private
-       key</Hlt>. The resulting key will be downloaded immediately. Store
-       it securely.
-    1. On the left-hand side of the page, click <Hlt>Install
-       App</Hlt>.
-    1. Choose an account to install the App into by clicking
-       <Hlt>Install</Hlt>.
-    1. Select <Hlt>Only select repositories</Hlt> and choose the
-       repositories you wish to grant the App access to. Remember that the App
-        will receive the permissions you selected earlier on _all_ of these
-        repositories.
-    1. Click <Hlt>Install</Hlt>.
-    1. In your browser's address bar, take note of the numeric identifier at the
-       end of the current page's URL. This is the <Hlt>Installation
-       ID</Hlt>.
-
-2. Create a `Secret` resource with the following structure:
-
-    ```yaml
-    apiVersion: v1
-    kind: Secret
-    metadata:
-      name: <name>
-      namespace: <project namespace>
-      labels:
-        kargo.akuity.io/cred-type: git
-    stringData:
-      githubAppID: <app id>
-      githubAppPrivateKey: <PEM-encoded private key>
-      githubAppInstallationID: <installation id>
-      repoURL: <repo url>
-      repoURLIsRegex: <true if repoURL is a pattern matching multiple repositories>
-    ```
-
-:::info
-Compared to personal access tokens, a benefit of authenticating with a GitHub
-App is that the App's permissions are not tied to a specific GitHub user.
-:::
-
-:::caution
-It is all too easy to violate the principle of least privilege when
-authenticating using this method.
-
-For convenience sake, it may be tempting to register a single GitHub App and
-select a broad set of repositories when installing that App into your
-organization. It may also be tempting to create a single set of
-[global credentials](#global-credentials) such that all Kargo projects can use
-them to access their repositories, _however_, this will have the undesirable
-effect of granting _all_ Kargo projects access to _all_ of the repositories
-selected when the App was installed.
-
-It is, instead, recommended to register a separate GitHub App for
-each Kargo project. When installing each App into your organization, only those
-repositories to which each Kargo project requires access should be selected.
-
-GitHub organizations are limited to registering 100 GitHub Apps, however, so
-this approach may not be feasible for organizations with many Kargo projects.
-:::
-
-:::caution
-A second way in which authentication using GitHub Apps may violate the principle
-of least privilege involves the fact that the same permissions are granted to
-the App on _all_ repositories that are selected when it is installed.
-
-If a Kargo project requires read-only access to one repository and read/write
-access to another, it is not possible to grant the App different permissions on
-the two. This may then lead to granting broader permissions than are strictly
-necessary.
-:::
-
-## Image Registry-Specific Authentication Options
-
-While many container image registries support authentication using long-lived
-credentials, such as a username and password (or personal access token), some
-either require or offer more secure options.
-
-This section provides registry-specific guidance on credential management and
-also covers options for gaining image repository access using workload identity
-on applicable platforms.
-
-### Amazon Elastic Container Registry (ECR)
-
-The authentication options described in this section are applicable only to
-container image repositories whose URLs indicate they are hosted in ECR.
-
-#### Long-Lived Credentials
-
-Elastic Container Registries do not _directly_ support long-lived credentials,
-however, an AWS access key ID and secret access key
-[can be used to obtain an authorization token](https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html#registry-auth-token)
-that is valid for 12 hours. Kargo can seamlessly obtain such a token and will
-cache it for a period of 10 hours.
-
-To use this option, your `Secret` should take the following form:
-
-```yaml
-apiVersion: v1
-kind: Secret
-metadata:
-  name: <name>
-  namespace: <project namespace>
-  labels:
-    kargo.akuity.io/cred-type: image
-stringData:
-  awsAccessKeyID: <access key id>
-  awsSecretAccessKey: <secret access key>
-  repoURL: <ecr url>
-```
-
-:::caution
-Following the principle of least privilege, the IAM user associated with the
-access key ID and secret access key should be limited only to read-only access
-to the required ECR repositories.
-:::
-
-:::caution
-This method of authentication is a "lowest common denominator" approach that
-will work regardless of where Kargo is deployed. i.e. if running Kargo outside EKS, this method will still work.
-
-If running Kargo within EKS, you may wish to either consider using EKS Pod Identity or IRSA
-instead.
-:::
-
-#### EKS Pod Identity or IAM Roles for Service Accounts (IRSA)
-
-If Kargo locates no `Secret` resources matching a repository URL and is deployed
-within an EKS cluster, it will attempt to use
-[EKS Pod Identity](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html)
-or
-[IAM Roles for Service Accounts (IRSA)](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
-to authenticate. Both of these rely upon some external setup. Leveraging either
-eliminates the need to store ECR credentials in a `Secret` resource.
-
-Follow
-[this overview](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html#pod-id-setup-overview)
-to set up EKS Pod Identity in your EKS cluster or
-[this one](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
-to set up IRSA. For either, you will assign an IAM role to the
-`kargo-controller` `ServiceAccount` within the `Namespace` to which Kargo is (or
-will be) installed.
-
-:::note
-To use IRSA, you will additionally need to specify the
-[ARN](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) of
-the controller's IAM role as the value of the
-`controller.serviceAccount.iamRole` setting in Kargo's Helm chart. Refer to
-[the advanced section of the installation guide](./10-installing-kargo.md#advanced-installation)
-for more details.
-:::
-
-
-At this point, an IAM role will be associated with the Kargo _controller_,
-however, that controller acts on behalf of multiple Kargo projects, each of
-which may require access to _different_ ECR repositories. To account for this,
-when Kargo attempts to access an ECR repository on behalf of a specific project,
-it will first attempt to
-[assume an IAM role](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
-specific to that project. The name of the role it attempts to assume will
-_always_ be of the form `kargo-project-<project name>`. It is this role that
-should be granted read-only access to applicable ECR repositories.
-
-:::info
-The name of the IAM role associated with each Kargo project is deliberately
-not configurable to prevent project admins from attempting to coerce Kargo into
-assuming arbitrary IAM roles.
-:::
-
-:::caution
-For optimal adherence to the principle of least permissions, the IAM role
-associated with the `kargo-controller` `ServiceAccount` should be limited only
-to the ability to assume project-specific IAM roles. Project-specific IAM roles
-should be limited only to read-only access to applicable ECR repositories.
-:::
-
-:::info
-If the Kargo controller is unable to assume a project-specific IAM role, it will
-fall back to using its own IAM role directly. For organizations without strict
-tenancy requirements, this can eliminate the need to manage a large number of
-project-specific IAM roles. While useful, this approach is not strictly
-recommended.
-:::
-
-Once Kargo is able to gain necessary permissions to access an ECR repository,
-it will follow a process similar to that described in the previous section to
-obtain a token that is valid for 12 hours and cached for 10.
-
-### Google Artifact Registry
-
-The authentication options described in this section are applicable only to
-container image repositories whose URLs indicate they are hosted in Google
-Artifact Registry.
-
-:::note
-Google Container Registry (GCR) has been deprecated in favor of Google Artifact
-Registry. For authentication to repositories with legacy GCR URLs, the same
-options outlined here may be applied.
-:::
-
-#### Long-Lived Credentials
-
-:::caution
-Google Artifact Registry does _directly_ support long-lived credentials
-[as described here](https://cloud.google.com/artifact-registry/docs/docker/authentication#json-key).
-The username `_json_key_base64` and the base64-encoded service account key
-may be stored in the `username` and `password` fields of a `Secret` resource as
-described [in the first section](#credentials-as-kubernetes-secret-resources) of
-this document. Kargo and Google both strongly discourage this method of
-authentication however.
-:::
-
-Google documentation recommends
-[using a service account key to obtain an access token](https://cloud.google.com/artifact-registry/docs/docker/authentication#token)
-that is valid for 60 minutes. Compared to the discouraged method of using the
-service account key to authenticate to the registry directly, this process does
-_not_ transmit the service account key over the wire. Kargo can seamlessly carry
-out this process and will cache the access token for a period of 40 minutes.
-
-To use this option, your `Secret` should take the following form:
-
-```yaml
-apiVersion: v1
-kind: Secret
-metadata:
-  name: <name>
-  namespace: <project namespace>
-  labels:
-    kargo.akuity.io/cred-type: image
-stringData:
-  gcpServiceAccountKey: <base64-encoded service account key>
-  repoURL: <artifact registry url>
-```
-
-:::note
-Service account keys contain structured data, so it is important that the
-key be base64-encoded.
-:::
-
-:::caution
-Following the principle of least privilege, the service account associated with
-the service account key should be limited only to read-only access to the
-required Google Artifact Registry repositories.
-:::
-
-:::caution
-This method of authentication is a "lowest common denominator" approach that
-will work regardless of where Kargo is deployed. i.e. If running Kargo outside
-of GKE, this method will still work.
-
-If running Kargo within GKE, you may wish to consider using Workload Identity
-Federation instead.
-:::
-
-#### Workload Identity Federation
-
-If Kargo locates no `Secret` resources matching a repository URL, and if Kargo
-is deployed within a GKE cluster, it will attempt to use
-[Workload Identity Federation](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)
-to authenticate, but this relies upon some external setup. Leveraging this
-option eliminates the need to store credentials in a `Secret` resource.
-
-First, follow
-[these directions](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#enable_on_cluster)
-to provision a new GKE cluster with Workload Identity Federation enabled or
-[these directions](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#enable-existing-cluster)
-to enable Workload Identity Federation on an existing GKE cluster.
-
-At this point, the `kargo-controller` `ServiceAccount` within the `Namespace` to
-which Kargo is (or will be) installed will be associated with an _IAM principal
-identifier_, which takes the following form:
-
-```plaintext
-principal://iam.googleapis.com/projects/<gcp project number>/locations/global/workloadIdentityPools/<gcp project name>.svc.id.goog/subject/ns/<kargo namespace>/sa/kargo-controller
-```
-
-Although associated with this _one_ principal, the Kargo controller acts on
-behalf of multiple Kargo projects, each of which may require access to
-_different_ Google Artifact Registry repositories. To account for this, when
-Kargo attempts to access a Google Artifact Registry repository on behalf of a
-specific project, it will first attempt to
-[impersonate a Google service account](https://cloud.google.com/iam/docs/service-account-impersonation) 
-specific to that project. The name of the service account it attempts to
-impersonate will _always_ be of the form
-`kargo-project-<kargo project name>@<gcp project name>.iam.gserviceaccount.com`.
-It is this service account that should be granted read-only access to applicable
-Google Artifact Registry repositories.
-
-:::info
-The name of the Google service account associated with each Kargo project is
-deliberately not configurable to prevent Kargo project admins from attempting to
-coerce Kargo into impersonating arbitrary Google service accounts.
-:::
-
-Once Kargo is able to impersonate the appropriate Google service account for a
-given project, it will follow a process similar to that described in the
-previous section to obtain a token that is valid for 60 minutes and cached for
-40.
-
-:::caution
-Following the principle of least privilege, the IAM principal associated with
-the `kargo-controller` `ServiceAccount` should be limited only to the ability to
-impersonate project-specific Google service accounts. Project-specific Google
-service accounts should be limited only to read-only access to the applicable
-Google Artifact Registry repositories.
-:::
-
-### Azure Container Registry (ACR)
-
-Azure Container Registry directly supports long-lived credentials.
-
-It is possible to
-[create tokens with repository-scoped permissions](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-repository-scoped-permissions),
-with or without an expiration date. These tokens can be stored in the
-`username` and `password` fields of a `Secret` resource as described
-[in the first section](#credentials-as-kubernetes-secret-resources) of this
-document.
-
-:::info
-Support for authentication to ACR repositories using workload identity, on par
-with Kargo's support for ECR and Google Artifact Registry, is likely to be
-included in a future release of Kargo.
-:::
diff --git a/docs/old-docs/30-how-to-guides/25-argo-cd-integration.md b/docs/old-docs/30-how-to-guides/25-argo-cd-integration.md
deleted file mode 100644
index a360b8ef..00000000
--- a/docs/old-docs/30-how-to-guides/25-argo-cd-integration.md
+++ /dev/null
@@ -1,66 +0,0 @@
----
-description: Learn how to integrate Kargo Stages with Argo CD Applications.
-sidebar_label: Argo CD Integration
----
-
-# Argo CD Integration
-
-Argo CD excels at syncing Kubernetes clusters to desired state stored in
-a Git repository, but lacks any concept of a "promotion", i.e. updating
-the desired state of some application instance to reflect the validated
-state of some other application instance. Kargo was created to orchestrate
-such processes. Because such orchestration naturally entails some direct
-and indirect integration with Argo CD, this page details certain key
-elements of the interactions between the two systems.
-
-:::note
-This page is a work in progress. Thank you for your patience as we work to add more details.
-:::
-
-## Updating Argo CD Applications
-
-In the course of orchestrating the transition of an application instance
-from one state to another, it is common for Kargo to updated Argo CD
-`Application` resources in some way. Such updates are enabled through the
-use of the
-[`argocd-update` promotion step](../35-references/10-promotion-steps.md#argocd-update).
-Often, these updates entail little more than modifying an `Application`'s 
-`operation` field to force the `Application` to be synced to recently
-updated desired state.
-
-### Authorizing Updates
-
-Performing updates of any kind to an `Application` resource naturally
-requires Kargo to be _authorized_ to do so. Kargo controllers have the
-requisite RBAC permissions to perform such updates, but being a
-multi-tenant system, Kargo must also understand, internally, when it
-is acceptable to utilize those broad permissions to update a specific 
-`Application` resource _on behalf of_ a given `Stage`.
-
-To enable Kargo controllers to update an Argo CD `Application` on behalf of
-a given `Stage`, that `Application` must be explicitly annotated as follows:
-
-```yaml
-kargo.akuity.io/authorized-stage: "<project-name>:<stage-name>"
-```
-
-Because an annotation such as the one above could only be added to
-an `Application` by a user who, themselves, is authorized to update
-that `Application`, Kargo interprets the presence of such an annotation
-as delegation of that user's authority to do so.
-
-In the following example, an `Application` has been annotated to
-authorize Kargo to update it on behalf of a `Stage` named `test`
-in the `kargo-demo` `Project`:
-
-```yaml
-apiVersion: argoproj.io/v1alpha1
-kind: Application
-metadata:
-  name: kargo-demo-test
-  namespace: argocd
-  annotations:
-    kargo.akuity.io/authorized-stage: kargo-demo:test
-spec:
-  # Application Specifications
-```
diff --git a/docs/old-docs/30-how-to-guides/30-managing-user-permissions.md b/docs/old-docs/30-how-to-guides/30-managing-user-permissions.md
deleted file mode 100644
index ff57fa38..00000000
--- a/docs/old-docs/30-how-to-guides/30-managing-user-permissions.md
+++ /dev/null
@@ -1,308 +0,0 @@
----
-description: Learn how to manage user permissions
-sidebar_label: Managing User Permissions
----
-
-# Managing User Permissions
-
-Kargo is typically configured to support single-sign-on (SSO) using an external
-identity provider that implements the
-[OpenID Connect](https://openid.net/developers/how-connect-works/) (OIDC)
- protocol.
-
-:::info
-Refer to
-[the advanced section of the installation guide](./10-installing-kargo.md#advanced-installation)
-for more details on how to configure Kargo to use an external identity provider.
-:::
-
-Kargo also implements authorization of all user actions using pure Kubernetes
-[RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/). i.e.
-Permission to perform various actions on various Kargo resources is therefore
-granted via `RoleBinding` resources that associate users or `ServiceAccount`
-resources with `Role` resources.
-
-Because Kargo users log into the Kargo CLI or UI via SSO, their identifies are
-not known to Kargo's underlying Kubernetes cluster. This represents an
-impediment to using Kubernetes RBAC to authorize the actions of such users.
-
-Kargo answers this challenge through a scheme that permits users to be mapped
-to zero or more Kubernetes `ServiceAccount` resources. The remainder of this
-page describes how to create those mappings.
-
-## User to `ServiceAccount` Mappings
-
-Whether logged into the Kargo CLI or UI, Kargo users are interacting with Kargo
-via the Kargo API server. The Kargo API server authenticates users via a bearer
-token issued by the external identity provider. On every request, the Kargo
-API server validates and decodes the token to obtain trusted information about
-the user which, importantly, includes _claims_ such as username, email address,
-and group membership. The exact claims available depend on the identity provider
-and the configuration of the Kargo API server. (Refer again to
-[the advanced section of the installation guide](./10-installing-kargo.md#advanced-installation).)
-
-Also at the time of authentication, the Kargo API server queries the Kubernetes
-API server to obtain a list of all `ServiceAccount` resources to which the user
-has been mapped. Kargo typically restricts this search to `ServiceAccount` resources
-in Kargo project namespaces only (i.e. only those labeled with 
-`kargo.akuity.io/project: "true"`). Refer to the next section for exceptions to
-this rule.
-
-ServiceAccount resources may be mapped to users through the use of annotations
-whose key begins with `rbac.kargo.akuity.io/claim.`. The value of the annotation
-may be a single value, or a comma-delimited list of values.
-
-In the following example, the `ServiceAccount` resource is mapped to all of:
-
-* Users identified as `alice` or `bob`.
-* A user with the email address `carl@example.com`.
-* All users in _either_ the `devops` or `kargo-admin` group.
-
-```yaml
-apiVersion: v1
-kind: ServiceAccount
-metadata:
-  name: admin
-  namespace: kargo-demo
-  annotations:
-    rbac.kargo.akuity.io/claim.sub: alice,bob
-    rbac.kargo.akuity.io/claim.email: carl@example.com
-    rbac.kargo.akuity.io/claim.groups: devops,kargo-admin
-```
-
-A user may be mapped to multiple `ServiceAccount` resources. A user's effective
-permissions are therefore the union of the permissions associated with all such
-`ServiceAccount` resources.
-
-## Managing Project-Level "Kargo Roles" with the CLI
-
-The Kargo CLI offers several conveniences for working with "Kargo Roles," which
-are simplified abstractions over Kubernetes `ServiceAccount`, `Role`, and
-`RoleBinding` resources.
-
-A Kargo Role combines the "who" of a `ServiceAccount` (with the mapping
-annotations described in the previous section) with the "what" of a `Role`
-resource's policy rules.
-
-Creating a Kargo Role, therefore, effects creation of an underlying
-`ServiceAccount`/`Role`/`RoleBinding` trio. Similarly, deleting a Kargo Role
-deletes those same underlying resources. Updating a Kargo Role via `grant` or
-`revoke` commands updates the underlying `ServiceAccount` or `RoleBinding`
-accordingly.
-
-:::info
-Before diving into the commands for Kargo Role management, please note the
-following details:
-
-* A Kargo Role _exists_ as long as an underlying `ServiceAccount` resource with
-  the same name exists in the Project namespace.
-* If any `RoleBinding` resources in the Project namespace reference the
-  `ServiceAccount` resource, then all of the `Role` resources referenced by
-  those `RoleBinding` resources are also considered part of the Kargo Role, as
-  are the `RoleBinding` resources themselves.
-* Kargo can only _manage_ Kargo Roles that are:
-    * Comprised of precisely one `RoleBinding` and `Role` resource.
-    * Explicitly annotated as _being_ Kargo-managed.
-* Kargo also normalizes the representation of policy rules in any `Role`
-  resource it manages, which is necessary to ensure that `grant` and `revoke`
-  operations can modify policy rules accurately, without unintended side
-  effects.
-
-In practice, if you GitOps your Project-level `ServiceAccount`, `Role`, and
-`RoleBinding` resources, those resources should _not_ be annotated as being
-Kargo-managed. This will prevent modification of those resources through
-imperative Kargo CLI commands like `grant` and `revoke`.
-:::
-
-All Kargo Roles associated with a Project are listed using the `kargo get roles`
-command:
-
-```shell
-kargo get roles --project kargo-demo 
-```
-
-```shell
-NAME      KARGO MANAGED   AGE
-default   false           20m
-```
-
-Here we see that Kargo counts a Kargo Role as existing due to the existence of
-the `default` `ServiceAccount` resource that Kubernetes automatically creates in
-any new namespace, including Kargo Project namespaces. Because the `default`
-`ServiceAccount` lacks the necessary annotations, Kargo does not consider it
-Kargo-managed.
-
-We can create a new Kargo-manged Kargo role with the `kargo create role`
-command:
-
-```shell
-kargo create role developer --project kargo-demo
-```
-
-```shell
-role.rbac.kargo.akuity.io/developer created
-```
-
-If we list the Kargo Roles again, we see the new `developer` Kargo Role, and
-that it is Kargo-managed:
-
-```shell
-kargo get roles --project kargo-demo
-```
-
-```shell
-NAME        KARGO MANAGED   AGE
-default     false           24m
-developer   true            64s
-```
-
-We can view a YAML representation of the `developer` Kargo Role and see that
-it's not very interesting yet:
-
-```shell
-kargo get role developer --project kargo-demo -o yaml
-```
-
-```shell
-apiVersion: rbac.kargo.akuity.io/v1alpha1
-kargoManaged: true
-kind: Role
-metadata:
-  creationTimestamp: "2024-05-01T13:27:20Z"
-  name: developer
-  namespace: kargo-demo
-```
-
-To make things more interesting, we can grant the `developer` Kargo Role to
-a users having the value `developer` in their `groups` claim:
-
-```shell
-kargo grant --role developer --claim groups=developer --project kargo-demo
-```
-
-```shell
-role.rbac.kargo.akuity.io/developer updated
-```
-
-And we can grant broad permissions on `Stage` resources to the `developer` Kargo
-Role:
-
-```shell
-kargo grant --role developer \
-  --verb '*' --resource-type stages \
-  --project kargo-demo
-```
-
-```shell
-role.rbac.kargo.akuity.io/developer updated
-```
-
-We can view the updated `developer` Kargo Role and see that it is now
-considerably more interesting:
-
-```shell
-kargo get role developer --project kargo-demo -o yaml
-```
-
-```yaml
-apiVersion: rbac.kargo.akuity.io/v1alpha1
-claims:
-  - name: groups
-    values:
-    - developer
-kargoManaged: true
-kind: Role
-metadata:
-  creationTimestamp: "2024-05-01T13:27:20Z"
-  name: developer
-  namespace: kargo-demo
-rules:
-- apiGroups:
-  - kargo.akuity.io
-  resources:
-  - stages
-  verbs:
-  - create
-  - delete
-  - deletecollection
-  - get
-  - list
-  - patch
-  - update
-  - watch
-```
-
-We can also revoke the `developer` Kargo Role from the `developer` group or
-revoke any permissions from the `developer` Kargo Role using the `kargo revoke`
-command, which supports all the same flags as the `kargo grant` command.
-
-Last, it may sometimes be useful to view a Kargo Role's underlying
-`ServiceAccount`, `Role`, and `RoleBinding` resources. This may be useful, for
-instance, to users who have managed Project-level permissions imperatively up to
-a point and now wish to make the transition to GitOps'ing those permissions.
-
-```shell
-kargo get role developer --as-kubernetes-resources --project kargo-demo
-```
-
-```yaml
-NAME        K8S SERVICE ACCOUNT   K8S ROLE BINDINGS   K8S ROLES   AGE
-developer   developer             developer           developer   13m
-```
-
-It is also possible to request alternative representations of the underlying
-resources:
-
-```shell
-kargo get role developer \
-  --as-kubernetes-resources -o yaml \
-  --project kargo-demo
-```
-
-:::note
-Output of the above command is not shown here due to its length.
-:::
-
-Last, it is, of course, possible to delete a Kargo Role:
-
-```shell
-kargo delete role developer --project kargo-demo
-```
-
-```shell
-role.rbac.kargo.akuity.io/developer deleted
-```
-
-## Kargo Role Matrix
-
-The table below outlines the maximum rules required based on the `kargo-admin` ClusterRole. When specifying verbs, it's recommended to apply the principle of least privilege, ensuring access is limited to what is necessary for the specific role.
-
-| **API Groups**              | **Resources**                                  | **Verbs**                                           |
-|-----------------------------|------------------------------------------------|-----------------------------------------------------|
-| `""`                        | `events`, `namespaces`, `serviceaccounts`      | `get`, `list`, `watch`                              |
-| `rbac.authorization.k8s.io` | `rolebindings`, `roles`                        | `get`, `list`, `watch`                              |
-| `kargo.akuity.io`           | `freights`, `projects`, `stages`, `warehouses` | `*`                                                 |
-| `kargo.akuity.io`           | `stages`                                       | `promote`                                           |
-| `kargo.akuity.io`           | `promotions`                                   | `create`, `delete`, `get`, `list`, `patch`, `watch` |
-| `kargo.akuity.io`           | `freights/status`                              | `patch`                                             |
-| `argoproj.io`               | `analysisruns`                                 | `delete`, `get`, `list`, `watch`                    |
-| `argoproj.io`               | `analysistemplates`                            | `*`                                                 |
-
-## Global Mappings
-
-In cases where certain, broad sets of permissions may be required by a large
-numbers of users, the administrator/operator installing Kargo may opt-in to
-designating one or more namespaces as homes for "global" `ServiceAccount`
-resources using the `api.oidc.globalServiceAccounts` setting in Kargo's Helm
-chart. Refer to
-[the advanced section of the installation guide](./10-installing-kargo.md#advanced-installation)
-for more details.
-
-Note that `ServiceAccount` resources in designated namespaces are not _truly_
-global because they are _still_ mapped to users according to the rules described
-in the previous section.
-
-Making use of this feature could be, for instance, a convenient method of
-granting read-only access to all Kargo resources in all projects to all users
-within an organization. Additional permissions may then be granted to users on a
-project-by-project basis.
diff --git a/docs/old-docs/30-how-to-guides/_category_.json b/docs/old-docs/30-how-to-guides/_category_.json
deleted file mode 100644
index 5011e45c..00000000
--- a/docs/old-docs/30-how-to-guides/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "label": "How-to Guides",
-  "link":  null
-}
diff --git a/docs/old-docs/35-references/10-promotion-steps.md b/docs/old-docs/35-references/10-promotion-steps.md
deleted file mode 100644
index 479233a0..00000000
--- a/docs/old-docs/35-references/10-promotion-steps.md
+++ /dev/null
@@ -1,1631 +0,0 @@
----
-sidebar_label: Promotion Steps Reference
-description: Learn about all of Kargo's built-in promotion steps
----
-
-# Promotion Steps Reference
-
-Kargo's promotion steps are the building blocks of a promotion process. They
-perform the actions necessary to promote a piece of Freight into a Stage.
-Promotion steps are designed to be composable, allowing users to construct
-complex promotion processes from simple, reusable components.
-
-## Defining a Promotion Step
-
-A promotion step is a YAML object with at least one key, `uses`, whose value is
-the name of the step to be executed. The step's configuration is provided in a
-subsequent key, `config`. The `config` key's value is an object whose keys are
-the configuration options for the step.
-
-```yaml
-steps:
-- uses: step-name
-  config:
-    option1: value1
-    option2: value2
-```
-
-:::info
-For a list of built-in promotion steps and configuration options, see the
-[Built-in Steps](#built-in-steps) section.
-:::
-
-### Step Aliases
-
-A step can be given an alias by providing an `as` key in the step definition.
-The value of the `as` key is the alias to be used to reference the
-[step's output](#step-outputs).
-
-```yaml
-steps:
-- uses: step-name
-  as: alias
-```
-
-### Step Variables
-
-A step can define variables that can be referenced in its configuration by
-providing a `vars` key in the step definition. The value of the `vars` key is a
-list of variables, each of which is an object with `name` and `value` keys.
-
-```yaml
-steps:
-- uses: step-name
-  vars:
-  - name: var1
-    value: value1
-  - name: var2
-    value: value2
-  config:
-    option1: ${{ vars.var1 }}
-    option2: ${{ vars.var2 }}
-```
-
-Variables defined in a step are scoped to that step and are not accessible to
-other steps. The values of variables may contain [expressions](./20-expression-language.md).
-In addition, the values of step variables may be references to the
-[outputs](#step-outputs) of other steps.
-
-```yaml
-steps:
-- uses: step1
-  as: step1
-- uses: step2
-  vars:
-  - name: var1
-    value: ${{ outputs.step1.someOutput }}
-```
-
-When a variable in a step is also defined as a global variable in the
-[Promotion Template](../30-how-to-guides/14-working-with-stages.md#promotion-templates),
-the step variable takes precedence over the global variable.
-
-### Step Outputs
-
-A promotion step may produce output that can be referenced by subsequent steps,
-allowing the output of one step to be used as input to another. The output of a
-step is defined by the step itself and is typically documented in the step's
-reference.
-
-```yaml
-steps:
-- uses: step-name
-  as: alias
-- uses: another-step
-  config:
-    input: ${{ outputs.alias.someOutput }}
-```
-
-### Step Retries
-
-When a step fails for any reason, it can be retried instead of immediately
-failing the entire `Promotion`. An _error threshold_ specifies the number of
-_consecutive_ failures required for retry attempts to be abandoned and the
-`Promotion` to fail.
-
-Independent of the error threshold, steps are also subject to a _timeout_. Any
-step that doesn't achieve its goal within that interval will cause the
-`Promotion` to fail. For steps that exhibit any kind of polling behavior, the
-timeout can cause a `Promotion` to fail with no _other_ failure having occurred.
-
-System-wide, the default error threshold is 1 and the default timeout is
-indefinite. Thus, default behavior is effectively no retries when a step fails
-for any reason and steps with any kind of polling behavior will poll
-indefinitely _as long a no other failure occurs._
-
-The implementations of individual steps can override these defaults. Users also
-may override these defaults through configuration. In the following example, the
-`git-wait-for-pr` step is configured not to fail the `Promotion` until three
-consecutive failed attempts to execute it. It is also configured to wait a
-maximum of 48 hours for the step to complete successfully (i.e. for the PR to be
-merged).
-
-```yaml
-steps:
-# ...
-- uses: wait-for-pr
-  retry:
-    errorThreshold: 3
-    timeout: 48h
-  config:
-    prNumber: ${{ outputs['open-pr'].prNumber }}
-```
-
-:::info
-This feature was introduced in Kargo v1.1.0, and is still undergoing refinements
-and improvements to better distinguish between transient and non-transient
-errors, and to provide more control over retry behavior like backoff strategies
-or time limits.
-:::
-
-### Promotion Task Step
-
-A step can be used to reference a
-[`PromotionTask` or `ClusterPromotionTask`](30-promotion-tasks.md)
-using the `task` key, whose value is an object with a `name` key that specifies
-the name of the task and optionally a `kind` key to specify if the task is a
-`ClusterPromotionTask`.
-
-```yaml
-steps:
-- task:
-    name: task-name
-    kind: ClusterPromotionTask
-```
-
-When a task is referenced, the `uses` key is not required.
-
-## Built-in Steps
-
-This section describes the promotion steps that are built directly into Kargo.
-Steps are presented roughly in the order in which they might appear in a typical
-promotion process. Similarly, configuration options for each step are laid out
-in order of their applicability to typical use cases.
-
-:::info
-Promotion steps support the use of [expr-lang] expressions in their
-configuration. Many examples in this reference document will include expressions
-to demonstrate their use. For more information on expressions, refer to our
-[Expression Language Reference](./20-expression-language.md).
-:::
-
-### `git-clone`
-
-`git-clone` is often the first step in a promotion process. It creates a
-[bare clone](https://git-scm.com/docs/git-clone#Documentation/git-clone.txt-code--barecode)
-of a remote Git repository, then checks out one or more branches, tags, or
-commits to working trees at specified paths. Checking out different revisions to
-different paths is useful for the common scenarios of combining content from
-multiple sources or rendering Stage-specific manifests to a Stage-specific
-branch.
-
-:::note
-It is a noteworthy limitation of Git that one branch cannot be checked out in
-multiple working trees.
-:::
-
-#### `git-clone` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `repoURL` | `string` | Y | The URL of a remote Git repository to clone. |
-| `insecureSkipTLSVerify` | `boolean` | N | Whether to bypass TLS certificate verification when cloning (and for all subsequent operations involving this clone). Setting this to `true` is highly discouraged in production. |
-| `checkout` | `[]object` | Y | The commits, branches, or tags to check out from the repository and the paths where they should be checked out. At least one must be specified. |
-| `checkout[].branch` | `string` | N | A branch to check out. Mutually exclusive with `commit` and `tag`. If none of these is specified, the default branch will be checked out. |
-| `checkout[].create` | `boolean` | N | In the event `branch` does not already exist on the remote, whether a new, empty, orphaned branch should be created. Default is `false`, but should commonly be set to `true` for Stage-specific branches, which may not exist yet at the time of a Stage's first promotion. |
-| `checkout[].commit` | `string` | N | A specific commit to check out. Mutually exclusive with `branch` and `tag`. If none of these is specified, the default branch will be checked out. |
-| `checkout[].tag` | `string` | N | A tag to check out. Mutually exclusive with `branch` and `commit`. If none of these is specified, the default branch will be checked out. |
-| `checkout[].path` | `string` | Y | The path for a working tree that will be created from the checked out revision. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-
-#### `git-clone` Examples
-
-<Tabs groupId="git-clone-examples">
-
-<TabItem value="common" label="Common Usage" default>
-
-The most common usage of this step is to check out a commit specified by the
-Freight being promoted as well as a Stage-specific branch. Subsequent steps are
-likely to perform actions that revise the contents of the Stage-specific branch
-using the commit from the Freight as input.
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-# Prepare the contents of ./out ...
-# Commit, push, etc...
-```
-
-</TabItem>
-
-<TabItem value="multiple-sources" label="Combining Multiple Sources">
-
-For this more advanced example, consider a Stage that requests Freight from two
-Warehouses, where one provides Kustomize "base" configuration, while the other
-provides a Stage-specific Kustomize overlay. Rendering the manifests intended
-for such a Stage will require combining the base and overlay configurations
-with the help of a [`copy`](#copy) step. For this case, a `git-clone` step may be
-configured similarly to the following:
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo, warehouse("base")).ID }}
-      path: ./src
-    - commit: ${{ commitFrom(vars.gitRepo, warehouse(ctx.stage + "-overlay")).ID }}
-      path: ./overlay
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: copy
-  config:
-    inPath: ./overlay/stages/${{ ctx.stage }}/kustomization.yaml
-    outPath: ./src/stages/${{ ctx.stage }}/kustomization.yaml
-- uses: kustomize-build
-  config:
-    path: ./src/stages/${{ ctx.stage }}
-    outPath: ./out
-# Commit, push, etc...
-```
-
-</TabItem>
-
-</Tabs>
-
-### `git-clear`
-
-`git-clear` deletes _the entire contents_ of a specified Git working tree
-(except for the `.git` file). It is equivalent to executing
-`git add . && git rm -rf --ignore-unmatch .`. This step is useful for the common
-scenario where the entire content of a Stage-specific branch is to be replaced
-with content from another branch or with content rendered using some
-configuration management tool.
-
-#### `git-clear` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `path` | `string` | Y | Path to a Git working tree whose entire contents are to be deleted. |
-
-#### `git-clear` Example
-
-```yaml
-steps:
-- uses: git-clone
-  config:
-    repoURL: https://github.com/example/repo.git
-    checkout:
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-# Prepare the contents of ./out ...
-# Commit, push, etc...
-```
-
-### `copy`
-
-`copy` copies files or the contents of entire directories from one specified
-location to another.
-
-#### `copy` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `inPath` | `string` | Y | Path to the file or directory to be copied. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-| `outPath` | `string` | Y | Path to the destination. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-
-#### `copy` Example
-
-The most common (though still advanced) usage of this step is to combine content
-from two working trees to use as input to a subsequent step, such as one that
-renders Stage-specific manifests.
-
-Consider a Stage that requests Freight from two Warehouses, where one provides
-Kustomize "base" configuration, while the other provides a Stage-specific
-Kustomize overlay. Rendering the manifests intended for such a Stage will
-require combining the base and overlay configurations:
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo, warehouse("base")).ID }}
-      path: ./src
-    - commit: ${{ commitFrom(vars.gitRepo, warehouse(ctx.stage + "-overlay")).ID }}
-      path: ./overlay
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: copy
-  config:
-    inPath: ./overlay/stages/${{ ctx.stage }}/kustomization.yaml
-    outPath: ./src/stages/${{ ctx.stage }}/kustomization.yaml
-# Render manifests to ./out, commit, push, etc...
-```
-
-### `delete`
-
-`delete` deletes a file or directory.
-
-#### `delete` Configuration
-
-| Name      | Type | Required | Description                              |
-|-----------|------|----------|------------------------------------------|
-| `path`    | `string` | Y | Path to the file or directory to delete. |
-
-#### `delete` Example
-
-One, common usage of this step is to remove intermediate files
-produced by the promotion process prior to a `git-commit` step:
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-
-# Steps that produce intermediate files in ./out...
-
-- uses: delete
-  config:
-    path: ./out/unwanted/file
-- uses: git-commit
-  config:
-    path: ./out
-```
-
-### `kustomize-set-image`
-
-`kustomize-set-image` updates the `kustomization.yaml` file in a specified
-directory to reflect a different revision of a container image. It is equivalent
-to executing `kustomize edit set image`. This step is commonly followed by a
-[`kustomize-build`](#kustomize-build) step.
-
-#### `kustomize-set-image` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `path` | `string` | Y | Path to a directory containing a `kustomization.yaml` file. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-| `images` | `[]object` | N | The details of changes to be applied to the `kustomization.yaml` file. When left unspecified, all images from the Freight collection will be set in the Kustomization file. Unless there is an ambiguous image name (for example, due to two Warehouses subscribing to the same repository), which requires manual configuration. |
-| `images[].image` | `string` | Y | Name/URL of the image being updated. |
-| `images[].tag` | `string` | N | A tag naming a specific revision of `image`. Mutually exclusive with `digest`. If none of these are specified, the tag specified by a piece of Freight referencing `image` will be used as the value of this field. |
-| `images[].digest` | `string` | N | A digest naming a specific revision of `image`. Mutually exclusive with `tag`. If none of these are specified, the tag specified by a piece of Freight referencing `image` will be used as the value of `tag`. |
-| `images[].newName` | `string` | N | A substitution for the name/URL of the image being updated. This is useful when different Stages have access to different container image repositories (assuming those different repositories contain equivalent images that are tagged identically). This may be a frequent consideration for users of Amazon's Elastic Container Registry. |
-
-#### `kustomize-set-image` Examples
-
-<Tabs groupId="kustomize-set-image">
-
-<TabItem value="common" label="Common Usage" default>
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-- name: imageRepo
-  value: my/image
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: kustomize-set-image
-  config:
-    path: ./src/base
-    images:
-    - image: ${{ vars.imageRepo }}
-      tag: ${{ imageFrom(vars.imageRepo).Tag }}
-# Render manifests to ./out, commit, push, etc...
-```
-
-</TabItem>
-
-<TabItem value="name-change" label="Changing an Image Name">
-
-For this example, consider the promotion of Freight containing a reference to
-some revision of the container image
-`123456789012.dkr.ecr.us-east-1.amazonaws.com/my-image`. This image exists in the
-`us-east-1` region of Amazon's Elastic Container Registry. However, assuming the
-Stage targeted by the promotion is backed by environments in the `us-west-2`
-region, it will be necessary to make a substitution when updating the
-`kustomization.yaml` file. This can be accomplished like so:
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: kustomize-set-image
-  config:
-    path: ./src/base
-    images:
-    - image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-image
-      newName: 123456789012.dkr.ecr.us-west-2.amazonaws.com/my-image
-# Render manifests to ./out, commit, push, etc...
-```
-
-</TabItem>
-
-</Tabs>
-
-#### `kustomize-set-image` Output
-
-| Name | Type | Description |
-|------|------|-------------|
-| `commitMessage` | `string` | A description of the change(s) applied by this step. Typically, a subsequent [`git-commit`](#git-commit) step will reference this output and aggregate this commit message fragment with other like it to build a comprehensive commit message that describes all changes. |
-
-### `kustomize-build`
-
-`kustomize-build` renders manifests from a specified directory containing a
-`kustomization.yaml` file to a specified file or to many files in a specified
-directory. This step is useful for the common scenario of rendering
-Stage-specific manifests to a Stage-specific branch. This step is commonly
-preceded by a [`git-clear`](#git-clear) step and followed by
-[`git-commit`](#git-commit) and [`git-push`](#git-push) steps.
-
-#### `kustomize-build` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `path` | `string` | Y | Path to a directory containing a `kustomization.yaml` file. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-| `outPath` | `string` | Y | Path to the file or directory where rendered manifests are to be written. If the path ends with `.yaml` or `.yml` it is presumed to indicate a file and is otherwise presumed to indicate a directory. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-| `plugin.helm.apiVersions` | `[]string` | N | Optionally specifies a list of supported API versions to be used when rendering manifests using Kustomize's Helm chart plugin. This is useful for charts that may contain logic specific to different Kubernetes API versions. |
-| `plugin.helm.kubeVersion` | `string` | N | Optionally specifies a Kubernetes version to be assumed when rendering manifests using Kustomize's Helm chart plugin. This is useful for charts that may contain logic specific to different Kubernetes versions. |
-
-#### `kustomize-build` Examples
-
-<Tabs groupId="kustomize-build">
-
-<TabItem value="file" label="Rendering to a File" default>
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: kustomize-build
-  config:
-    path: ./src/stages/${{ ctx.stage }}
-    outPath: ./out/manifests.yaml
-# Commit, push, etc...
-```
-
-</TabItem>
-
-<TabItem value="dir" label="Rendering to a Directory" default>
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: kustomize-build
-  config:
-    path: ./src/stages/${{ ctx.stage }}
-    outPath: ./out
-# Commit, push, etc...
-```
-
-</TabItem>
-
-</Tabs>
-
-### `json-update`
-
-`json-update` updates the values of specified keys in any JSON file.
-
-#### `json-update` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `path` | `string` | Y | Path to a JSON file. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |                      |
-| `updates` | `[]object` | Y | The details of changes to be applied to the file. At least one must be specified. |
-| `updates[].key` | `string` | Y | The key to update within the file. For nested values, use a JSON dot notation path. See [sjson documentation](https://github.com/tidwall/sjson) for supported syntax. |
-| `updates[].value`| `any` | Y | The new value for the key. Typically specified using an expression. Supports strings, numbers, booleans, arrays, and objects. |
-
-#### `json-update` Example
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-    - uses: git-clear
-      config:
-        path: ./out
-- uses: json-update
-  config:
-    path: configs/settings.json
-    updates:
-    - key: image.tag
-      value: ${{ imageFrom("my/image").Tag }}
-# Render manifests to ./out, commit, push, etc...
-```
-
-#### `json-update` Output
-
-| Name | Type | Description |
-|------|------|-------------|
-| `commitMessage` | `string` | A description of the change(s) applied by this step. Typically, a subsequent [`git-commit`](#git-commit) step will reference this output and aggregate this commit message fragment with other like it to build a comprehensive commit message that describes all changes. |
-
-### `yaml-update`
-
-`yaml-update` updates the values of specified keys in any YAML file. This step
-most often used to update image tags or digests in a Helm values and is commonly
-followed by a [`helm-template`](#helm-template) step.
-
-#### `yaml-update` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `path` | `string` | Y | Path to a YAML file. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-| `updates` | `[]object` | Y | The details of changes to be applied to the file. At least one must be specified. |
-| `updates[].key` | `string` | Y | The key to update within the file. For nested values, use dots to delimit key parts. e.g. `image.tag`. The syntax is identical to that supported by the `json-update` step and is documented in more detail [here](https://github.com/tidwall/sjson?tab=readme-ov-file#path-syntax). |
-| `updates[].value` | `string` | Y | The new value for the key. Typically specified using an expression. |
-
-#### `yaml-update` Example
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: yaml-update
-  config:
-    path: ./src/charts/my-chart/values.yaml
-    updates:
-    - key: image.tag
-      value: ${{ imageFrom("my/image").Tag }}
-# Render manifests to ./out, commit, push, etc...
-```
-
-#### `yaml-update` Output
-
-| Name | Type | Description |
-|------|------|-------------|
-| `commitMessage` | `string` | A description of the change(s) applied by this step. Typically, a subsequent [`git-commit`](#git-commit) step will reference this output and aggregate this commit message fragment with other like it to build a comprehensive commit message that describes all changes. |
-
-### `helm-update-chart`
-
-`helm-update-chart` performs specified updates on the `dependencies` section of
-a specified Helm chart's `Chart.yaml` file. This step is useful for the common
-scenario of updating a chart's dependencies to reflect new versions of charts
-referenced by the Freight being promoted. This step is commonly followed by a
-[`helm-template`](#helm-template) step.
-
-#### `helm-update-chart` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `path` | `string` | Y | Path to a Helm chart (i.e. to a directory containing a `Chart.yaml` file). This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-| `charts` | `[]string` | Y | The details of dependency (subschart) updates to be applied to the chart's `Chart.yaml` file. |
-| `charts[].repository` | `string` | Y | The URL of the Helm chart repository in the `dependencies` entry whose `version` field is to be updated. Must _exactly_ match the `repository` field of that entry. |
-| `charts[].name` | `string` | Y | The name of the chart in in the `dependencies` entry whose `version` field is to be updated. Must exactly match the `name` field of that entry. |
-| `charts[].version` | `string` | Y | The version to which the dependency should be updated. |
-
-#### `helm-update-chart` Examples
-
-<Tabs groupId="helm-update-chart">
-
-<TabItem value="classic" label="Classic Chart Repository" default>
-
-Given a `Chart.yaml` file such as the following:
-
-```yaml
-apiVersion: v2
-name: example
-type: application
-version: 0.1.0
-appVersion: 0.1.0
-dependencies:
-- repository: https://example-chart-repo
-  name: some-chart
-  version: 1.2.3
-```
-
-The `dependencies` can be updated to reflect the version of `some-chart`
-referenced by the Freight being promoted like so:
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-- name: chartRepo
-  value: https://example-chart-repo
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: helm-update-chart
-  config:
-    path: ./src/charts/my-chart
-    charts:
-    - repository: ${{ chartRepo }}
-      name: some-chart
-      version: ${{ chartFrom(chartRepo).Version }}
-# Render manifests to ./out, commit, push, etc...
-```
-
-</TabItem>
-
-<TabItem value="oci" label="OCI Chart Repository" default>
-
-:::caution
-Classic (HTTP/HTTPS) Helm chart repositories can contain many differently named
-charts. A specific chart, therefore, can be identified by a repository URL and
-a chart name.
-
-OCI repositories, on the other hand, are organizational constructs within OCI
-_registries._ Each OCI repository is presumed to contain versions of only a
-single chart. As such, a specific chart can be identified by a repository URL
-alone.
-
-Kargo Warehouses understand this distinction well, so a subscription to an OCI
-chart repository will utilize its URL only, _without_ specifying a chart name.
-For example:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Warehouse
-metadata:
-  name: my-warehouse
-  namespace: kargo-demo
-spec:
-  subscriptions:
-  - chart:
-      repoURL: oci://example-chart-registry/some-chart
-      semverConstraint: ^1.0.0
-```
-
-Helm deals with this difference somewhat more awkwardly, however. When a Helm
-chart references a chart in an OCI repository, it must reference the _registry_
-by URL in the `repository` field and _still_ specify a chart name in the name
-field. For example:
-
-```yaml
-apiVersion: v2
-name: example
-type: application
-version: 0.1.0
-appVersion: 0.1.0
-dependencies:
-- repository: oci://example-chart-registry
-  name: some-chart
-  version: 1.2.3
-```
-
-__When using `helm-update-chart` to update the dependencies in a `Chart.yaml`
-file, you must play by Helm's rules and use the _registry_ URL in the
-`repository` field and the _repository_ name (chart name) in the `name` field.__
-:::
-
-:::info
-As a general rule, when configuring Kargo to update something, observe the
-conventions of the thing being updated, even if those conventions differ from
-Kargo's own. Kargo is aware of such differences and will adapt accordingly.
-:::
-
-Given a `Chart.yaml` file such as the following:
-
-```yaml
-apiVersion: v2
-name: example
-type: application
-version: 0.1.0
-appVersion: 0.1.0
-dependencies:
-- repository: oci://example-chart-registry
-  name: some-chart
-  version: 1.2.3
-```
-
-The `dependencies` can be updated to reflect the version of
-`oci://example-chart-registry/some-chart` referenced by the Freight being
-promoted like so:
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-- name: chartReg
-  value: oci://example-chart-registry
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: helm-update-chart
-  config:
-    path: ./src/charts/my-chart
-    charts:
-    - repository: ${{ chartReg }}
-      name: some-chart
-      version: ${{ chartFrom(chartReg + "/some-chart").Version }}
-# Render manifests to ./out, commit, push, etc...
-```
-
-</TabItem>
-
-</Tabs>
-
-#### `helm-update-chart` Output
-
-| Name | Type | Description |
-|------|------|-------------|
-| `commitMessage` | `string` | A description of the change(s) applied by this step. Typically, a subsequent [`git-commit`](#git-commit) step will reference this output and aggregate this commit message fragment with other like it to build a comprehensive commit message that describes all changes. |
-
-### `helm-template`
-
-`helm-template` renders a specified Helm chart to a specified directory or to
-many files in a specified directory. This step is useful for the common scenario
-of rendering Stage-specific manifests to a Stage-specific branch. This step is
-commonly preceded by a [`git-clear`](#git-clear) step and followed by
-[`git-commit`](#git-commit) and [`git-push`](#git-push) steps.
-
-#### `helm-template` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `path` | `string` | Y | Path to a Helm chart (i.e. to a directory containing a `Chart.yaml` file). This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-| `outPath` | `string` | Y | Path to the file or directory where rendered manifests are to be written. If the path ends with `.yaml` or `.yml` it is presumed to indicate a file and is otherwise presumed to indicate a directory. |
-| `releaseName` | `string` | Y | Release name to use when rendering the manifests. |
-| `namespace` | `string` | N | Optional namespace to use when rendering the manifests. This is commonly omitted. GitOps agents such as Argo CD will generally ensure the installation of manifests into the namespace specified by their own configuration. |
-| `valuesFiles` | `[]string` | N | Helm values files (apart from the chart's default `values.yaml`) to be used when rendering the manifests.  |
-| `includeCRDs` | `boolean` | N | Whether to include CRDs in the rendered manifests. This is `false` by default. |
-| `kubeVersion` | `string` | N | Optionally specifies a Kubernetes version to be assumed when rendering manifests. This is useful for charts that may contain logic specific to different Kubernetes versions. |
-| `apiVersions` | `[]string` | N | Allows a manual set of supported API versions to be specified. |
-
-#### `helm-template` Examples
-
-<Tabs groupId="kustomize-build">
-
-<TabItem value="file" label="Rendering to a File" default>
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: helm-template
-  config:
-    path: ./src/charts/my-chart
-    valuesFiles:
-    - ./src/charts/my-chart/${{ ctx.stage }}-values.yaml
-    outPath: ./out/manifests.yaml
-# Commit, push, etc...
-```
-
-</TabItem>
-
-<TabItem value="dir" label="Rendering to a Directory">
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: helm-template
-  config:
-    path: ./src/charts/my-chart
-    valuesFiles:
-    - ./src/charts/my-chart/${{ ctx.stage }}-values.yaml
-    outPath: ./out
-# Commit, push, etc...
-```
-
-</TabItem>
-
-</Tabs>
-
-### `git-commit`
-
-`git-commit` commits all changes in a working tree to its checked out branch.
-This step is often used after previous steps have put the working tree into the
-desired state and is commonly followed by a [`git-push`](#git-push) step.
-
-#### `git-commit` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `path` | `string` | Y | Path to a Git working tree containing changes to be committed. This path is relative to the temporary workspace that Kargo provisions for use by the promotion process. |
-| `message` | `string` | Y | The commit message. |
-| `author` | `[]object` | N | Optionally provider authorship information for the commit. |
-| `author.name` | `string` | N | The committer's name. |
-| `author.email` | `string` | N | The committer's email address. |
-
-#### `git-commit` Example
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: git-clone
-  config:
-    repoURL: ${{ vars.gitRepo }}
-    checkout:
-    - commit: ${{ commitFrom(vars.gitRepo).ID }}
-      path: ./src
-    - branch: stage/${{ ctx.stage }}
-      create: true
-      path: ./out
-- uses: git-clear
-  config:
-    path: ./out
-- uses: kustomize-set-image
-  as: update-image
-  config:
-    images:
-    - image: my/image
-- uses: kustomize-build
-  config:
-    path: ./src/stages/${{ ctx.stage }}
-    outPath: ./out
-- uses: git-commit
-  config:
-    path: ./out
-    message: ${{ outputs['update-image'].commitMessage }}
-# Push, etc...
-```
-
-#### `git-commit` Output
-
-| Name | Type | Description |
-|------|------|-------------|
-| `commit` | `string` | The ID (SHA) of the commit created by this step. If the step short-circuited and did not create a new commit because there were no differences from the current head of the branch, this value will be the ID of the existing commit at the head of the branch instead. Typically, a subsequent [`argocd-update`](#argocd-update) step will reference this output to learn the ID of the commit that an applicable Argo CD `ApplicationSource` should be observably synced to under healthy conditions. |
-
-### `git-push`
-
-`git-push` pushes the committed changes in a specified working tree to a
-specified branch in the remote repository. This step typically follows a
-`git-commit` step and is often followed by a `git-open-pr` step.
-
-This step also implements its own, internal retry logic. If a push fails, with
-the cause determined to be the presence of new commits in the remote branch that
-are not present in the local branch, the step will attempt to rebase before
-retrying the push. Any merge conflict requiring manual resolution will
-immediately halt further attempts.
-
-:::info
-This step's internal retry logic is helpful in scenarios when concurrent
-Promotions to multiple Stages may all write to the same branch of the same
-repository.
-
-Because conflicts requiring manual resolution will halt further attempts, it is
-recommended to design your Promotion processes such that Promotions to multiple
-Stages that write to the same branch do not write to the same files.
-:::
-
-#### `git-push` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `path` | `string` | Y | Path to a Git working tree containing committed changes. |
-| `targetBranch` | `string` | N | The branch to push to in the remote repository. Mutually exclusive with `generateTargetBranch=true`. If neither of these is provided, the target branch will be the same as the branch currently checked out in the working tree. |
-| `maxAttempts` | `int32` | N | The maximum number of attempts to make when pushing to the remote repository. Default is 50. |
-| `generateTargetBranch` | `boolean` | N | Whether to push to a remote branch named like `kargo/<project>/<stage>/promotion`. If such a branch does not already exist, it will be created. A value of 'true' is mutually exclusive with `targetBranch`. If neither of these is provided, the target branch will be the currently checked out branch. This option is useful when a subsequent promotion step will open a pull request against a Stage-specific branch. In such a case, the generated target branch pushed to by the `git-push` step can later be utilized as the source branch of the pull request. |
-
-#### `git-push` Examples
-
-<Tabs groupId="git-push">
-
-<TabItem value="common" label="Common Usage" default>
-
-```yaml
-steps:
-# Clone, prepare the contents of ./out, etc...
-- uses: git-commit
-  config:
-    path: ./out
-    message: rendered updated manifests
-- uses: git-push
-  config:
-    path: ./out
-```
-
-</TabItem>
-
-<TabItem value="pr" label="For Use With a PR">
-
-```yaml
-steps:
-# Clone, prepare the contents of ./out, etc...
-- uses: git-commit
-  config:
-    path: ./out
-    message: rendered updated manifests
-- uses: git-push
-  as: push
-  config:
-    path: ./out
-    generateTargetBranch: true
-# Open a PR and wait for it to be merged or closed...
-```
-
-</TabItem>
-
-</Tabs>
-
-#### `git-push` Output
-
-| Name | Type | Description |
-|------|------|-------------|
-| `branch` | `string` | The name of the remote branch pushed to by this step. This is especially useful when the `generateTargetBranch=true` option has been used, in which case a subsequent [`git-open-pr`](#git-open-pr) will typically reference this output to learn what branch to use as the head branch of a new pull request. |
-| `commit` | `string` | The ID (SHA) of the commit pushed by this step. |
-
-### `git-open-pr`
-
-`git-open-pr` opens a pull request in a specified remote repository using
-specified source and target branches. This step is often used after a `git-push`
-and is commonly followed by a `git-wait-for-pr` step.
-
-At present, this feature only supports GitHub pull requests and GitLab merge
-requests.
-
-#### `git-open-pr` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `repoURL` | `string` | Y | The URL of a remote Git repository. |
-| `provider` | `string` | N | The name of the Git provider to use. Currently only `github` and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified.  |
-| `insecureSkipTLSVerify` | `boolean` | N | Indicates whether to bypass TLS certificate verification when interfacing with the Git provider. Setting this to `true` is highly discouraged in production. |
-| `sourceBranch` | `string` | Y | Specifies the source branch for the pull request. |
-| `targetBranch` | `string` | N | The branch to which the changes should be merged. |
-| `createTargetBranch` | `boolean` | N | Indicates whether a new, empty orphaned branch should be created and pushed to the remote if the target branch does not already exist there. Default is `false`. |
-| `title` | `string` | N | The title for the pull request. Kargo generates a title based on the commit messages if it is not explicitly specified. |
-| `labels` | `[]string` | N | Labels to add to the pull request. |
-
-#### `git-open-pr`  Example
-
-```yaml
-steps:
-# Clone, prepare the contents of ./out, commit, etc...
-- uses: git-push
-  as: push
-  config:
-    path: ./out
-    generateTargetBranch: true
-- uses: git-open-pr
-  as: open-pr
-  config:
-    repoURL: https://github.com/example/repo.git
-    createTargetBranch: true
-    sourceBranch: ${{ outputs.push.branch }}
-    targetBranch: stage/${{ ctx.stage }}
-# Wait for the PR to be merged or closed...
-```
-
-#### `git-open-pr` Output
-
-| Name | Type | Description |
-|------|------|-------------|
-| `prNumber` | `number` | The numeric identifier of the pull request opened by this step. Typically, a subsequent [`git-wait-for-pr`](#git-wait-for-pr) step will reference this output to learn what pull request to monitor. |
-
-### `git-wait-for-pr`
-
-`git-wait-for-pr` waits for a specified open pull request to be merged or
-closed. This step commonly follows a `git-open-pr` step and is commonly followed
-by an `argocd-update` step.
-
-#### `git-wait-for-pr` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `repoURL` | `string` | Y | The URL of a remote Git repository. |
-| `provider` | `string` | N | The name of the Git provider to use. Currently only `github` and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. |
-| `insecureSkipTLSVerify` | `boolean` | N | Indicates whether to bypass TLS certificate verification when interfacing with the Git provider. Setting this to `true` is highly discouraged in production. |
-| `prNumber` | `string` | Y | The number of the pull request to wait for. |
-
-#### `git-wait-for-pr` Output
-
-| Name | Type | Description |
-|------|------|-------------|
-| `commit` | `string` | The ID (SHA) of the new commit at the head of the target branch after merge. Typically, a subsequent [`argocd-update`](#argocd-update) step will reference this output to learn the ID of the commit that an applicable Argo CD `ApplicationSource` should be observably synced to under healthy conditions. |
-
-#### `git-wait-for-pr` Example
-
-```yaml
-steps:
-# Clone, prepare the contents of ./out, commit, etc...
-- uses: git-push
-  as: push
-  config:
-    path: ./out
-    generateTargetBranch: true
-- uses: git-open-pr
-  as: open-pr
-  config:
-    repoURL: https://github.com/example/repo.git
-    createTargetBranch: true
-    sourceBranch: ${{ outputs.push.branch }}
-    targetBranch: stage/${{ ctx.stage }}
-- uses: git-wait-for-pr
-  as: wait-for-pr
-  config:
-    repoURL: https://github.com/example/repo.git
-    prNumber: ${{ outputs['open-pr'].prNumber }}
-```
-
-### `argocd-update`
-
-`argocd-update` updates one or more Argo CD `Application` resources in various
-ways. Among other scenarios, this step is useful for the common one of forcing
-an Argo CD `Application` to sync after previous steps have updated a remote
-branch referenced by the `Application`. This step is commonly the last step in a
-promotion process.
-
-:::note
-For an Argo CD `Application` resource to be managed by a Kargo `Stage`,
-the `Application` _must_ have an annotation of the following form:
-
-```yaml
-kargo.akuity.io/authorized-stage: "<project-name>:<stage-name>"
-```
-
-Such an annotation offers proof that a user who is themselves authorized
-to update the `Application` in question has consented to a specific
-`Stage` updating the `Application` as well.
-
-The following example shows how to configure an Argo CD `Application`
-manifest to authorize the `test` `Stage` of the `kargo-demo` `Project`:
-
-```yaml
-apiVersion: argoproj.io/v1alpha1
-kind: Application
-metadata:
-  name: kargo-demo-test
-  namespace: argocd
-  annotations:
-    kargo.akuity.io/authorized-stage: kargo-demo:test
-spec:
-  # Application specifications go here
-```
-:::
-
-:::info
-Enforcement of Argo CD
-[sync windows](https://argo-cd.readthedocs.io/en/stable/user-guide/sync_windows/)
-was improved substantially in Argo CD v2.11.0. If you wish for the `argocd-update`
-step to honor sync windows, you must use Argo CD v2.11.0 or later.
-
-_Additionally, it is recommended that if a promotion process is expected to
-sometimes encounter an active deny window, the `argocd-update` step should be
-configured with a timeout that is at least as long as the longest expected deny
-window. (The step's default timeout is five minutes.)_
-:::
-
-#### `argocd-update` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `apps` | `[]object` | Y | Describes Argo CD `Application` resources to update and how to update them. At least one must be specified.  |
-| `apps[].name` | `string` | Y | The name of the Argo CD `Application`. __Note:__ A small technical restriction on this field is that any [expressions](20-expression-language.md) used therein are limited to accessing `ctx` and `vars` and may not access `secrets` or any Freight. This is because templates in this field are, at times, evaluated outside the context of an actual `Promotion` for the purposes of building an index. In practice, this restriction does not prove to be especially limiting. |
-| `apps[].namespace` | `string` | N | The namespace of the Argo CD `Application` resource to be updated. If left unspecified, the namespace will be the Kargo controller's configured default -- typically `argocd`. __Note:__ This field is subject to the same restrictions as the `name` field. See above. |
-| `apps[].sources` | `[]object` | N | Describes Argo CD `ApplicationSource`s to update and how to update them. |
-| `apps[].sources[].repoURL` | `string` | Y | The value of the target `ApplicationSource`'s  own `repoURL` field. This must match exactly. |
-| `apps[].sources[].chart` | `string` | N | Applicable only when the target `ApplicationSource` references a Helm chart repository, the value of the target `ApplicationSource`'s  own `chart` field. This must match exactly. |
-| `apps[].sources[].desiredRevision` | `string` | N | Specifies the desired revision for the source. i.e. The revision to which the source must be observably synced when performing a health check. Prior to v1.1.0, if left undefined, the desired revision was determined by Freight (if possible). Beginning with v1.1.0, if left undefined, Kargo will not require the source to be observably synced to any particular source to be considered healthy. Note that the source's `targetRevision` will not be updated to this revision unless `updateTargetRevision=true` is also set. |
-| `apps[].sources[].updateTargetRevision` | `boolean` | Y | Indicates whether the target `ApplicationSource` should be updated such that its `targetRevision` field points directly at the desired revision. A `true` value in this field requires `desiredRevision` to be specified. |
-| `apps[].sources[].kustomize` | `object` | N | Describes updates to an Argo CD `ApplicationSource`'s Kustomize-specific properties. |
-| `apps[].sources[].kustomize.images` | `[]object` | Y | Describes how to update an Argo CD `ApplicationSource`'s Kustomize-specific properties to reference specific versions of container images. |
-| `apps[].sources[].kustomize.images[].repoURL` | `string` | Y | URL of the image being updated. |
-| `apps[].sources[].kustomize.images[].tag` | `string` | N | A tag naming a specific revision of the image specified by `repoURL`. Mutually exclusive with `digest`. One of `digest` or `tag` must be specified. |
-| `apps[].sources[].kustomize.images[].digest` | `string` | N | A digest naming a specific revision of the image specified by `repoURL`. Mutually exclusive with `tag`. One of `digest` or `tag` must be specified. |
-| `apps[].sources[].kustomize.images[].newName` | `string` | N | A substitution for the name/URL of the image being updated. This is useful when different Stages have access to different container image repositories (assuming those different repositories contain equivalent images that are tagged identically). This may be a frequent consideration for users of Amazon's Elastic Container Registry. |
-| `apps[].sources[].helm` | `object` | N | Describes updates to an Argo CD `ApplicationSource`'s Helm parameters. |
-| `apps[].sources[].helm.images` | `[]object` | Y | Describes how to update  an Argo CD `ApplicationSource`'s Helm parameters to reference specific versions of container images. |
-| `apps[].sources[].helm.images[].key` | `string` | Y | The key to update within the target `ApplicationSource`'s `helm.parameters` map. See Helm documentation on the [format and limitations](https://helm.sh/docs/intro/using_helm/#the-format-and-limitations-of---set) of the notation used in this field. |
-| `apps[].sources[].helm.images[].value` | `string` | Y | Specifies the new value for the key. Typically, a value from [`chartFrom()`](20-expression-language.md#chartfrom) is used here. |
-
-#### `argocd-update` Examples
-
-<Tabs groupId="argocd-update">
-
-<TabItem value="common" label="Common Usage" default>
-
-```yaml
-steps:
-# Clone, render manifests, commit, push, etc...
-- uses: git-commit
-  as: commit
-  config:
-    path: ./out
-    message: ${{ outputs['update-image'].commitMessage }}
-- uses: git-push
-  config:
-    path: ./out
-- uses: argocd-update
-  config:
-    apps:
-    - name: my-app
-      sources:
-      - repoURL: https://github.com/example/repo.git
-        desiredRevision: ${{ outputs.commit.commit }}
-```
-
-</TabItem>
-
-<TabItem value="target-revision" label="Updating Target Revision">
-
-:::caution
-Without making any modifications to a Git repository, this example simply
-updates a "live" Argo CD `Application` resource to point its `targetRevision`
-field at a specific version of a Helm chart, which Argo CD will pull directly
-from the chart repository.
-
-While this can be a useful technique, it should be used with caution. This is
-not "real GitOps" since the state of the `Application` resource is not backed
-up in a Git repository. If the `Application` resource were deleted, there would
-be no remaining record of its desired state.
-:::
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: argocd-update
-  config:
-    apps:
-    - name: my-app
-      sources:
-      - repoURL: ${{ chartRepo }}
-        chart: my-chart
-        targetRevision: ${{ chartFrom(chartRepo, "my-chart").Version }}
-```
-
-</TabItem>
-
-<TabItem value="updating-image-kustomize" label="Updating an Image with Kustomize">
-
-:::caution
-Without making any modifications to a Git repository, this example simply
-updates Kustomize-specific properties of a "live" Argo CD `Application`
-resource.
-
-While this can be a useful technique, it should be used with caution. This is
-not "real GitOps" since the state of the `Application` resource is not backed up
-in a Git repository. If the `Application` resource were deleted, there would be
-no remaining record of its desired state.
-:::
-
-```yaml
-vars:
-- name: gitRepo
-  value: https://github.com/example/repo.git
-steps:
-- uses: argocd-update
-  config:
-    apps:
-    - name: my-app
-      sources:
-      - repoURL: https://github.com/example/repo.git
-        kustomize:
-          images:
-          - repoURL: ${{ vars.imageRepo }}
-            tag: ${{ imageFrom(vars.imageRepo).Tag }}
-```
-
-</TabItem>
-
-<TabItem value="updating-image-helm" label="Updating an Image with Helm">
-
-:::caution
-Without making any modifications to a Git repository, this example simply
-updates Helm-specific properties of a "live" Argo CD `Application` resource.
-
-While this can be a useful technique, it should be used with caution. This is
-not "real GitOps" since the state of the `Application` resource is not backed
-up in a Git repository. If the `Application` resource were deleted, there would
-be no remaining record of its desired state.
-:::
-
-```yaml
-steps:
-- uses: argocd-update
-  config:
-    apps:
-    - name: my-app
-      sources:
-      - repoURL: https://github.com/example/repo.git
-        helm:
-          images:
-          - key: image.tag
-            value: ${{ imageFrom("my/image").Tag }}
-```
-
-</TabItem>
-
-</Tabs>
-
-#### `argocd-update` Health Checks
-
-The `argocd-update` step is unique among all other built-in promotion steps in
-that, on successful completion, it will register health checks to be performed
-upon the target Stage on an ongoing basis. This health check configuration is
-_opaque_ to the rest of Kargo and is understood only by health check
-functionality built into the step. This permits Kargo to factor the health and
-sync state of Argo CD `Application` resources into the overall health of a Stage
-without requiring Kargo to understand `Application` health directly.
-
-:::info
-Although the `argocd-update` step is the only promotion step to currently
-utilize this health check framework, we anticipate that future built-in and
-third-party promotion steps will take advantage of it as well.
-:::
-
-### `http`
-
-`http` is a generic step that makes an HTTP/S request to enable basic integration
-with a wide variety of external services.
-
-#### `http` Configuration
-
-| Name | Type | Required | Description |
-|------|------|----------|-------------|
-| `method` | `string` | Y | The HTTP method to use. |
-| `url` | `string` | Y | The URL to which the request should be made. |
-| `headers` | `[]object` | N | A list of headers to include in the request. |
-| `headers[].name` | `string` | Y | The name of the header. |
-| `headers[].value` | `string` | Y | The value of the header. |
-| `queryParams` | `[]object` | N | A list of query parameters to include in the request. |
-| `queryParams[].name` | `string` | Y | The name of the query parameter. |
-| `queryParams[].value` | `string` | Y | The value of the query parameter. The provided value will automatically be URL-encoded if necessary. |
-| `body` | `string` | N | The body of the request. __Note:__ As this field is a `string`, take care to utilize [`quote()`](./20-expression-language.md#quote) if the body is a valid JSON `object`. Refer to the example below of posting a message to a Slack channel. |
-| `insecureSkipTLSVerify` | `boolean` | N | Indicates whether to bypass TLS certificate verification when making the request. Setting this to `true` is highly discouraged. |
-| `timeout` | `string` | N | A string representation of the maximum time interval to wait for a request to complete. _This is the timeout for an individual HTTP request. If a request is retried, each attempt is independently subject to this timeout._ See Go's [`time` package docs](https://pkg.go.dev/time#ParseDuration) for a description of the accepted format. |
-| `successExpression` | `string` | N | An [expr-lang] expression that can evaluate the response to determine success. If this is left undefined and `failureExpression` _is_ defined, the default success criteria will be the inverse of the specified failure criteria. If both are left undefined, success is `true` when the HTTP status code is `2xx`. If `successExpression` and `failureExpression` are both defined and both evaluate to `true`, the failure takes precedence. Note that this expression should _not_ be offset by `${{` and `}}`. See examples for more details. |
-| `failureExpression` | `string` | N | An [expr-lang] expression that can evaluate the response to determine failure. If this is left undefined and `successExpression` _is_ defined, the default failure criteria will be the inverse of the specified success criteria. If both are left undefined, failure is `true` when the HTTP status code is _not_ `2xx`. If `successExpression` and `failureExpression` are both defined and both evaluate to `true`, the failure takes precedence. Note that this expression should _not_ be offset by `${{` and `}}`. See examples for more details. |
-| `outputs` | `[]object` | N | A list of rules for extracting outputs from the HTTP response. These are only applied to responses deemed successful. |
-| `outputs[].name` | `string` | Y | The name of the output. |
-| `outputs[].fromExpression` | `string` | Y | An [expr-lang] expression that can extract a value from the HTTP response. Note that this expression should _not_ be offset by `${{` and `}}`. See examples for more details. |
-
-:::note
-An HTTP response that is not conclusively determined to have succeeded or failed
-will result in the step reporting a result of `Running`. Kargo will
-[retry](#step-retries) such a step on its next attempt at reconciling the
-`Promotion` resource. This will continue until the step succeeds, fails,
-exhausts the configured maximum number of retries, or a configured timeout has
-elapsed.
-:::
-
-#### `http` Expressions
-
-The `successExpression`, `failureExpression`, and `outputs[].fromExpression`
-fields all support [expr-lang] expressions.
-
-:::note
-The expressions included in the `successExpression`, `failureExpression`, and
-`outputs[].fromExpression` fields should _not_ be offset by `${{` and `}}`. This
-is to prevent the expressions from being evaluated by Kargo during
-pre-processing of step configurations. The `http` step itself will evaluate
-these expressions.
-:::
-
-A `response` object (a `map[string]any`) is available to these expressions. It
-is structured as follows:
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `status` | `int` | The HTTP status code of the response. |
-| `headers` | `http.Header` | The headers of the response. See applicable [Go documentation](https://pkg.go.dev/net/http#Header). |
-| `header` | `func(string) string` | `headers` can be inconvenient to work with directly. This function allows you to access a header by name. |
-| `body` | `map[string]any` | The body of the response, if any, unmarshaled into a map. If the response body is empty, this map will also be empty. |
-
-#### `http` Examples
-
-<Tabs groupId="http">
-
-<TabItem value="basic" label="Basic Usage" default>
-
-This examples configuration makes a `GET` request to the
-[Cat Facts API.](https://www.catfacts.net/api/) and uses the default
-success/failure criteria.
-
-```yaml
-steps:
-# ...
-- uses: http
-  as: cat-facts
-  config:
-    method: GET
-    url: https://www.catfacts.net/api/
-    outputs:
-    - name: status
-      fromExpression: response.status
-    - name: fact1
-      fromExpression: response.body.facts[0]
-    - name: fact2
-      fromExpression: response.body.facts[1]
-```
-
-Assuming a `200` response with the following JSON body:
-
-```json
-{
-    "facts": [
-        {
-            "fact_number": 1,
-            "fact": "Kittens have baby teeth, which are replaced by permanent teeth around the age of 7 months."
-        },
-        {
-            "fact_number": 2,
-            "fact": "Each day in the US, animal shelters are forced to destroy 30,000 dogs and cats."
-        }
-    ]
-}
-```
-
-The step would succeed and produce the following outputs:
-
-| Name     | Type | Value |
-|----------|------|-------|
-| `status` | `int` | `200` |
-| `fact1` | `string` | `Kittens have baby teeth, which are replaced by permanent teeth around the age of 7 months.` |
-| `fact2` | `string` | `Each day in the US, animal shelters are forced to destroy 30,000 dogs and cats.` |
-
-</TabItem>
-
-<TabItem value="polling" label="Polling">
-
-Building on the basic example, this configuration defines explicit success and
-failure criteria. Any response meeting neither of these criteria will result in
-the step reporting a result of `Running` and being retried. Note the use of
-[retry](#step-retries) configuration to set a timeout for the step.
-
-```yaml
-steps:
-# ...
-- uses: http
-  as: cat-facts
-  retry:
-    timeout: 10m
-  config:
-    method: GET
-    url: https://www.catfacts.net/api/
-    successExpression: response.status == 200
-    failureExpression: response.status == 404
-    outputs:
-    - name: status
-      fromExpression: response.status
-    - name: fact1
-      fromExpression: response.body.facts[0]
-    - name: fact2
-      fromExpression: response.body.facts[1]
-```
-
-Our request is considered:
-
-- Successful if the response status is `200`.
-- A failure if the response status is `404`.
-- Running if the response status is anything else. i.e. Any other status code
-  will result in a retry.
-
-</TabItem>
-
-<TabItem value="slack" label="Posting to Slack">
-
-This example is adapted from
-[Slack's own documentation](https://api.slack.com/tutorials/tracks/posting-messages-with-curl):
-
-```yaml
-vars:
-- name: slackChannel
-  value: C123456
-steps:
-# ...
-- uses: http
-  config:
-    method: POST
-    url: https://slack.com/api/chat.postMessage
-    headers:
-    - name: Authorization
-      value: Bearer ${{ secrets.slack.token }}
-    - name: Content-Type
-      value: application/json
-    body: |
-      ${{ quote({
-        "channel": vars.slackChannel,
-        "blocks": [
-          {
-            "type": "section",
-            "text": {
-              "type": "mrkdwn",
-              "text": "Hi I am a bot that can post *_fancy_* messages to any public channel."
-            }
-          }
-        ]
-      }) }}
-```
-
-</TabItem>
-
-</Tabs>
-
-#### `http` Outputs
-
-The `http` step only produces the outputs described by the `outputs` field of
-its configuration.
-
-### `compose-output`
-
-`compose-output` is a step that composes a new output from one or more existing
-outputs. This step can be useful when subsequent steps need to reference a
-combination of outputs from previous steps, or to allow a
-[`PromotionTask`](30-promotion-tasks.md) to provide easy access to outputs from
-the steps it contains.
-
-#### `compose-output` Configuration
-
-The `compose-output` step accepts an arbitrary set of key-value pairs, where the
-key is the name of the output to be created and the value is arbitrary and can
-be an [Expression Language](./20-expression-language.md) expression.
-
-#### `compose-output` Example
-
-```yaml
-vars:
-- name: repoURL
-  value: https://github.com/example/repo
-steps:
-- uses: git-open-pr
-  as: open-pr
-  config:
-    repoURL: ${{ vars.repoURL }}
-    createTargetBranch: true
-    sourceBranch: ${{ outputs.push.branch }}
-    targetBranch: stage/${{ ctx.stage }}
-- uses: compose-output
-  as: pr-link
-  config:
-    url: ${{ vars.repoURL }}/pull/${{ outputs['open-pr'].prNumber }}
-- uses: http
-  config:
-    method: POST
-    url: https://slack.com/api/chat.postMessage
-    headers:
-    - name: Authorization
-      value: Bearer ${{ secrets.slack.token }}
-    - name: Content-Type
-      value: application/json
-    body: |
-      ${{ quote({
-        "channel": "C123456",
-        "blocks": [
-          {
-            "type": "section",
-            "text": {
-              "type": "mrkdwn",
-              "text": "A new PR has been opened: ${{ outputs['pr-link'].url }}"
-            }
-          }
-        ]
-      }) }}
-```
-
-[expr-lang]: https://expr-lang.org/
diff --git a/docs/old-docs/35-references/20-expression-language.md b/docs/old-docs/35-references/20-expression-language.md
deleted file mode 100644
index 30fe4f2d..00000000
--- a/docs/old-docs/35-references/20-expression-language.md
+++ /dev/null
@@ -1,316 +0,0 @@
----
-sidebar_label: Expression Language Reference
-description: Learn about expression language support in promotion step configurations
----
-
-# Expression Language Reference
-
-The steps of a user-defined promotion process may take advantage of expressions
-in their configuration.
-
-:::info
-The documentation on this page assumes a general familiarity with the concept of
-Promotions and some knowledge of how a promotion process is defined as a
-sequence of discrete steps.
-
-For an overview of Promotions, refer to the
-[concepts doc](../concepts#promotions).
-
-For detailed coverage of individual promotion steps, refer to the
-[Promotion Steps Reference](./10-promotion-steps.md).
-:::
-
-## Syntax
-
-All steps in a user-defined promotion processes (i.e. those described by a
-`Stage` resource's `spec.promotionTemplate.spec.steps` field) support the use of
-[expr-lang](https://expr-lang.org/) as a means of dynamically resolving values
-in their configuration at promotion time.
-
-All expressions must be enclosed within the `${{` and `}}` delimiters. This is
-not universally true for all applications of expr-lang. Kargo selected these
-specific delimiters to mimic GitHub Actions expression syntax, which many users
-will already be familiar with.
-
-Basic example:
-
-```yaml
-config:
-  message: ${{ "Hello, world!" }}
-```
-
-The above example will be evaluated as the following:
-
-```yaml
-config:
-  message: Hello, world!
-```
-
-The
-[expr-lang language definition docs](https://expr-lang.org/docs/language-definition)
-provide a comprehensive overview of the language's syntax and capabilities, so
-this reference will continue to focus only on Kargo-specific extensions and
-usage.
-
-## Behavior
-
-Kargo will evaluate expressions just-in-time as each step of a promotion process
-is executed. It will _only_ evaluate expressions within _values_ of a
-configuration block and will _not_ evaluate expressions within keys. Expressions
-in values are evaluated recursively, so expressions may be nested any number of
-levels deep within a configuration block.
-
-### Validation
-
-Kargo parses configuration blocks _before_ evaluating expressions, so any
-configuration containing expressions _must_ be well-formed YAML even prior to
-evaluation. Further validation (e.g. for adherence to a step-specific schema) is
-performed only _after_ expressions are evaluated.
-
-### Types
-
-Due to the requirement that configuration blocks be well-formed YAML, all fields
-containing expressions must be `string`s. Internally, all expressions will also
-evaluate to `string`s, however, Kargo will attempt to coerce the results to
-other valid JSON types (YAML is a superset of JSON) including `object`, `array`,
-`number`, `boolean`, and `null` before concluding that the evaluated expression
-should continue to be treated as a `string`.
-
-This behavior should be unsurprising and perhaps even familiar to experienced
-YAML users, as YAML parsers behave in the same way. `42`, for example, is
-interpreted as a JSON `number` unless it is explicitly quoted (i.e. `"42"`) to
-specify that it should be interpreted as a `string`.
-
-In practice, this means care should be taken to use Kargo's built-in `quote()`
-function in cases where an evaluated expression may appear to be a `number` or
-`boolean`, for instance, but should be treated as a `string`.
-
-For example:
-
-```yaml
-config:
-  numField: ${{ 40 + 2 }} # Will be treated as a number
-  strField: ${{ quote(40 + 2) }} # Will be treated as a string
-```
-
-The above example will be evaluated to the following:
-
-```yaml
-config:
-  numField: 42
-  strField: "42"
-```
-
-## Pre-Defined Variables
-
-Kargo provides a number of pre-defined variables that are accessible within
-expressions. This section enumerates these variables, their structure, and use.
-
-| Name | Type | Description |
-|------|------|-------------|
-| `ctx` | `object` | `string` fields `project`, `stage`, and `promotion` provide convenient access to details of a `Promotion`. |
-| `outputs` | `object` | A map of output from previous promotion steps indexed by step aliases. |
-| `secrets` | `object` | A map of maps indexed by the names of all Kubernetes `Secret`s in the `Promotion`'s `Project` and the keys within the `Data` block of each. |
-| `vars` | `object` | A user-defined map of variable names to static values of any type. The map is derived from a `Promotion`'s `spec.promotionTemplate.spec.vars` field. Variable names must observe standard Go variable-naming rules. Variables values may, themselves, be defined using an expression. `vars` (contains previously defined variables) and `ctx` are available to expressions defining the values of variables, however, `outputs` and `secrets` are not. |
-
-:::info
-Expect other useful variables to be added in the future.
-:::
-
-The following example promotion process clones a repository and checks out
-two branches to different directories, uses Kustomize with source from one
-branch to render some Kubernetes manifests that it commits to the other branch,
-and pushes back to the repository. These steps make extensive use of the
-pre-defined variables `ctx`, `outputs`, and `vars`.
-
-```yaml
-promotionTemplate:
-  spec:
-    vars:
-    - name: gitRepo
-      value: https://github.com/example/repo.git
-    - name: srcPath
-      value: ./src
-    - name: outPath
-      value: ./out
-    - name: targetBranch
-      value: stage/${{ ctx.stage }}
-    steps:
-    - uses: git-clone
-      config:
-        repoURL: ${{ vars.gitRepo }}
-        checkout:
-        - fromFreight: true
-          path: ${{ vars.srcPath }}
-        - branch: ${{ vars.targetBranch }}
-          create: true
-          path: ${{ vars.outPath }}
-    - uses: git-clear
-      config:
-        path: ${{ vars.outPath }}
-    - uses: kustomize-set-image
-      as: update-image
-      config:
-        path: ${{ vars.srcPath }}/base
-        images:
-        - image: public.ecr.aws/nginx/nginx
-    - uses: kustomize-build
-      config:
-        path: ${{ vars.srcPath }}/stages/${{ ctx.stage }}
-        outPath: ${{ vars.outPath }}
-    - uses: git-commit
-      as: commit
-      config:
-        path: ${{ vars.outPath }}
-        message: ${{ outputs['update-image'].commitMessage }}
-    - uses: git-push
-      config:
-        path: ${{ vars.outPath }}
-        targetBranch: ${{ vars.targetBranch }}
-    - uses: argocd-update
-      config:
-        apps:
-        - name: example-${{ ctx.stage }}
-          sources:
-          - repoURL: ${{ vars.gitRepo }}
-            desiredRevision: ${{ outputs.commit.commit }}
-```
-
-:::info
-Since the usage of expressions and pre-defined variables effectively
-parameterizes the promotion process, the same promotion process can be reused in
-other `Projects` or `Stages` with few, if any, modifications (other than the
-definition of the static variables).
-
-At present, such re-use can be achieved only through manual copy/paste, but
-support for a new, top-level `PromotionTemplate` resource type is planned for an
-upcoming release.
-:::
-
-## Functions
-
-Several functions are built-in to Kargo's expression language. This section
-describes each of them.
-
-### `quote()`
-
-The `quote()` function takes a single argument of any type and returns a string
-representation. This is useful for scenarios where an expression evaluates to a
-non-`string` JSON type, but you wish to treat it as a `string` regardless.
-
-Example:
-
-```yaml
-config:
-  numField: ${{ 40 + 2 }} # Will be treated as a number
-  strField: ${{ quote(40 + 2) }} # Will be treated as a string
-```
-
-### `warehouse()`
-
-The `warehouse()` function takes a single argument of type `string`, which is the
-name of a `Warehouse` resource in the same `Project` as the `Promotion` being
-executed. It returns a `FreightOrigin` object representing that `Warehouse`
-with the following fields:
-
-| Field | Description |
-|-------|-------------|
-| `Kind` | The kind of the `FreightOrigin`. Always equals `Warehouse` for this function. |
-| `Name` | The name of the `Warehouse` resource. |
-
-The `FreightOrigin` object can be used as an optional argument to the
-`commitFrom()`, `imageFrom()`, or `chartFrom()` functions to disambiguate the
-desired source of an artifact when necessary.
-
-See the next sections for examples.
-
-### `commitFrom()`
-
-The `commitFrom()` function takes the URL of a Git repository as its first
-argument and returns a corresponding `GitCommit` object from the `Promotion`'s
-`FreightCollection` with the following fields:
-
-| Field | Description |
-|-------|-------------|
-| `RepoURL` | The URL of the Git repository the commit originates from. |
-| `ID`      | The ID of the Git commit. |
-| `Branch`  | The branch of the repository where this commit was found. Only present if the `Warehouse`'s Git subscription is configured to track branches. |
-| `Tag`     | The tag of the repository where this commit was found. Only present if the `Warehouse`'s Git subscription is configured to track tags. |
-| `Message` | The first line of the commit message (up to 80 characters). |
-| `Author` | The name and email address of the commit author. |
-| `Committer` | The name and email address of the committer. |
-
-In the event that a `Stage` requests `Freight` from multiple origins
-(`Warehouse`s) and more than one of those can provide a `GitCommit` object from
-the specified repository, a `FreightOrigin` may be used as a second argument to
-disambiguate the desired source.
-
-Example:
-
-```yaml
-config:
-  commitID: ${{ commitFrom("https://github.com/example/repo.git", warehouse("my-warehouse")).ID }}
-```
-
-### `imageFrom()`
-
-The `imageFrom()` function takes the URL of a container image repository as its
-first argument and returns a corresponding `Image` object from the `Promotion`'s
-`FreightCollection` with the following fields:
-
-| Field | Description |
-|-------|-------------|
-| `RepoURL` | The URL of the container image repository the image originates from. |
-| `GitRepoURL` | (Deprecated as of version 1.5, will be removed in version 1.7) The URL of the Git repository which contains the source code for the image. Only present if Kargo was able to infer it from the URL. |
-| `Tag` | The tag of the image. |
-| `Digest` | The digest of the image. |
-
-In the event that a `Stage` requests `Freight` from multiple origins
-(`Warehouse`s) and more than one of those can provide an `Image` object from the
-specified repository, a `FreightOrigin` may be used as a second argument to
-disambiguate the desired source.
-
-Example:
-
-```yaml
-config:
-  imageTag: ${{ imageFrom("public.ecr.aws/nginx/nginx", warehouse("my-warehouse")).Tag }}
-```
-
-### `chartFrom()`
-
-The `chartFrom()` function takes the URL of a Helm chart repository as its first
-argument and returns a corresponding `Chart` object from the `Promotion`'s
-`FreightCollection` with the following fields:
-
-| Field | Description |
-|-------|-------------|
-| `RepoURL` | The URL of the Helm chart repository the chart originates from. For HTTP/S repositories, this is the URL of the repository. For OCI repositories, this is the URL of the container image repository including the chart's name. |
-| `Name` | The name of the Helm chart. Only present for HTTP/S repositories. |
-| `Version` | The version of the Helm chart. |
-
-For Helm charts stored in OCI registries, the URL should be the full path to the
-repository within that registry.
-
-For Helm charts stored in classic (http/s) repositories, which can store
-multiple different charts within a single repository, a second argument should
-be used to specify the name of the chart within the repository.
-
-In the event that a `Stage` requests `Freight` from multiple origins
-(`Warehouse`s) and more than one of those can provide a `Chart` object from the
-specified repository, a `FreightOrigin` may be used as a final argument to
-disambiguate the desired source.
-
-OCI registry example:
-
-```yaml
-config:
-  chartVersion: ${{ chartFrom("oci://example.com/my-chart", warehouse("my-warehouse")).Version }}
-```
-
-Classic repository example:
-
-```yaml
-config:
-  chartVersion: ${{ chartFrom("https://example.com/charts", "my-chart", warehouse("my-warehouse")).Version }}
-```
diff --git a/docs/old-docs/35-references/30-promotion-tasks.md b/docs/old-docs/35-references/30-promotion-tasks.md
deleted file mode 100644
index 7a9cabb6..00000000
--- a/docs/old-docs/35-references/30-promotion-tasks.md
+++ /dev/null
@@ -1,257 +0,0 @@
----
-sidebar_label: Promotion Tasks Reference
-description: Learn about Kargo's promotion tasks that can define reusable promotion steps.
----
-
-# Promotion Tasks Reference
-
-`PromotionTask`s allow you to define a set of
-[Promotion Steps](./10-promotion-steps.md) on a project or global
-(`ClusterPromotionTask`) level that can be reused across multiple
-[Promotion Templates](../30-how-to-guides/14-working-with-stages.md#promotion-templates).
-
-## Defining a Promotion Task
-
-A simple `PromotionTask` is defined as follows:
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: PromotionTask
-metadata:
-  name: open-pr-and-wait
-  namespace: kargo-demo
-spec:
-  vars:
-  - name: repoURL
-  - name: sourceBranch
-  - name: targetBranch
-  steps:
-  - uses: git-open-pr
-    as: open-pr
-    config:
-      repoURL: ${{ vars.repoURL }}
-      createTargetBranch: true
-      sourceBranch: ${{ vars.sourceBranch }}
-      targetBranch: ${{ vars.targetBranch }}
-  - uses: git-wait-for-pr
-    as: wait-for-pr
-    config:
-      repoURL: ${{ vars.repoURL }}
-      prNumber: ${{ task.outputs['open-pr'].prNumber }}
-  - uses: compose-output
-    as: output
-    config:
-      mergeCommit: ${{ task.outputs['wait-for-pr'].commit }}
-```
-
-### Promotion Task Variables
-
-The `spec.vars` section of a `PromotionTask` defines the input variables that
-it expects when it is used in a Promotion Template. Each variable requires a
-`name` and optionally a default `value`. When the `value` is not provided, the
-variable is considered required and must be provided when the `PromotionTask`
-is used in a Promotion Template.
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: PromotionTask
-# ...omitted for brevity
-spec:
-  vars:
-  # This variable is required
-  - name: repoURL
-  # This variable is optional and defaults to "main"
-  - name: targetBranch
-    value: main
-```
-
-Variables can be referenced in the `PromotionTask` steps' configuration using
-the `${{ vars.<variable-name> }}` syntax. For example, the `repoURL` variable
-is referenced as `${{ vars.repoURL }}`.
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: PromotionTask
-# ...omitted for brevity
-spec:
-  vars:
-  - name: repoURL
-  - name: sourceBranch
-  - name: targetBranch
-    value: main
-  steps:
-  - uses: git-open-pr
-    as: open-pr
-    config:
-      repoURL: ${{ vars.repoURL }}
-      createTargetBranch: true
-      sourceBranch: ${{ vars.sourceBranch }}
-      targetBranch: ${{ vars.targetBranch }}
-```
-
-When the `PromotionTask` is used in a Promotion Template, the input variables
-must be provided as `vars` of the step referencing the `PromotionTask`.
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Stage
-# ...omitted for brevity
-spec:
-  promotionTemplate:
-    spec:
-      steps:
-      - task:
-          name: my-promotion-task
-          vars:
-          - name: repoURL
-            value: https://github.com/example/repository.git
-          - name: sourceBranch
-            value: feature-branch
-```
-
-When the Promotion Template defines a
-[`vars` section](../30-how-to-guides/14-working-with-stages.md#promotion-templates)
-the variables are inherited by the `PromotionTask` and do not require redefinition
-unless they need to be overridden.
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Stage
-# ...omitted for brevity
-spec:
-  promotionTemplate:
-    spec:
-      vars:
-      - name: repoURL
-        value: https://github.com/example/repository.git
-      steps:
-        - task:
-            name: my-promotion-task
-            vars:
-            - name: sourceBranch
-              value: feature-branch
-```
-
-### Promotion Task Steps
-
-The `spec.steps` section of a `PromotionTask` define the sequence of steps that
-are inflated when a `Promotion` is created from a Promotion Template that
-references the task. Each step works the as a [regular step](10-promotion-steps.md)
-in a Promotion Template, except that references to other tasks are not allowed.
-
-#### Promotion Task Context
-
-The steps of a `PromotionTask` have access to an additional `task`
-[pre-defined variable](20-expression-language.md#pre-defined-variables) that
-provides access to the `outputs` of the previous steps. The `task.outputs`
-property is a map of step aliases to their outputs.
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: PromotionTask
-# ...omitted for brevity
-spec:
-  steps:
-  - uses: git-open-pr
-    as: open-pr
-    config:
-      repoURL: ${{ vars.repoURL }}
-      createTargetBranch: true
-      sourceBranch: ${{ vars.sourceBranch }}
-      targetBranch: ${{ vars.targetBranch }}
-  - uses: git-wait-for-pr
-    as: wait-for-pr
-    config:
-      repoURL: ${{ vars.repoURL }}
-      prNumber: ${{ task.outputs['open-pr'].prNumber }}
-```
-
-### Promotion Task Outputs
-
-Outputs of a `PromotionTask` can be made more accessible by defining them using
-a [`compose-output` step](10-promotion-steps.md#compose-output). The outputs are
-then made available under the alias defined in the
-[`as` field](10-promotion-steps.md#step-aliases) of the step referencing the
-`PromotionTask`.
-
-```yaml
----
-apiVersion: kargo.akuity.io/v1alpha1
-kind: PromotionTask
-metadata:
-  name: open-pr-and-wait
-  namespace: kargo-demo
-spec:
-    # ...omitted for brevity
-    - uses: compose-output
-      as: output
-      config:
-        mergeCommit: ${{ task.outputs['wait-for-pr'].commit }}
----
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Stage
-# ...omitted for brevity
-spec:
-  promotionTemplate:
-    spec:
-      steps:
-      - task:
-          name: open-pr-and-wait
-          as: pr
-          # ...additional configuration
-      - uses: http
-        config:
-          method: POST
-          url: https://slack.com/api/chat.postMessage
-          headers:
-          - name: Authorization
-            value: Bearer ${{ secrets.slack.token }}
-          - name: Content-Type
-            value: application/json
-          body: |
-            ${{ quote({
-              "channel": "C123456",
-              "blocks": [
-                {
-                  "type": "section",
-                  "text": {
-                    "type": "mrkdwn",
-                    "text": "A new commit was merged: ${{ outputs.pr.mergeCommit }}"
-                  }
-                }
-              ]
-            }) }}
-```
-
-### Cluster Promotion Task
-
-A `ClusterPromotionTask` is a `PromotionTask` that is available to all
-[projects](../30-how-to-guides/11-working-with-projects.md)
-in the cluster. The `ClusterPromotionTask` is defined the same way as a
-`PromotionTask`, but without the `namespace` field in the metadata.
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: ClusterPromotionTask
-metadata:
-  name: open-pr-and-wait
-spec:
-  # ...equivalent to a PromotionTask
-```
-
-A `ClusterPromotionTask` can be used in a Promotion Template the same way as a
-`PromotionTask`, but requires the additional specification of the `kind` field
-in the step referencing the task.
-
-```yaml
-apiVersion: kargo.akuity.io/v1alpha1
-kind: Stage
-# ...omitted for brevity
-spec:
-  promotionTemplate:
-    spec:
-      steps:
-      - kind: ClusterPromotionTask
-        name: open-pr-and-wait
-      # ...additional configuration
-```
diff --git a/docs/old-docs/35-references/_category_.json b/docs/old-docs/35-references/_category_.json
deleted file mode 100644
index bf7735e4..00000000
--- a/docs/old-docs/35-references/_category_.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "label": "Reference Docs",
-  "link":  null
-}
diff --git a/docs/old-docs/40-contributor-guide/10-hacking-on-kargo.md b/docs/old-docs/40-contributor-guide/10-hacking-on-kargo.md
deleted file mode 100644
index 59ae98f8..00000000
--- a/docs/old-docs/40-contributor-guide/10-hacking-on-kargo.md
+++ /dev/null
@@ -1,467 +0,0 @@
----
-description: Learn how to set up a development environment to begin contributing to Kargo
-sidebar_label: Hacking on Kargo
----
-
-# Hacking on Kargo
-
-Kargo is implemented in Go. For maximum productivity in your text editor or IDE,
-it is recommended that you have installed the latest stable releases of Go and
-applicable editor/IDE extensions, however, this is not strictly required to be
-successful.
-
-## Running Tests
-
-In order to minimize the setup required to apply small changes and to reduce the
-incidence of tests passing locally, but failing during the continuous
-integration process due to environmental differences, we've made it easy to
-execute tests within a container that is maximally similar to those used in CI.
-
-To take advantage of this, you only need `make` and
-[Docker](https://docs.docker.com/engine/install/) (or a Docker-compatible 
-container-runtime).
-
-To run all unit tests:
-
-```shell
-make hack-test-unit
-```
-
-:::info
-If you wish to opt-out of executing the tests within a container (for
-performance reasons, perhaps), drop the `hack-` prefix from the target:
-
-```shell
-make test-unit
-```
-
-This will require Go to be installed locally.
-:::
-
-## Running Linters
-
-It is also possible to execute a variety of different linters that perform
-static code analysis, detect code hygiene issues, assert adherence to project
-standards, etc. As with unit tests, we've made it easy to execute linters within
-a container that is maximally similar to those used in CI.
-
-To lint Go code only:
-
-```shell
-make hack-lint-go
-```
-
-To lint generated protobuf definitions only:
-
-```shell
-make hack-lint-proto
-```
-
-To lint Helm charts only:
-
-```shell
-make hack-lint-charts
-```
-
-To run _all_ linters with one command:
-
-```shell
-make hack-lint
-```
-
-:::info
-If you wish to opt-out of executing any or all linters within a container (for
-performance reasons, perhaps), drop the `hack-` prefix from the desired target.
-
-This will require quite a variety of tools to be installed locally, so we do not
-recommend this if you can avoid it.
-:::
-
-## Executing Code Generation
-
-Anytime the contents of the `api/` directory have been modified, a code
-generation process must be manually executed. As with tests and linters, this
-process is easy to execute within a container, which eliminates the need to
-install various tools or specific versions thereof:
-
-```shell
-make hack-codegen
-```
-
-:::info
-If you wish to opt-out of executing code-generation within a container (for
-performance reasons, perhaps), drop the `hack-` prefix from the target:
-
-```shell
-make codegen
-```
-
-This will require quite a variety of tools to be installed locally, so we do not
-recommend this if you can avoid it.
-:::
-
-## Building the Image
-
-To build source into a Docker image that will be tagged as `kargo:dev`,
-execute the following:
-
-```shell
-make hack-build
-```
-
-:::info
-There is seldom a need to do this, as the next section will cover a better
-option for rapidly building and deploying Kargo from source.
-:::
-
-:::tip
-The [Docker buildx](https://github.com/docker/buildx) machine required by the
-build process has to be created with the `--driver-opt network=host` option to
-allow it to access the (temporary) local image registry used for the base image.
-
-If you encounter an error during the build process (e.g. `failed to resolve
-source metadata for localhost:5001/kargo-base:latest-arm64` or `granting
-entitlement network.host is not allowed by build daemon configuration`), you
-may need to (re)create the machine using `docker buildx create` with this
-option set.
-:::
-
-## Iterating Quickly
-
-This section focuses on the best approaches for gaining rapid feedback on
-changes you make to Kargo's code base.
-
-The fastest path to learning whether changes you have applied work as desired is
-to execute unit tests as described in previous sections. If the changes you are
-applying are complex, it can also be advantageous to exercise them, end-to-end,
-as a user would. Because Kargo is dependent on a Kubernetes cluster, this raises
-the question of how Kargo can not only be built from source, but also deployed
-to a live Kubernetes cluster efficiently enough to enable a tight feedback loop
-as you continue iterating on your changes.
-
-The remainder of this section covers the approaches we recommend for enabling
-this.
-
-:::info
-We may eventually provide convenient methods of running _some_ Kargo components
-as native processes.
-:::
-
-1. Launch or re-use an existing local Kubernetes cluster.
-
-    Any of the following options are viable:
-
-    <Tabs groupId="cluster-start">
-    <TabItem value="docker-desktop" label="Docker Desktop">
-
-    If you are a
-    [Docker Desktop](https://www.docker.com/products/docker-desktop/)
-    user, you can follow
-    [these instructions](https://docs.docker.com/desktop/kubernetes/) to enable
-    its built-in Kubernetes support.
-
-    :::info
-    A specific benefit of this option is that nothing special is required in
-    terms of creating a local image registry connected to the cluster.
-    Additionally, this approach requires no specific port-forwarding rules to be
-    defined.
-    :::
-    :::info
-    Although this is one of the fastest paths to a local Kubernetes cluster, be
-    aware that Docker Desktop supports only a _single_ Kubernetes cluster. If
-    that cluster reaches a state you are dissatisfied with, resetting it will
-    remove not just Kargo-related resources, but _all_ your workloads and data.
-    :::
-
-    To install Kargo's prerequisites, you will need
-    [Helm](https://helm.sh/docs/intro/install/) installed first, and can then
-    execute a convenient `make` target:
-
-    ```shell
-    make hack-install-prereqs
-    ```
-   
-    </TabItem>
-    <TabItem value="orbstack" label="OrbStack">
-
-    [OrbStack](https://orbstack.dev/) is a fast, lightweight, drop-in replacement
-    for Docker Desktop for macOS only. You can follow
-    [these instructions](https://docs.docker.com/desktop/kubernetes/) to enable
-    its built-in Kubernetes support.
-
-    :::info
-    A specific benefit of this option is that nothing special is required in
-    terms of creating a local image registry connected to the cluster.
-    Additionally, this approach requires no specific port-forwarding rules to be
-    defined.
-    :::
-    :::info
-    Although this is one of the fastest paths to a local Kubernetes cluster, be
-    aware that OrbStack supports only a _single_ Kubernetes cluster. If
-    that cluster reaches a state you are dissatisfied with, resetting it will
-    remove not just Kargo-related resources, but _all_ your workloads and data.
-    :::
-
-    To install Kargo's prerequisites, you will need
-    [Helm](https://helm.sh/docs/intro/install/) installed first, and can then
-    execute a convenient `make` target:
-
-    ```shell
-    make hack-install-prereqs
-    ```
-
-    </TabItem>
-    <TabItem value="kind" label="kind">
-
-    If you have any Docker-compatible container runtime installed (including
-    native Docker, Docker Desktop, or OrbStack), you can easily launch a
-    disposable cluster to facilitate Kargo development using
-    [kind](https://kind.sigs.k8s.io/#installation-and-usage).
-
-    This option also requires
-    [ctlptl](https://github.com/tilt-dev/ctlptl#how-do-i-install-it) and
-    [Helm](https://helm.sh/docs/intro/install/) to be installed.
-
-    The following `make` target will launch a kind cluster with a local image
-    registry wired into it, various port-forwarding rules pre-configured, and
-    Kargo's prerequisites installed:
-
-    ```shell
-    make hack-kind-up
-    ```
-
-    :::info
-    While this option is a bit more complex than using Docker Desktop or OrbStack
-    directly, it offers the advantage of being fully-disposable. If your cluster
-    reaches a state you are dissatisfied with, you can simply destroy it and
-    launch a new one.
-    :::
-
-    </TabItem>
-    <TabItem value="k3d" label="k3d">
-
-    If you have any Docker-compatible container runtime installed (including
-    native Docker, Docker Desktop, or OrbStack), you can easily launch a
-    disposable cluster to facilitate Kargo development using
-    [k3d](https://k3d.io).
-
-    This option also requires
-    [ctlptl](https://github.com/tilt-dev/ctlptl#how-do-i-install-it) and
-    [Helm](https://helm.sh/docs/intro/install/) to be installed.
-
-    The following `make` target will launch a kind cluster with a local image
-    registry wired into it, various port-forwarding rules pre-configured, and
-    Kargo's prerequisites installed:
-
-    ```shell
-    make hack-k3d-up
-    ```
-
-    :::info
-    While this option is a bit more complex than using Docker Desktop or OrbStack
-    directly, it offers the advantage of being fully-disposable. If your cluster
-    reaches a state you are dissatisfied with, you can simply destroy it and
-    launch a new one.
-    :::
-
-    </TabItem>
-    </Tabs>
-
-    Whichever approach you choose, your cluster will end up with recent, stable
-    versions of [cert-manager](https://cert-manager.io/) and 
-    [Argo CD](https://argoproj.github.io/cd/) installed.
-
-    :::info
-    The Argo CD dashboard will be exposed at
-    [localhost:30080](https://localhost:30080).
-
-    The username and password are both `admin`.
-
-    You may safely ignore any certificate warnings.
-    :::
-
-1. Build and deploy Kargo from source:
-
-    [Tilt](https://docs.tilt.dev/#macoslinux) is a convenient tool that builds
-    container images from source and seamlessly deploys them to a local
-    Kubernetes cluster. More importantly, it enables developers to rapidly
-    rebuild and replace running components with the click of a button.
-
-    :::warning
-    If using OrbStack, be advised it is only compatible with Tilt as of Tilt
-    v0.33.6. Please use that version or greater.
-    :::
-
-    ```shell
-    tilt up
-    ```
-
-    Tilt will also launch a web-based UI running at
-    [http://localhost:10350](http://localhost:10350). Visit this in your web
-    browser to view the build and deployment status of each Kargo component as
-    well as the logs from each component.
-
-    :::info
-    Tilt is often configured to watch files and automatically rebuild and replace
-    running components when their source code is changed. This is deliberately
-    disabled for Kargo since the Docker image takes long enough to build that
-    it’s better to conserve system resources by only rebuilding when you choose.
-    The web UI makes it easy to identify components whose source has been
-    altered. They can be rebuilt and replaced with a single click.
-    :::
-
-1. If necessary, build the CLI from source:
-
-    ```shell
-    make hack-build-cli
-    ```
-
-    This will produce an executable at `bin/kargo-<os>-<arch>`.
-
-    You can log in using:
-
-    ```shell
-    bin/kargo-<os>-<arch> login http://localhost:30081 \
-      --admin \
-      --password admin \
-      --insecure-skip-tls-verify
-    ```
-
-1. If necessary, access the Kargo UI at
-   [localhost:30082](http://localhost:30082).
-
-    The admin account password is `admin`.
-
-    You may safely ignore any certificate warnings.
-
-1. When you are done with Tilt, interrupt the running `tilt up` process with
-   `ctrl + c`. Components _will remain running in the cluster_, but Tilt will no
-   longer be in control. If Tilt is restarted later, it will retake control of
-   the already-running components.
-
-    If you wish to undeploy everything Tilt has deployed for you, use `tilt
-    down`.
-
-1. Clean up your local Kubernetes cluster.
-
-    <Tabs groupId="cluster-start">
-    <TabItem value="docker-desktop" label="Docker Desktop">
-
-    Docker Desktop supports only a _single_ Kubernetes cluster. If you are
-    comfortable deleting not just Kargo-related resources, but _all_ your
-    workloads and data, the cluster can be reset from the Docker Desktop
-    Dashboard.
-
-    If, instead, you wish to preserve non-Kargo-related workloads and data, you
-    will need to manually uninstall Kargo's prerequisites:
-
-    ```
-    make hack-uninstall-prereqs
-    ```
-   
-    </TabItem>
-    <TabItem value="orbstack" label="OrbStack">
-
-    OrbStack supports only a _single_ Kubernetes cluster. If you are
-    comfortable deleting not just Kargo-related resources, but _all_ your
-    workloads and data, you can destroy the cluster with:
-
-    ```shell
-    orb delete k8s
-    ```
-
-    If, instead, you wish to preserve non-Kargo-related workloads and data, you
-    will need to manually uninstall Kargo's prerequisites:
-
-    ```
-    make hack-uninstall-prereqs
-    ```
-
-    </TabItem>
-    <TabItem value="kind" label="kind">
-
-    To destroy the cluster, use:
-
-    ```shell
-    make hack-kind-down
-    ```
-
-    :::info
-    This command deliberately leaves your local image registry running so that if
-    you resume work later, you are doing so with a local registry that’s already
-    primed with most layers of Kargo’s image.
-
-    If you wish to stop the registry, use:
-
-    ```shell
-    docker stop kargo-dev-registry
-    ```
-
-    To destroy it, use:
-
-    ```shell
-    docker rm -f kargo-dev-registry
-    ```
-    :::
-
-    </TabItem>
-    <TabItem value="k3d" label="k3d">
-
-    To destroy the cluster, use:
-
-    ```shell
-    make hack-k3d-down
-    ```
-
-    :::info
-    This command deliberately leaves your local image registry running so that if
-    you resume work later, you are doing so with a local registry that’s already
-    primed with most layers of Kargo’s image.
-
-    If you wish to stop the registry, use:
-
-    ```shell
-    docker stop kargo-dev-registry
-    ```
-
-    To destroy it, use:
-
-    ```shell
-    docker rm -f kargo-dev-registry
-    ```
-    :::
-
-    </TabItem>
-    </Tabs>
-
-## Contributing to Documentation
-
-Contributors should ensure that their changes are accompanied by relevant documentation
-updates. This helps maintain the project's sustainability. Pull requests with
-corresponding documentation updates are more likely to be merged faster.
-
-To make this process smoother, you can refer to [Docusaurus](https://docusaurus.io/docs)
-for guidance on writing and maintaining docs effectively.
-
-### Previewing Doc Changes Locally
-
-After making your changes, preview the documentation locally to ensure everything renders
-correctly. You can either run it in a container or natively on your system.
-
-To build and serve the docs inside a container:
-
-```shell
-make hack-serve-docs
-```
-
-:::info
-If you wish to opt-out of executing code-generation within a container (for
-performance reasons, perhaps), drop the `hack-` prefix from the target to run the docs natively on your system:
-
-```shell
-make serve-docs
-```
-
-This will require quite a variety of tools to be installed locally, so we do not
-recommend this if you can avoid it.
-:::
diff --git a/docs/old-docs/40-contributor-guide/15-release-procedures.md b/docs/old-docs/40-contributor-guide/15-release-procedures.md
deleted file mode 100644
index fe017ed7..00000000
--- a/docs/old-docs/40-contributor-guide/15-release-procedures.md
+++ /dev/null
@@ -1,170 +0,0 @@
----
-description: Steps required by Kargo maintainers to orchestrate a minor or major release of Kargo.
-sidebar_label: Release Procedures
----
-
-# Release Procedures
-
-This document outlines the few manual steps that Kargo maintainers must follow
-to orchestrate a minor or major release of Kargo.
-
-## Conventions
-
-Throughout this document, we will use:
-
-* `M` and `m` to denote the major and minor version numbers of the upcoming
-  release (i.e. the one being performed), respectively. 
-
-* `M-1` and `m-1` to denote the _previous_ major and minor version numbers,
-  respectively.
-
-* `M+1` and `m+1` to denote the _next_ major and minor version numbers,
-  respectively.
-
-* `L` to denote the latest minor version of the `M-1` line.
-
-## Timeline
-
-The steps outlined below should be started on the Monday preceding the expected
-release date -- which is always a Friday.
-
-## Steps
-
-1. Open a PR [similar to this one](https://github.com/akuity/kargo/pull/1932)
-   to revise the roadmap.
-
-    * The roadmap should be updated to reflect that work for the `vM.m.0`
-      release is complete.
-    * Planned work that was not completed should be moved to a future release.
-    * The next release (`vM.m+1.0` or `vM+1.0.0`) should be updated to reflect
-      that work is in-progress, along with the expected release date.
-
-      :::note
-      "Edge" documentation at
-      [main.kargo.akuity.io](https://main.docs.kargo.io) is continuously
-      published from the `main` branch and production documentation at
-      [kargo.akuity.io](https://docs.kargo.io) is continuously published from
-      the previous release branch (`release-M.m-1` or `release-M-1.L`). There
-      are two consequences of this:
-
-      * The production documentation will _not_ immediately reflect changes made
-        to the `main` branch, nor will it reflect changes made to the release branch
-        for the upcoming release. (Which does not exist yet. See next step.)
-
-      * This step should ideally be performed _prior_ to the creation of the
-        release branch (see next step) in order to avoid the need for two separate
-        PRs to update both branches.
-      :::
-
-1. Create a release branch of the form `release-M.m`.
-
-    This can be done locally by a maintainer. Presuming `upstream` is a remote
-    pointing to the main Kargo repository:
-
-      ```shell
-      git checkout main
-      git pull upstream main
-      git checkout -b release-M.m
-      git push upstream release-M.m
-      ```
-
-    :::note
-    After the creation of this branch, anything merged to `main` is excluded
-    from the upcoming release unless explicitly cherry-picked into the
-    `release-M.m branch`. As such, this step should ideally be performed
-    _after_ the majority of work for the upcoming release is complete.
-
-    In some cases, this may be performed early to:
-
-      * Un-block work on the next release.
-      * Facilitate the creation of a release candidate for use by non-engineers
-        while work on the upcoming release continues.
-    :::
-
-1. Merge any release-specific upgrade logic into the `release-M.m` branch.
-
-    :::info
-    Pre-`v1.0.0`, we are making a best effort to automatically compensate for
-    breaking changes between minor releases for users upgrading _directly_ from
-    any release in the `v0.m-1` line. This means release-specific upgrade
-    logic does not need to be merged into `main`.
-    :::
-
-1. Open a PR [similar to this one](https://github.com/akuity/kargo/pull/1925)
-   against the previous release branch (`release-M.m-1` or `release-M-1.L`) to
-   lock production documentation (e.g. for download and installation procedures)
-   into permanently reflecting the latest stable release.
-
-    :::note
-    Production documentation is continuously published from the previous
-    release branch, so this step is necessary to ensure that the production
-    documentation is not inadvertently broken by any subsequent steps.
-
-    This step will also ensure that when the current production documentation
-    is archived, it will reflect the latest release to which that documentation
-    was applicable.
-    :::
-
-1. Cut `vM.m.0-rc.1` from the Kargo
-   [release page](https://github.com/akuity/kargo/releases/new).
-
-    * The release process itself is fully-automated.
-    * Be certain to reference the head of the `release-M.m` branch and _not_ `main`.
-    * Be sure to check the __"Set as a pre-release"__ box.
-    * Wait for the
-      [automated release process](https://github.com/akuity/kargo/actions/workflows/release.yaml)
-      to complete.
-
-1. Open a PR [like this one](https://github.com/akuity/kargo/pull/1926) against
-   `main` to make the edge documentation (e.g. for download and installation
-   procedures) reflect the recently built release candidate.
-
-    :::info
-    The edge documentation is continuously published from the `main` branch, so
-    this step makes it easy for non-engineers to test the release candidate by
-    adhering to instructions in the edge documentation, without any need to
-    compensate for the release candidate not being counted as "latest" on
-    account of being a pre-release.
-    :::
-
-1. Alert non-engineer stakeholders to the availability of the release candidate.
-
-1. Bug fixes and last minute features should be merged to `main` and backported
-   to the `release-M.m` (in bulk, when possible).
-
-1. Repeat steps 5-8 as necessary until the release candidate is deemed stable
-   by relevant stakeholders.
-
-1. Draft release notes for the upcoming release.
-
-    :::info
-    This can be done concurrently with the previous steps.
-
-    Some stakeholders may desire early access to these notes to inform blog
-    posts, marketing materials, etc.
-    :::
-
-1. Cut `vM.m.0` from the Kargo
-   [release page](https://github.com/akuity/kargo/releases/new).
-
-    * Be certain to reference the head of the `release-M.m` branch and _not_ `main`.
-    * Be certain to include the final draft of the release notes.
-    * Be sure to check the __"Set as the latest release"__ box.
-    * Wait for the
-      [automated release process](https://github.com/akuity/kargo/actions/workflows/release.yaml)
-      to complete.
-
-1. Mark the release branch (`release-M.m`) as the __"Production branch"__
-   [in Netlify](https://app.netlify.com/sites/docs-kargo-akuity-io/configuration/deploys#branches-and-deploy-contexts).
-
-    * Also add the previous release branch (`release-M.m-1` or
-      `release-M-1.L`) to __"Branch deploys"__.
-    * After changing the __"Production branch"__, it will be necessary to
-      [manually trigger a deployment](https://app.netlify.com/sites/docs-kargo-io/deploys)
-      of the production documentation.
-
-1. Open a PR to revert the changes from step 6.
-
-1. Inform relevant stakeholders that the release is complete.
-
-1. 🎉 Celebrate!
diff --git a/docs/old-docs/40-contributor-guide/20-signing-commits.md b/docs/old-docs/40-contributor-guide/20-signing-commits.md
deleted file mode 100644
index d9f58093..00000000
--- a/docs/old-docs/40-contributor-guide/20-signing-commits.md
+++ /dev/null
@@ -1,73 +0,0 @@
----
-description: Find out how to sign commits when contributing to Kargo
-sidebar_label: Signing Commits
----
-
-# Signing Commits
-
-All commits merged into Kargo's main branch MUST bear a DCO (Developer
-Certificate of Origin) sign-off. This is a line placed at the end of a commit
-message containing a contributor’s “signature.” In adding this, the contributor
-certifies that they have the right to contribute the material in question.
-
-Here are the steps to sign your work:
-
-1. Verify the contribution in your commit complies with the
-   [terms of the DCO](https://developercertificate.org/).
-
-1. Add a line like the following to your commit message:
-
-   ```
-   Signed-off-by: Joe Smith <joe.smith@example.com>
-   ```
-
-   You MUST use your legal name -- handles or other pseudonyms are not
-   permitted.
-
-   While you could manually add DCO sign-off to every commit, there is an easier
-   way:
-
-   1. Configure your git client appropriately. This is one-time setup.
-
-      ```shell
-      git config user.name <legal name>
-      git config user.email <email address you use for GitHub>
-      ```
-
-      If you work on multiple projects that require a DCO sign-off, you can
-      configure your git client to use these settings globally instead of only
-      for Kargo:
-
-      ```shell
-      git config --global user.name <legal name>
-      git config --global user.email <email address you use for GitHub>
-      ```
-
-   1. Use the --signoff or -s (lowercase) flag when making each commit. For
-      example:
-
-      ```shell
-      git commit --message "<commit message>" --signoff
-      ```
-
-      If you ever make a commit and forget to use the `--signoff` flag, you can
-      amend your commit with this information before pushing:
-
-      ```shell
-      git commit --amend --signoff
-      ```
-
-   1. You can verify the above worked as expected using `git log`. Your latest
-      commit should look similar to this one:
-
-      ```shell
-      Author: Joe Smith <joe.smith@example.com>
-      Date:   Thu Feb 2 11:41:15 2018 -0800
-
-      Update README
-
-      Signed-off-by: Joe Smith <joe.smith@example.com>
-      ```
-
-      Notice the `Author` and `Signed-off-by` lines match. If they do not, the
-      PR will be rejected by the automated DCO check.
diff --git a/docs/old-docs/40-contributor-guide/30-code-of-conduct.md b/docs/old-docs/40-contributor-guide/30-code-of-conduct.md
deleted file mode 100644
index ac3901b3..00000000
--- a/docs/old-docs/40-contributor-guide/30-code-of-conduct.md
+++ /dev/null
@@ -1,137 +0,0 @@
----
-sidebar_label: Code of Conduct
-description: Contributor Code of Conduct for the open source Kargo project
----
-
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-We as members, contributors, and leaders pledge to make participation in our
-community a harassment-free experience for everyone, regardless of age, body
-size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
-nationality, personal appearance, race, caste, color, religion, or sexual
-identity and orientation.
-
-We pledge to act and interact in ways that contribute to an open, welcoming,
-diverse, inclusive, and healthy community.
-
-## Our Standards
-
-Examples of behavior that contributes to a positive environment for our
-community include:
-
-* Demonstrating empathy and kindness toward other people
-* Being respectful of differing opinions, viewpoints, and experiences
-* Giving and gracefully accepting constructive feedback
-* Accepting responsibility and apologizing to those affected by our mistakes,
-  and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the overall
-  community
-
-Examples of unacceptable behavior include:
-
-* The use of sexualized language or imagery, and sexual attention or advances of
-  any kind
-* Trolling, insulting or derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or email address,
-  without their explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
-  professional setting
-
-## Enforcement Responsibilities
-
-Community leaders are responsible for clarifying and enforcing our standards of
-acceptable behavior and will take appropriate and fair corrective action in
-response to any behavior that they deem inappropriate, threatening, offensive,
-or harmful.
-
-Community leaders have the right and responsibility to remove, edit, or reject
-comments, commits, code, wiki edits, issues, and other contributions that are
-not aligned to this Code of Conduct, and will communicate reasons for moderation
-decisions when appropriate.
-
-## Scope
-
-This Code of Conduct applies within all community spaces, and also applies when
-an individual is officially representing the community in public spaces.
-Examples of representing our community include using an official e-mail address,
-posting via an official social media account, or acting as an appointed
-representative at an online or offline event.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the community leaders responsible for enforcement at
-[kent@akuity.io](mailto:kent@akuity.io).
-All complaints will be reviewed and investigated promptly and fairly.
-
-All community leaders are obligated to respect the privacy and security of the
-reporter of any incident.
-
-## Enforcement Guidelines
-
-Community leaders will follow these Community Impact Guidelines in determining
-the consequences for any action they deem in violation of this Code of Conduct:
-
-### 1. Correction
-
-**Community Impact**: Use of inappropriate language or other behavior deemed
-unprofessional or unwelcome in the community.
-
-**Consequence**: A private, written warning from community leaders, providing
-clarity around the nature of the violation and an explanation of why the
-behavior was inappropriate. A public apology may be requested.
-
-### 2. Warning
-
-**Community Impact**: A violation through a single incident or series of
-actions.
-
-**Consequence**: A warning with consequences for continued behavior. No
-interaction with the people involved, including unsolicited interaction with
-those enforcing the Code of Conduct, for a specified period of time. This
-includes avoiding interactions in community spaces as well as external channels
-like social media. Violating these terms may lead to a temporary or permanent
-ban.
-
-### 3. Temporary Ban
-
-**Community Impact**: A serious violation of community standards, including
-sustained inappropriate behavior.
-
-**Consequence**: A temporary ban from any sort of interaction or public
-communication with the community for a specified period of time. No public or
-private interaction with the people involved, including unsolicited interaction
-with those enforcing the Code of Conduct, is allowed during this period.
-Violating these terms may lead to a permanent ban.
-
-### 4. Permanent Ban
-
-**Community Impact**: Demonstrating a pattern of violation of community
-standards, including sustained inappropriate behavior, harassment of an
-individual, or aggression toward or disparagement of classes of individuals.
-
-**Consequence**: A permanent ban from any sort of public interaction within the
-community.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage],
-version 2.1, available at
-[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
-
-Community Impact Guidelines were inspired by
-[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
-
-For answers to common questions about this code of conduct, see the FAQ at
-[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
-[https://www.contributor-covenant.org/translations][translations].
-
-[homepage]: https://www.contributor-covenant.org
-[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
-[Mozilla CoC]: https://github.com/mozilla/diversity
-[FAQ]: https://www.contributor-covenant.org/faq
-[translations]: https://www.contributor-covenant.org/translations
diff --git a/docs/old-docs/40-contributor-guide/5-debugging-kargo.md b/docs/old-docs/40-contributor-guide/5-debugging-kargo.md
deleted file mode 100644
index f606be7f..00000000
--- a/docs/old-docs/40-contributor-guide/5-debugging-kargo.md
+++ /dev/null
@@ -1,63 +0,0 @@
----
-description: Steps to help you debug Kargo when you run into issues.
-sidebar_label: Debugging Kargo
----
-
-# Debugging Kargo
-
-From time to time, you may need to debug Kargo. This document outlines options
-to help you achieve a better understanding of what Kargo (internally) is doing,
-especially when you are running into issues which are not immediately obvious
-by just reading the code.
-
-As a Kargo user, you may not need to debug Kargo itself, but this document may
-serve you when a Kargo maintainer asks you to provide additional information
-about an issue you are facing.
-
-## Enabling pprof endpoints
-
-Kargo components can be configured to expose [`pprof` endpoints](https://golang.org/pkg/net/http/pprof/).
-These endpoints can be used to profile the components when they are running,
-and can be useful to understand what the components are doing and where they
-are spending time.
-
-To enable the `pprof` endpoint on a component, you can set the
-`PPROF_BIND_ADDRESS` environment variable to the address where the component
-should listen for `pprof` requests. For example, to enable the `pprof` endpoint
-on port `6060` of the controller, you can set the `PPROF_BIND_ADDRESS`
-environment variable to `:6060`.
-
-```yaml
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: kargo-controller
-spec:
-  # ...omitted for brevity
-  template:
-    spec:
-      containers:
-      - name: kargo-controller
-        env:
-        - name: PPROF_BIND_ADDRESS
-          value: ":6060"
-```
-
-After setting the `PPROF_BIND_ADDRESS` environment variable, the `pprof`
-endpoints will be available at `http://<controller-ip>:6060/debug/pprof/`.
-
-### Collecting a profile
-
-To collect a profile, you can port-forward the `pprof` address to your local
-machine and collect the data from an endpoint of choice. For example, to
-collect a heap profile, you can run:
-
-```console
-$ kubectl port-forward -n <namespace> deployment/<component> 6060
-$ curl -Sk -v http://localhost:6060/debug/pprof/heap > heap.out
-```
-
-This will collect a heap profile in the `heap.out` file, which you can then
-[analyze using `go`](https://go.dev/blog/pprof), or share with a Kargo
-maintainer.
diff --git a/docs/old-docs/40-contributor-guide/index.md b/docs/old-docs/40-contributor-guide/index.md
deleted file mode 100644
index 45d0c825..00000000
--- a/docs/old-docs/40-contributor-guide/index.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-description: A comprehensive introduction for developers who are looking to get involved with contributing directly to the Kargo project
-sidebar_label: Contributor Guide
----
-
-# Kargo Contributor Guide
-
-This contributor guide is intended as a comprehensive introduction for
-developers who are looking to get involved with contributing directly to the
-Kargo project.
-
-This guide is decomposed into the following, high-level topics:
-
-* [Hacking on Kargo](./10-hacking-on-kargo.md)
-* [Signing Commits](./20-signing-commits.md)
-* [Code of Conduct](./30-code-of-conduct.md)
diff --git a/docs/old-docs/45-resources.md b/docs/old-docs/45-resources.md
deleted file mode 100644
index 6da6a651..00000000
--- a/docs/old-docs/45-resources.md
+++ /dev/null
@@ -1,50 +0,0 @@
----
-description: Learn about Kargo through our talks from conferences 
-sidebar_label: Resources
----
-
-# Resources
-
-Learn more about Kargo through talks and webinars presented at conferences like KubeCon, ArgoCon, GitOpsCon, etc!
-
-## Kargo GA Webinar 🎉
-By Kelsey Hightower & Jesse Suen
-
-<center>
-<div style={{position: "relative", width: "100%", "padding-top": "56.25%"}}>
-  <iframe style={{position: "absolute", top: 0, left: 0, width: "100%", height: "100%"}} src="https://www.youtube.com/embed/GvGt0yuApvE?si=UCzHwdzg7R9_mfjJ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen/>
-</div>
-</center>
-
-## Streamlining Kubernetes with Kargo
-By Christian Hernandez | GitHub OpenSourceFriday 2024
-
-<center>
-<div style={{position: "relative", width: "100%", "padding-top": "56.25%"}}>
-  <iframe style={{position: "absolute", top: 0, left: 0, width: "100%", height: "100%"}} src="https://www.youtube.com/embed/vQ8coBb-3eE?si=26tO8z9-ZoQwbv3N" title="Streamlining Kubernetes with Kargo: #OpenSourceFriday feat. Christian Hernandez" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen/>
-</div>
-</center>
-
----
-
-## GitOps Pipelines: Everything Everywhere All at Once
-By Christian Hernandez | GitOpsCon North America 2024
-
-<center>
-<div style={{position: "relative", width: "100%", "padding-top": "56.25%"}}>
-  <iframe style={{position: "absolute", top: 0, left: 0, width: "100%", height: "100%"}} src="https://www.youtube.com/embed/ZHGWXISTP68?si=jFdESQ5yXbQn0M3D" title="GitOps Pipelines: Everything Everywhere All at Once - Christian Hernandez" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen/>
-</div>
-</center>
-
----
-
-## Multi-Stage Deployment Pipelines the GitOps Way
-By Jesse Suen & Kent Rancourt | GitOpsCon Europe 2023
-
-<center>
-<div style={{position: "relative", width: "100%", "padding-top": "56.25%"}}>
-  <iframe style={{position: "absolute", top: 0, left: 0, width: "100%", height: "100%"}} src="https://www.youtube.com/embed/tl8w72tehZ0?si=q-uuzl64JXKyuqH1" title="Multi-Stage Deployment Pipelines the GitOps Way - Jesse Suen & Kent Rancourt" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen/>
-</div>
-</center>
-
----
\ No newline at end of file
diff --git a/docs/old-docs/50-roadmap.md b/docs/old-docs/50-roadmap.md
deleted file mode 100644
index d6070f70..00000000
--- a/docs/old-docs/50-roadmap.md
+++ /dev/null
@@ -1,126 +0,0 @@
----
-sidebar_label: Roadmap
-Description: See what's on the roadmap of Kargo and find out more about the latest releases
----
-
-# Kargo Roadmap
-
-Over a series of releases, Kargo's maintainers have settled into a cadence of a
-minor release roughly every five weeks, with two or three major features
-completed per release.
-
-:::caution
-This roadmap tracks only _major_ features and is subject to change at any time,
-for the most up-to-date information, please see the [GitHub
-Project](https://github.com/akuity/kargo/milestones)
-:::
-
-## In Progress
-
-### v1.1.0
-
-__Expected:__ 22 November 2024
-
-This release will focus on improving the flexibility of the promotion steps
-introduced in v0.9.0 through the addition of support for an expression language.
-
-This release also kicks off a three phase plan with the end goal of enabling
-custom/third-party promotion steps.
-
-## Upcoming
-
-### v1.2.0
-
-This release is to be the second phase of three advancing Kargo toward support
-for custom/third-party promotion steps, with a focus on ensuring a secure and
-isolated execution environment for those steps.
-
-### v1.3.0
-
-The third and final phase of the three advancing Kargo toward support for
-custom/third-party promotion steps will focus on:
-
-* Establishing a formal specification for developers wishing to implement their
-  own promotion steps.
-
-* Building the mechanisms whereby operators may install and users may leverage
-  versioned, custom/third-party promotion steps.
-
-### v1.4.0 and Beyond
-
-* Packaging common workflows as pre-defined, composite promotion steps.
-* TBD
-
-## Completed
-
-### v1.0.0
-
-v1.0.0 was our long-anticipated GA release containing only small features,
-bug fixes, stability improvements, and the final removal of the legacy
-promotion mechanisms that were deprecated in v0.9.0.
-
-### v0.9.0
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| [Promotion Steps](https://github.com/akuity/kargo/issues/2219) | strategic refactor | Transitioned from opinionated promotion mechanisms to an ordered lists of more finely-grained promotion directives steps reminiscent of GitHub Actions. These enable greater flexibility in addressing outlying use cases and have left us with a clear path forward for to eventually enable third-party integrations. |
-| Production Readiness | chore | <ul><li>Prioritized stability of existing features.</li><li>Paid down technical debt.</li><li>**This does not mean v0.9.0 is production-ready. It means it is several steps closer to it.**</li></ul> |
-
-### v0.8.0
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| Auth via [GitHub Apps](https://docs.github.com/en/apps) | feature | Support GitHub Apps as an authentication option for GitHub repositories. |
-| Multiple `Freight` per `Stage` | feature | Permit `Stage`s to host multiple pieces of `Freight` from different `Warehouse`s. Different artifacts, or sets of artifacts, can be promoted through parallel pipelines with different/independent cadence. |
-| Production Readiness | chore | <ul><li>Prioritized stability of existing features.</li><li>Paid down technical debt.</li><li>**This does not mean v0.8.0 is production-ready. It means it is several steps closer to it.**</li></ul> |
-
-### v0.7.0
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| Multiple `Warehouse`s | feature | Improved UI support for displaying Freight from multiple `Warehouse`s. |
-| Manual `Freight` Creation | feature | Added UI feature for manual `Freight` creation. |
-| ECR/GAR Support | feature | Added multiple options for authenticating to image repositories in ECR and Google Artifact Registry, including support for EKS Pod Identity and GKE Workload Identity Federation. | 
-| [Patch Promotions](https://github.com/akuity/kargo/issues/1250) | poc | Support a generalized option to promote arbitrary configuration (e.g. strings, files, and directories) to other paths of a GitOps repository. |
-| Production Readiness | chore | Prioritized stability of existing features. **This does not mean v0.7.0 is production-ready. It means it is several steps closer to it.** |
-
-### v0.6.0
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| Project Management | feature | Added user / role / permission management capabilities to the CLI and UI. |
-| Events | feature | Kargo emits noteworthy events as Kubernetes events. Events are also viewable in the UI. |
-| Production Readiness | chore | Prioritized stability of existing features. **This does not mean v0.6.0 is production-ready. It means it is several steps closer to it.** |
-
-### v0.5.0
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| `Warehouse` Rules/Filters | feature | Introduced optional path-based constraints on Git repository subscriptions. |
-| Credential Storage | refactor | Simplified and streamlined format and storage of repository credentials. |
-| Credential Management | feature | Added credential management capabilities to the CLI and UI. |
-| CLI Improvements | refactor | Overhauled the CLI to make the tree of sub-commands more intuitive, with improved consistency in usage and documentation from command to command. |
-| UI Improvements | feature | Achieved near-parity with CLI features. |
-
-### v0.4.0
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| `Warehouse` Rules/Filters | feature | Introduced optional tag-based constraints on Git repository subscriptions. |
-| Project Management | feature | <ul><li>Introduced `Project` CRD to simplify project initialization.</li><li>Removed `PromotionPolicy` CRD and folded its functionality directly into the `Project` CRD.</li></ul> |
-
-### v0.3.0
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| GitHub PR-Based Promotion | feature | Pull request-based promotions are now supported on GitHub. |
-| Verifications | feature | `Stage` resources can now execute a user-defined verification process after a promotion. These can be defined using Argo Rollouts `AnalysisTemplate` resources, and executions take the form of `AnalysisRun` resources. |
-| Improved RBAC | feature | SSO user identities can now be mapped to Kubernetes `ServiceAccount` resources using annotations. |
-
-### v0.2.0
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| `Freight` CRD | feature | Freight changed from being a property of a `Stage`, to being its own `Freight` CRD. |
-| `Warehouse` CRD | feature | `Freight` production was decoupled from a pipeline's first `Stage` and now comes from a `Warehouse`. |
-| Kargo Render | breaking change | The Bookkeeper project was rebranded as Kargo Render -- a Kargo sub-project for rendering manifests. |
diff --git a/docs/old-docs/README.md b/docs/old-docs/README.md
deleted file mode 100644
index b28f96e4..00000000
--- a/docs/old-docs/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# Old Docs
-
-Old docs are preserved here, short-term, for maintainer convenience since we may
-periodically draw excerpts from them as we continue delivering new content.
diff --git a/docs/old-docs/_category_.json b/docs/old-docs/_category_.json
deleted file mode 100644
index 6fad2840..00000000
--- a/docs/old-docs/_category_.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "label": "Old Docs",
-  "collapsible": true,
-  "collapsed": true
-}
diff --git a/docs/old-docs/static/.nojekyll b/docs/old-docs/static/.nojekyll
deleted file mode 100644
index e69de29b..00000000
diff --git a/docs/old-docs/static/img/architecture.png b/docs/old-docs/static/img/architecture.png
deleted file mode 100644
index e4baba6d..00000000
Binary files a/docs/old-docs/static/img/architecture.png and /dev/null differ
diff --git a/docs/old-docs/static/img/argo-dashboard.png b/docs/old-docs/static/img/argo-dashboard.png
deleted file mode 100644
index fcdee7a1..00000000
Binary files a/docs/old-docs/static/img/argo-dashboard.png and /dev/null differ
diff --git a/docs/old-docs/static/img/create-stage-2.png b/docs/old-docs/static/img/create-stage-2.png
deleted file mode 100644
index e888a785..00000000
Binary files a/docs/old-docs/static/img/create-stage-2.png and /dev/null differ
diff --git a/docs/old-docs/static/img/create-stage-3.png b/docs/old-docs/static/img/create-stage-3.png
deleted file mode 100644
index e676a1b8..00000000
Binary files a/docs/old-docs/static/img/create-stage-3.png and /dev/null differ
diff --git a/docs/old-docs/static/img/create-stage.png b/docs/old-docs/static/img/create-stage.png
deleted file mode 100644
index 7041e9c9..00000000
Binary files a/docs/old-docs/static/img/create-stage.png and /dev/null differ
diff --git a/docs/old-docs/static/img/delete-stage.png b/docs/old-docs/static/img/delete-stage.png
deleted file mode 100644
index 5a10336f..00000000
Binary files a/docs/old-docs/static/img/delete-stage.png and /dev/null differ
diff --git a/docs/old-docs/static/img/kargo-dashboard-projects.png b/docs/old-docs/static/img/kargo-dashboard-projects.png
deleted file mode 100644
index 3d6df83f..00000000
Binary files a/docs/old-docs/static/img/kargo-dashboard-projects.png and /dev/null differ
diff --git a/docs/old-docs/static/img/kargo-dashboard-promotion.png b/docs/old-docs/static/img/kargo-dashboard-promotion.png
deleted file mode 100644
index 4087592d..00000000
Binary files a/docs/old-docs/static/img/kargo-dashboard-promotion.png and /dev/null differ
diff --git a/docs/old-docs/static/img/kargo-dashboard.png b/docs/old-docs/static/img/kargo-dashboard.png
deleted file mode 100644
index 5a4f518d..00000000
Binary files a/docs/old-docs/static/img/kargo-dashboard.png and /dev/null differ
diff --git a/docs/old-docs/static/img/kargo-freight-verified.png b/docs/old-docs/static/img/kargo-freight-verified.png
deleted file mode 100644
index d9e7f8d0..00000000
Binary files a/docs/old-docs/static/img/kargo-freight-verified.png and /dev/null differ
diff --git a/docs/old-docs/static/img/kargo-frieght-timeline.png b/docs/old-docs/static/img/kargo-frieght-timeline.png
deleted file mode 100644
index 7ae78a0c..00000000
Binary files a/docs/old-docs/static/img/kargo-frieght-timeline.png and /dev/null differ
diff --git a/docs/old-docs/static/img/kargo-projects.png b/docs/old-docs/static/img/kargo-projects.png
deleted file mode 100644
index 6f473747..00000000
Binary files a/docs/old-docs/static/img/kargo-projects.png and /dev/null differ
diff --git a/docs/old-docs/static/img/kargo-promote-option-2.png b/docs/old-docs/static/img/kargo-promote-option-2.png
deleted file mode 100644
index fe37f0d9..00000000
Binary files a/docs/old-docs/static/img/kargo-promote-option-2.png and /dev/null differ
diff --git a/docs/old-docs/static/img/kargo-promote-option.png b/docs/old-docs/static/img/kargo-promote-option.png
deleted file mode 100644
index 1532d413..00000000
Binary files a/docs/old-docs/static/img/kargo-promote-option.png and /dev/null differ
diff --git a/docs/old-docs/static/img/kargo.png b/docs/old-docs/static/img/kargo.png
deleted file mode 100644
index 67bd6ad0..00000000
Binary files a/docs/old-docs/static/img/kargo.png and /dev/null differ
diff --git a/docs/old-docs/static/img/logo.svg b/docs/old-docs/static/img/logo.svg
deleted file mode 100644
index 9db6d0d0..00000000
--- a/docs/old-docs/static/img/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M99 52h84v34H99z"/><path d="M23 163c-7.398 0-13.843-4.027-17.303-10A19.886 19.886 0 0 0 3 163c0 11.046 8.954 20 20 20h20v-20H23z" fill="#3ECC5F"/><path d="M112.98 57.376L183 53V43c0-11.046-8.954-20-20-20H73l-2.5-4.33c-1.112-1.925-3.889-1.925-5 0L63 23l-2.5-4.33c-1.111-1.925-3.889-1.925-5 0L53 23l-2.5-4.33c-1.111-1.925-3.889-1.925-5 0L43 23c-.022 0-.042.003-.065.003l-4.142-4.141c-1.57-1.571-4.252-.853-4.828 1.294l-1.369 5.104-5.192-1.392c-2.148-.575-4.111 1.389-3.535 3.536l1.39 5.193-5.102 1.367c-2.148.576-2.867 3.259-1.296 4.83l4.142 4.142c0 .021-.003.042-.003.064l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 53l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 63l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 73l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 83l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 93l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 103l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 113l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 123l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 133l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 143l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 153l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 163c0 11.046 8.954 20 20 20h120c11.046 0 20-8.954 20-20V83l-70.02-4.376A10.645 10.645 0 0 1 103 68c0-5.621 4.37-10.273 9.98-10.624" fill="#3ECC5F"/><path fill="#3ECC5F" d="M143 183h30v-40h-30z"/><path d="M193 158c-.219 0-.428.037-.639.064-.038-.15-.074-.301-.116-.451A5 5 0 0 0 190.32 148a4.96 4.96 0 0 0-3.016 1.036 26.531 26.531 0 0 0-.335-.336 4.955 4.955 0 0 0 1.011-2.987 5 5 0 0 0-9.599-1.959c-.148-.042-.297-.077-.445-.115.027-.211.064-.42.064-.639a5 5 0 0 0-5-5 5 5 0 0 0-5 5c0 .219.037.428.064.639-.148.038-.297.073-.445.115a4.998 4.998 0 0 0-9.599 1.959c0 1.125.384 2.151 1.011 2.987-3.717 3.632-6.031 8.693-6.031 14.3 0 11.046 8.954 20 20 20 9.339 0 17.16-6.41 19.361-15.064.211.027.42.064.639.064a5 5 0 0 0 5-5 5 5 0 0 0-5-5" fill="#44D860"/><path fill="#3ECC5F" d="M153 123h30v-20h-30z"/><path d="M193 115.5a2.5 2.5 0 1 0 0-5c-.109 0-.214.019-.319.032-.02-.075-.037-.15-.058-.225a2.501 2.501 0 0 0-.963-4.807c-.569 0-1.088.197-1.508.518a6.653 6.653 0 0 0-.168-.168c.314-.417.506-.931.506-1.494a2.5 2.5 0 0 0-4.8-.979A9.987 9.987 0 0 0 183 103c-5.522 0-10 4.478-10 10s4.478 10 10 10c.934 0 1.833-.138 2.69-.377a2.5 2.5 0 0 0 4.8-.979c0-.563-.192-1.077-.506-1.494.057-.055.113-.111.168-.168.42.321.939.518 1.508.518a2.5 2.5 0 0 0 .963-4.807c.021-.074.038-.15.058-.225.105.013.21.032.319.032" fill="#44D860"/><path d="M63 55.5a2.5 2.5 0 0 1-2.5-2.5c0-4.136-3.364-7.5-7.5-7.5s-7.5 3.364-7.5 7.5a2.5 2.5 0 1 1-5 0c0-6.893 5.607-12.5 12.5-12.5S65.5 46.107 65.5 53a2.5 2.5 0 0 1-2.5 2.5" fill="#000"/><path d="M103 183h60c11.046 0 20-8.954 20-20V93h-60c-11.046 0-20 8.954-20 20v70z" fill="#FFFF50"/><path d="M168.02 124h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0-49.814h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 19.814h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2M183 61.611c-.012 0-.022-.006-.034-.005-3.09.105-4.552 3.196-5.842 5.923-1.346 2.85-2.387 4.703-4.093 4.647-1.889-.068-2.969-2.202-4.113-4.46-1.314-2.594-2.814-5.536-5.963-5.426-3.046.104-4.513 2.794-5.807 5.167-1.377 2.528-2.314 4.065-4.121 3.994-1.927-.07-2.951-1.805-4.136-3.813-1.321-2.236-2.848-4.75-5.936-4.664-2.994.103-4.465 2.385-5.763 4.4-1.373 2.13-2.335 3.428-4.165 3.351-1.973-.07-2.992-1.51-4.171-3.177-1.324-1.873-2.816-3.993-5.895-3.89-2.928.1-4.399 1.97-5.696 3.618-1.232 1.564-2.194 2.802-4.229 2.724a1 1 0 0 0-.072 2c3.017.101 4.545-1.8 5.872-3.487 1.177-1.496 2.193-2.787 4.193-2.855 1.926-.082 2.829 1.115 4.195 3.045 1.297 1.834 2.769 3.914 5.731 4.021 3.103.104 4.596-2.215 5.918-4.267 1.182-1.834 2.202-3.417 4.15-3.484 1.793-.067 2.769 1.35 4.145 3.681 1.297 2.197 2.766 4.686 5.787 4.796 3.125.108 4.634-2.62 5.949-5.035 1.139-2.088 2.214-4.06 4.119-4.126 1.793-.042 2.728 1.595 4.111 4.33 1.292 2.553 2.757 5.445 5.825 5.556l.169.003c3.064 0 4.518-3.075 5.805-5.794 1.139-2.41 2.217-4.68 4.067-4.773v-2z" fill="#000"/><path fill="#3ECC5F" d="M83 183h40v-40H83z"/><path d="M143 158c-.219 0-.428.037-.639.064-.038-.15-.074-.301-.116-.451A5 5 0 0 0 140.32 148a4.96 4.96 0 0 0-3.016 1.036 26.531 26.531 0 0 0-.335-.336 4.955 4.955 0 0 0 1.011-2.987 5 5 0 0 0-9.599-1.959c-.148-.042-.297-.077-.445-.115.027-.211.064-.42.064-.639a5 5 0 0 0-5-5 5 5 0 0 0-5 5c0 .219.037.428.064.639-.148.038-.297.073-.445.115a4.998 4.998 0 0 0-9.599 1.959c0 1.125.384 2.151 1.011 2.987-3.717 3.632-6.031 8.693-6.031 14.3 0 11.046 8.954 20 20 20 9.339 0 17.16-6.41 19.361-15.064.211.027.42.064.639.064a5 5 0 0 0 5-5 5 5 0 0 0-5-5" fill="#44D860"/><path fill="#3ECC5F" d="M83 123h40v-20H83z"/><path d="M133 115.5a2.5 2.5 0 1 0 0-5c-.109 0-.214.019-.319.032-.02-.075-.037-.15-.058-.225a2.501 2.501 0 0 0-.963-4.807c-.569 0-1.088.197-1.508.518a6.653 6.653 0 0 0-.168-.168c.314-.417.506-.931.506-1.494a2.5 2.5 0 0 0-4.8-.979A9.987 9.987 0 0 0 123 103c-5.522 0-10 4.478-10 10s4.478 10 10 10c.934 0 1.833-.138 2.69-.377a2.5 2.5 0 0 0 4.8-.979c0-.563-.192-1.077-.506-1.494.057-.055.113-.111.168-.168.42.321.939.518 1.508.518a2.5 2.5 0 0 0 .963-4.807c.021-.074.038-.15.058-.225.105.013.21.032.319.032" fill="#44D860"/><path d="M143 41.75c-.16 0-.33-.02-.49-.05a2.52 2.52 0 0 1-.47-.14c-.15-.06-.29-.14-.431-.23-.13-.09-.259-.2-.38-.31-.109-.12-.219-.24-.309-.38s-.17-.28-.231-.43a2.619 2.619 0 0 1-.189-.96c0-.16.02-.33.05-.49.03-.16.08-.31.139-.47.061-.15.141-.29.231-.43.09-.13.2-.26.309-.38.121-.11.25-.22.38-.31.141-.09.281-.17.431-.23.149-.06.31-.11.47-.14.32-.07.65-.07.98 0 .159.03.32.08.47.14.149.06.29.14.43.23.13.09.259.2.38.31.11.12.22.25.31.38.09.14.17.28.23.43.06.16.11.31.14.47.029.16.05.33.05.49 0 .66-.271 1.31-.73 1.77-.121.11-.25.22-.38.31-.14.09-.281.17-.43.23a2.565 2.565 0 0 1-.96.19m20-1.25c-.66 0-1.3-.27-1.771-.73a3.802 3.802 0 0 1-.309-.38c-.09-.14-.17-.28-.231-.43a2.619 2.619 0 0 1-.189-.96c0-.66.27-1.3.729-1.77.121-.11.25-.22.38-.31.141-.09.281-.17.431-.23.149-.06.31-.11.47-.14.32-.07.66-.07.98 0 .159.03.32.08.47.14.149.06.29.14.43.23.13.09.259.2.38.31.459.47.73 1.11.73 1.77 0 .16-.021.33-.05.49-.03.16-.08.32-.14.47-.07.15-.14.29-.23.43-.09.13-.2.26-.31.38-.121.11-.25.22-.38.31-.14.09-.281.17-.43.23a2.565 2.565 0 0 1-.96.19" fill="#000"/></g></svg>
\ No newline at end of file
diff --git a/docs/old-docs/static/img/refresh-stage.png b/docs/old-docs/static/img/refresh-stage.png
deleted file mode 100644
index ef955819..00000000
Binary files a/docs/old-docs/static/img/refresh-stage.png and /dev/null differ
diff --git a/docs/old-docs/static/img/reverify-freight.png b/docs/old-docs/static/img/reverify-freight.png
deleted file mode 100644
index 6c5c784a..00000000
Binary files a/docs/old-docs/static/img/reverify-freight.png and /dev/null differ
diff --git a/docs/old-docs/static/img/undraw_docusaurus_mountain.svg b/docs/old-docs/static/img/undraw_docusaurus_mountain.svg
deleted file mode 100644
index af961c49..00000000
--- a/docs/old-docs/static/img/undraw_docusaurus_mountain.svg
+++ /dev/null
@@ -1,171 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1088" height="687.962" viewBox="0 0 1088 687.962">
-  <title>Easy to Use</title>
-  <g id="Group_12" data-name="Group 12" transform="translate(-57 -56)">
-    <g id="Group_11" data-name="Group 11" transform="translate(57 56)">
-      <path id="Path_83" data-name="Path 83" d="M1017.81,560.461c-5.27,45.15-16.22,81.4-31.25,110.31-20,38.52-54.21,54.04-84.77,70.28a193.275,193.275,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.282,657.282,0,0,0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07,5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12,52.29-235.46,134.74-296.47,155.97-115.41,369.76-110.57,523.43,7.88C941.15,276.621,1036.99,396.031,1017.81,560.461Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
-      <path id="Path_84" data-name="Path 84" d="M986.56,670.771c-20,38.52-47.21,64.04-77.77,80.28a193.272,193.272,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.3,657.3,0,0,0-104.09-13.16q-14.97-.675-29.97-.67-23.13.03-46.25,1.72c-100.17,7.36-253.82-6.43-321.42-143.29L382,283.981,444.95,445.6l20.09,51.59,55.37-75.98L549,381.981l130.2,149.27,36.8-81.27L970.78,657.9l14.21,11.59Z" transform="translate(-56 -106.019)" fill="#f2f2f2"/>
-      <path id="Path_85" data-name="Path 85" d="M302,282.962l26-57,36,83-31-60Z" opacity="0.1"/>
-      <path id="Path_86" data-name="Path 86" d="M610.5,753.821q-14.97-.675-29.97-.67L465.04,497.191Z" transform="translate(-56 -106.019)" opacity="0.1"/>
-      <path id="Path_87" data-name="Path 87" d="M464.411,315.191,493,292.962l130,150-132-128Z" opacity="0.1"/>
-      <path id="Path_88" data-name="Path 88" d="M908.79,751.051a193.265,193.265,0,0,1-27.46,11.94L679.2,531.251Z" transform="translate(-56 -106.019)" opacity="0.1"/>
-      <circle id="Ellipse_11" data-name="Ellipse 11" cx="3" cy="3" r="3" transform="translate(479 98.962)" fill="#f2f2f2"/>
-      <circle id="Ellipse_12" data-name="Ellipse 12" cx="3" cy="3" r="3" transform="translate(396 201.962)" fill="#f2f2f2"/>
-      <circle id="Ellipse_13" data-name="Ellipse 13" cx="2" cy="2" r="2" transform="translate(600 220.962)" fill="#f2f2f2"/>
-      <circle id="Ellipse_14" data-name="Ellipse 14" cx="2" cy="2" r="2" transform="translate(180 265.962)" fill="#f2f2f2"/>
-      <circle id="Ellipse_15" data-name="Ellipse 15" cx="2" cy="2" r="2" transform="translate(612 96.962)" fill="#f2f2f2"/>
-      <circle id="Ellipse_16" data-name="Ellipse 16" cx="2" cy="2" r="2" transform="translate(736 192.962)" fill="#f2f2f2"/>
-      <circle id="Ellipse_17" data-name="Ellipse 17" cx="2" cy="2" r="2" transform="translate(858 344.962)" fill="#f2f2f2"/>
-      <path id="Path_89" data-name="Path 89" d="M306,121.222h-2.76v-2.76h-1.48v2.76H299V122.7h2.76v2.759h1.48V122.7H306Z" fill="#f2f2f2"/>
-      <path id="Path_90" data-name="Path 90" d="M848,424.222h-2.76v-2.76h-1.48v2.76H841V425.7h2.76v2.759h1.48V425.7H848Z" fill="#f2f2f2"/>
-      <path id="Path_91" data-name="Path 91" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
-      <path id="Path_92" data-name="Path 92" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" opacity="0.1"/>
-      <ellipse id="Ellipse_18" data-name="Ellipse 18" cx="544" cy="30" rx="544" ry="30" transform="translate(0 583.962)" fill="#3f3d56"/>
-      <path id="Path_93" data-name="Path 93" d="M624,677.981c0,33.137-14.775,24-33,24s-33,9.137-33-24,33-96,33-96S624,644.844,624,677.981Z" transform="translate(-56 -106.019)" fill="#ff6584"/>
-      <path id="Path_94" data-name="Path 94" d="M606,690.66c0,15.062-6.716,10.909-15,10.909s-15,4.153-15-10.909,15-43.636,15-43.636S606,675.6,606,690.66Z" transform="translate(-56 -106.019)" opacity="0.1"/>
-      <rect id="Rectangle_97" data-name="Rectangle 97" width="92" height="18" rx="9" transform="translate(489 604.962)" fill="#2f2e41"/>
-      <rect id="Rectangle_98" data-name="Rectangle 98" width="92" height="18" rx="9" transform="translate(489 586.962)" fill="#2f2e41"/>
-      <path id="Path_95" data-name="Path 95" d="M193,596.547c0,55.343,34.719,100.126,77.626,100.126" transform="translate(-56 -106.019)" fill="#3f3d56"/>
-      <path id="Path_96" data-name="Path 96" d="M270.626,696.673c0-55.965,38.745-101.251,86.626-101.251" transform="translate(-56 -106.019)" fill="#6c63ff"/>
-      <path id="Path_97" data-name="Path 97" d="M221.125,601.564c0,52.57,22.14,95.109,49.5,95.109" transform="translate(-56 -106.019)" fill="#6c63ff"/>
-      <path id="Path_98" data-name="Path 98" d="M270.626,696.673c0-71.511,44.783-129.377,100.126-129.377" transform="translate(-56 -106.019)" fill="#3f3d56"/>
-      <path id="Path_99" data-name="Path 99" d="M254.3,697.379s11.009-.339,14.326-2.7,16.934-5.183,17.757-1.395,16.544,18.844,4.115,18.945-28.879-1.936-32.19-3.953S254.3,697.379,254.3,697.379Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
-      <path id="Path_100" data-name="Path 100" d="M290.716,710.909c-12.429.1-28.879-1.936-32.19-3.953-2.522-1.536-3.527-7.048-3.863-9.591l-.368.014s.7,8.879,4.009,10.9,19.761,4.053,32.19,3.953c3.588-.029,4.827-1.305,4.759-3.2C294.755,710.174,293.386,710.887,290.716,710.909Z" transform="translate(-56 -106.019)" opacity="0.2"/>
-      <path id="Path_101" data-name="Path 101" d="M777.429,633.081c0,38.029,23.857,68.8,53.341,68.8" transform="translate(-56 -106.019)" fill="#3f3d56"/>
-      <path id="Path_102" data-name="Path 102" d="M830.769,701.882c0-38.456,26.623-69.575,59.525-69.575" transform="translate(-56 -106.019)" fill="#6c63ff"/>
-      <path id="Path_103" data-name="Path 103" d="M796.755,636.528c0,36.124,15.213,65.354,34.014,65.354" transform="translate(-56 -106.019)" fill="#6c63ff"/>
-      <path id="Path_104" data-name="Path 104" d="M830.769,701.882c0-49.139,30.773-88.9,68.8-88.9" transform="translate(-56 -106.019)" fill="#3f3d56"/>
-      <path id="Path_105" data-name="Path 105" d="M819.548,702.367s7.565-.233,9.844-1.856,11.636-3.562,12.2-.958,11.368,12.949,2.828,13.018-19.844-1.33-22.119-2.716S819.548,702.367,819.548,702.367Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
-      <path id="Path_106" data-name="Path 106" d="M844.574,711.664c-8.54.069-19.844-1.33-22.119-2.716-1.733-1.056-2.423-4.843-2.654-6.59l-.253.01s.479,6.1,2.755,7.487,13.579,2.785,22.119,2.716c2.465-.02,3.317-.9,3.27-2.2C847.349,711.159,846.409,711.649,844.574,711.664Z" transform="translate(-56 -106.019)" opacity="0.2"/>
-      <path id="Path_107" data-name="Path 107" d="M949.813,724.718s11.36-1.729,14.5-4.591,16.89-7.488,18.217-3.667,19.494,17.447,6.633,19.107-30.153,1.609-33.835-.065S949.813,724.718,949.813,724.718Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
-      <path id="Path_108" data-name="Path 108" d="M989.228,734.173c-12.86,1.659-30.153,1.609-33.835-.065-2.8-1.275-4.535-6.858-5.2-9.45l-.379.061s1.833,9.109,5.516,10.783,20.975,1.725,33.835.065c3.712-.479,4.836-1.956,4.529-3.906C993.319,732.907,991.991,733.817,989.228,734.173Z" transform="translate(-56 -106.019)" opacity="0.2"/>
-      <path id="Path_109" data-name="Path 109" d="M670.26,723.9s9.587-1.459,12.237-3.875,14.255-6.32,15.374-3.095,16.452,14.725,5.6,16.125-25.448,1.358-28.555-.055S670.26,723.9,670.26,723.9Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
-      <path id="Path_110" data-name="Path 110" d="M703.524,731.875c-10.853,1.4-25.448,1.358-28.555-.055-2.367-1.076-3.827-5.788-4.39-7.976l-.32.051s1.547,7.687,4.655,9.1,17.7,1.456,28.555.055c3.133-.4,4.081-1.651,3.822-3.3C706.977,730.807,705.856,731.575,703.524,731.875Z" transform="translate(-56 -106.019)" opacity="0.2"/>
-      <path id="Path_111" data-name="Path 111" d="M178.389,719.109s7.463-1.136,9.527-3.016,11.1-4.92,11.969-2.409,12.808,11.463,4.358,12.553-19.811,1.057-22.23-.043S178.389,719.109,178.389,719.109Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
-      <path id="Path_112" data-name="Path 112" d="M204.285,725.321c-8.449,1.09-19.811,1.057-22.23-.043-1.842-.838-2.979-4.506-3.417-6.209l-.249.04s1.2,5.984,3.624,7.085,13.781,1.133,22.23.043c2.439-.315,3.177-1.285,2.976-2.566C206.973,724.489,206.1,725.087,204.285,725.321Z" transform="translate(-56 -106.019)" opacity="0.2"/>
-      <path id="Path_113" data-name="Path 113" d="M439.7,707.337c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873,42.118-36.793,93.694-36.793S439.7,677.117,439.7,707.337Z" transform="translate(-56 -106.019)" opacity="0.1"/>
-      <path id="Path_114" data-name="Path 114" d="M439.7,699.9c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873S295.04,663.1,346.616,663.1,439.7,669.676,439.7,699.9Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
-    </g>
-    <g id="docusaurus_keytar" transform="translate(312.271 493.733)">
-      <path id="Path_40" data-name="Path 40" d="M99,52h91.791V89.153H99Z" transform="translate(5.904 -14.001)" fill="#fff" fill-rule="evenodd"/>
-      <path id="Path_41" data-name="Path 41" d="M24.855,163.927A21.828,21.828,0,0,1,5.947,153a21.829,21.829,0,0,0,18.908,32.782H46.71V163.927Z" transform="translate(-3 -4.634)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_42" data-name="Path 42" d="M121.861,61.1l76.514-4.782V45.39A21.854,21.854,0,0,0,176.52,23.535H78.173L75.441,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L64.513,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L53.586,18.8a3.154,3.154,0,0,0-5.464,0L45.39,23.535c-.024,0-.046,0-.071,0l-4.526-4.525a3.153,3.153,0,0,0-5.276,1.414l-1.5,5.577-5.674-1.521a3.154,3.154,0,0,0-3.863,3.864L26,34.023l-5.575,1.494a3.155,3.155,0,0,0-1.416,5.278l4.526,4.526c0,.023,0,.046,0,.07L18.8,48.122a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,59.05a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,69.977a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,80.9a3.154,3.154,0,0,0,0,5.464L23.535,89.1,18.8,91.832a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,102.76a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,113.687a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,124.615a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,135.542a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,146.469a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,157.4a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,168.324a3.154,3.154,0,0,0,0,5.464l4.732,2.732A21.854,21.854,0,0,0,45.39,198.375H176.52a21.854,21.854,0,0,0,21.855-21.855V89.1l-76.514-4.782a11.632,11.632,0,0,1,0-23.219" transform="translate(-1.681 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_43" data-name="Path 43" d="M143,186.71h32.782V143H143Z" transform="translate(9.984 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_44" data-name="Path 44" d="M196.71,159.855a5.438,5.438,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(10.912 -6.025)" fill="#44d860" fill-rule="evenodd"/>
-      <path id="Path_45" data-name="Path 45" d="M153,124.855h32.782V103H153Z" transform="translate(10.912 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_46" data-name="Path 46" d="M194.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.814,2.814,0,0,0,.349.035" transform="translate(12.767 -9.377)" fill="#44d860" fill-rule="evenodd"/>
-      <path id="Path_47" data-name="Path 47" d="M65.087,56.891a2.732,2.732,0,0,1-2.732-2.732,8.2,8.2,0,0,0-16.391,0,2.732,2.732,0,0,1-5.464,0,13.659,13.659,0,0,1,27.319,0,2.732,2.732,0,0,1-2.732,2.732" transform="translate(0.478 -15.068)" fill-rule="evenodd"/>
-      <path id="Path_48" data-name="Path 48" d="M103,191.347h65.565a21.854,21.854,0,0,0,21.855-21.855V93H124.855A21.854,21.854,0,0,0,103,114.855Z" transform="translate(6.275 -10.199)" fill="#ffff50" fill-rule="evenodd"/>
-      <path id="Path_49" data-name="Path 49" d="M173.216,129.787H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0-54.434H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.652H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186M189.585,61.611c-.013,0-.024-.007-.037-.005-3.377.115-4.974,3.492-6.384,6.472-1.471,3.114-2.608,5.139-4.473,5.078-2.064-.074-3.244-2.406-4.494-4.874-1.436-2.835-3.075-6.049-6.516-5.929-3.329.114-4.932,3.053-6.346,5.646-1.5,2.762-2.529,4.442-4.5,4.364-2.106-.076-3.225-1.972-4.52-4.167-1.444-2.443-3.112-5.191-6.487-5.1-3.272.113-4.879,2.606-6.3,4.808-1.5,2.328-2.552,3.746-4.551,3.662-2.156-.076-3.27-1.65-4.558-3.472-1.447-2.047-3.077-4.363-6.442-4.251-3.2.109-4.807,2.153-6.224,3.954-1.346,1.709-2.4,3.062-4.621,2.977a1.093,1.093,0,0,0-.079,2.186c3.3.11,4.967-1.967,6.417-3.81,1.286-1.635,2.4-3.045,4.582-3.12,2.1-.09,3.091,1.218,4.584,3.327,1.417,2,3.026,4.277,6.263,4.394,3.391.114,5.022-2.42,6.467-4.663,1.292-2,2.406-3.734,4.535-3.807,1.959-.073,3.026,1.475,4.529,4.022,1.417,2.4,3.023,5.121,6.324,5.241,3.415.118,5.064-2.863,6.5-5.5,1.245-2.282,2.419-4.437,4.5-4.509,1.959-.046,2.981,1.743,4.492,4.732,1.412,2.79,3.013,5.95,6.365,6.071l.185,0c3.348,0,4.937-3.36,6.343-6.331,1.245-2.634,2.423-5.114,4.444-5.216Z" transform="translate(7.109 -13.11)" fill-rule="evenodd"/>
-      <path id="Path_50" data-name="Path 50" d="M83,186.71h43.71V143H83Z" transform="translate(4.42 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 109.327, 91.085)">
-        <rect id="Rectangle_3" data-name="Rectangle 3" width="92.361" height="36.462" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
-        <g id="Group_2" data-name="Group 2" transform="translate(1.531 23.03)">
-          <rect id="Rectangle_4" data-name="Rectangle 4" width="5.336" height="5.336" rx="1" transform="translate(16.797 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_5" data-name="Rectangle 5" width="5.336" height="5.336" rx="1" transform="translate(23.12 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_6" data-name="Rectangle 6" width="5.336" height="5.336" rx="1" transform="translate(29.444 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_7" data-name="Rectangle 7" width="5.336" height="5.336" rx="1" transform="translate(35.768 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_8" data-name="Rectangle 8" width="5.336" height="5.336" rx="1" transform="translate(42.091 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_9" data-name="Rectangle 9" width="5.336" height="5.336" rx="1" transform="translate(48.415 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_10" data-name="Rectangle 10" width="5.336" height="5.336" rx="1" transform="translate(54.739 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_11" data-name="Rectangle 11" width="5.336" height="5.336" rx="1" transform="translate(61.063 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_12" data-name="Rectangle 12" width="5.336" height="5.336" rx="1" transform="translate(67.386 0)" fill="#4a4a4a"/>
-          <path id="Path_51" data-name="Path 51" d="M1.093,0H14.518a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0ZM75,0H88.426a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H75a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,75,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-        </g>
-        <g id="Group_3" data-name="Group 3" transform="translate(1.531 10.261)">
-          <path id="Path_52" data-name="Path 52" d="M1.093,0H6.218A1.093,1.093,0,0,1,7.31,1.093V4.242A1.093,1.093,0,0,1,6.218,5.335H1.093A1.093,1.093,0,0,1,0,4.242V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-          <rect id="Rectangle_13" data-name="Rectangle 13" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_14" data-name="Rectangle 14" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_15" data-name="Rectangle 15" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_16" data-name="Rectangle 16" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_17" data-name="Rectangle 17" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_18" data-name="Rectangle 18" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_19" data-name="Rectangle 19" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_20" data-name="Rectangle 20" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_21" data-name="Rectangle 21" width="5.336" height="5.336" rx="1" transform="translate(58.888 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_22" data-name="Rectangle 22" width="5.336" height="5.336" rx="1" transform="translate(65.212 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_23" data-name="Rectangle 23" width="5.336" height="5.336" rx="1" transform="translate(71.536 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_24" data-name="Rectangle 24" width="5.336" height="5.336" rx="1" transform="translate(77.859 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_25" data-name="Rectangle 25" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
-        </g>
-        <g id="Group_4" data-name="Group 4" transform="translate(91.05 9.546) rotate(180)">
-          <path id="Path_53" data-name="Path 53" d="M1.093,0H6.219A1.093,1.093,0,0,1,7.312,1.093v3.15A1.093,1.093,0,0,1,6.219,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-          <rect id="Rectangle_26" data-name="Rectangle 26" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_27" data-name="Rectangle 27" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_28" data-name="Rectangle 28" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_29" data-name="Rectangle 29" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_30" data-name="Rectangle 30" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_31" data-name="Rectangle 31" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_32" data-name="Rectangle 32" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_33" data-name="Rectangle 33" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_34" data-name="Rectangle 34" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_35" data-name="Rectangle 35" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_36" data-name="Rectangle 36" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_37" data-name="Rectangle 37" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_38" data-name="Rectangle 38" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_39" data-name="Rectangle 39" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_40" data-name="Rectangle 40" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_41" data-name="Rectangle 41" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_42" data-name="Rectangle 42" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_43" data-name="Rectangle 43" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_44" data-name="Rectangle 44" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_45" data-name="Rectangle 45" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_46" data-name="Rectangle 46" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_47" data-name="Rectangle 47" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_48" data-name="Rectangle 48" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_49" data-name="Rectangle 49" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_50" data-name="Rectangle 50" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_51" data-name="Rectangle 51" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
-        </g>
-        <g id="Group_6" data-name="Group 6" transform="translate(1.531 16.584)">
-          <path id="Path_54" data-name="Path 54" d="M1.093,0h7.3A1.093,1.093,0,0,1,9.485,1.093v3.15A1.093,1.093,0,0,1,8.392,5.336h-7.3A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-          <g id="Group_5" data-name="Group 5" transform="translate(10.671 0)">
-            <rect id="Rectangle_52" data-name="Rectangle 52" width="5.336" height="5.336" rx="1" fill="#4a4a4a"/>
-            <rect id="Rectangle_53" data-name="Rectangle 53" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_54" data-name="Rectangle 54" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_55" data-name="Rectangle 55" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_56" data-name="Rectangle 56" width="5.336" height="5.336" rx="1" transform="translate(25.295 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_57" data-name="Rectangle 57" width="5.336" height="5.336" rx="1" transform="translate(31.619 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_58" data-name="Rectangle 58" width="5.336" height="5.336" rx="1" transform="translate(37.942 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_59" data-name="Rectangle 59" width="5.336" height="5.336" rx="1" transform="translate(44.265 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_60" data-name="Rectangle 60" width="5.336" height="5.336" rx="1" transform="translate(50.589 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_61" data-name="Rectangle 61" width="5.336" height="5.336" rx="1" transform="translate(56.912 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_62" data-name="Rectangle 62" width="5.336" height="5.336" rx="1" transform="translate(63.236 0)" fill="#4a4a4a"/>
-          </g>
-          <path id="Path_55" data-name="Path 55" d="M1.094,0H8A1.093,1.093,0,0,1,9.091,1.093v3.15A1.093,1.093,0,0,1,8,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(80.428 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-        </g>
-        <g id="Group_7" data-name="Group 7" transform="translate(1.531 29.627)">
-          <rect id="Rectangle_63" data-name="Rectangle 63" width="5.336" height="5.336" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_64" data-name="Rectangle 64" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_65" data-name="Rectangle 65" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_66" data-name="Rectangle 66" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
-          <path id="Path_56" data-name="Path 56" d="M1.093,0H31.515a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.244V1.093A1.093,1.093,0,0,1,1.093,0ZM34.687,0h3.942a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H34.687a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,34.687,0Z" transform="translate(25.294 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-          <rect id="Rectangle_67" data-name="Rectangle 67" width="5.336" height="5.336" rx="1" transform="translate(66.003 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_68" data-name="Rectangle 68" width="5.336" height="5.336" rx="1" transform="translate(72.327 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_69" data-name="Rectangle 69" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
-          <path id="Path_57" data-name="Path 57" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(83.59 2.273) rotate(180)" fill="#4a4a4a"/>
-          <path id="Path_58" data-name="Path 58" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(78.255 3.063)" fill="#4a4a4a"/>
-        </g>
-        <rect id="Rectangle_70" data-name="Rectangle 70" width="88.927" height="2.371" rx="1.085" transform="translate(1.925 1.17)" fill="#4a4a4a"/>
-        <rect id="Rectangle_71" data-name="Rectangle 71" width="4.986" height="1.581" rx="0.723" transform="translate(4.1 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_72" data-name="Rectangle 72" width="4.986" height="1.581" rx="0.723" transform="translate(10.923 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_73" data-name="Rectangle 73" width="4.986" height="1.581" rx="0.723" transform="translate(16.173 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_74" data-name="Rectangle 74" width="4.986" height="1.581" rx="0.723" transform="translate(21.421 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_75" data-name="Rectangle 75" width="4.986" height="1.581" rx="0.723" transform="translate(26.671 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_76" data-name="Rectangle 76" width="4.986" height="1.581" rx="0.723" transform="translate(33.232 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_77" data-name="Rectangle 77" width="4.986" height="1.581" rx="0.723" transform="translate(38.48 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_78" data-name="Rectangle 78" width="4.986" height="1.581" rx="0.723" transform="translate(43.73 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_79" data-name="Rectangle 79" width="4.986" height="1.581" rx="0.723" transform="translate(48.978 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_80" data-name="Rectangle 80" width="4.986" height="1.581" rx="0.723" transform="translate(55.54 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_81" data-name="Rectangle 81" width="4.986" height="1.581" rx="0.723" transform="translate(60.788 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_82" data-name="Rectangle 82" width="4.986" height="1.581" rx="0.723" transform="translate(66.038 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_83" data-name="Rectangle 83" width="4.986" height="1.581" rx="0.723" transform="translate(72.599 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_84" data-name="Rectangle 84" width="4.986" height="1.581" rx="0.723" transform="translate(77.847 1.566)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_85" data-name="Rectangle 85" width="4.986" height="1.581" rx="0.723" transform="translate(83.097 1.566)" fill="#d8d8d8" opacity="0.136"/>
-      </g>
-      <path id="Path_59" data-name="Path 59" d="M146.71,159.855a5.439,5.439,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(6.275 -6.025)" fill="#44d860" fill-rule="evenodd"/>
-      <path id="Path_60" data-name="Path 60" d="M83,124.855h43.71V103H83Z" transform="translate(4.42 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_61" data-name="Path 61" d="M134.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.811,2.811,0,0,0,.349.035" transform="translate(7.202 -9.377)" fill="#44d860" fill-rule="evenodd"/>
-      <path id="Path_62" data-name="Path 62" d="M143.232,42.33a2.967,2.967,0,0,1-.535-.055,2.754,2.754,0,0,1-.514-.153,2.838,2.838,0,0,1-.471-.251,4.139,4.139,0,0,1-.415-.339,3.2,3.2,0,0,1-.338-.415A2.7,2.7,0,0,1,140.5,39.6a2.968,2.968,0,0,1,.055-.535,3.152,3.152,0,0,1,.152-.514,2.874,2.874,0,0,1,.252-.47,2.633,2.633,0,0,1,.753-.754,2.837,2.837,0,0,1,.471-.251,2.753,2.753,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,4.019,4.019,0,0,1,.339.415,2.786,2.786,0,0,1,.251.47,2.864,2.864,0,0,1,.208,1.049,2.77,2.77,0,0,1-.8,1.934,4.139,4.139,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459m21.855-1.366a2.789,2.789,0,0,1-1.935-.8,4.162,4.162,0,0,1-.338-.415,2.7,2.7,0,0,1-.459-1.519,2.789,2.789,0,0,1,.8-1.934,4.139,4.139,0,0,1,.415-.339,2.838,2.838,0,0,1,.471-.251,2.752,2.752,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,2.79,2.79,0,0,1,.8,1.934,3.069,3.069,0,0,1-.055.535,2.779,2.779,0,0,1-.153.514,3.885,3.885,0,0,1-.251.47,4.02,4.02,0,0,1-.339.415,4.138,4.138,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459" transform="translate(9.753 -15.532)" fill-rule="evenodd"/>
-    </g>
-  </g>
-</svg>
diff --git a/docs/old-docs/static/img/undraw_docusaurus_react.svg b/docs/old-docs/static/img/undraw_docusaurus_react.svg
deleted file mode 100644
index 94b5cf08..00000000
--- a/docs/old-docs/static/img/undraw_docusaurus_react.svg
+++ /dev/null
@@ -1,170 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1041.277" height="554.141" viewBox="0 0 1041.277 554.141">
-  <title>Powered by React</title>
-  <g id="Group_24" data-name="Group 24" transform="translate(-440 -263)">
-    <g id="Group_23" data-name="Group 23" transform="translate(439.989 262.965)">
-      <path id="Path_299" data-name="Path 299" d="M1040.82,611.12q-1.74,3.75-3.47,7.4-2.7,5.67-5.33,11.12c-.78,1.61-1.56,3.19-2.32,4.77-8.6,17.57-16.63,33.11-23.45,45.89A73.21,73.21,0,0,1,942.44,719l-151.65,1.65h-1.6l-13,.14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107,1.16-95.51,1-11.11.12-69,.75H429l-44.75.48h-.48l-141.5,1.53-42.33.46a87.991,87.991,0,0,1-10.79-.54h0c-1.22-.14-2.44-.3-3.65-.49a87.38,87.38,0,0,1-51.29-27.54C116,678.37,102.75,655,93.85,629.64q-1.93-5.49-3.6-11.12C59.44,514.37,97,380,164.6,290.08q4.25-5.64,8.64-11l.07-.08c20.79-25.52,44.1-46.84,68.93-62,44-26.91,92.75-34.49,140.7-11.9,40.57,19.12,78.45,28.11,115.17,30.55,3.71.24,7.42.42,11.11.53,84.23,2.65,163.17-27.7,255.87-47.29,3.69-.78,7.39-1.55,11.12-2.28,66.13-13.16,139.49-20.1,226.73-5.51a189.089,189.089,0,0,1,26.76,6.4q5.77,1.86,11.12,4c41.64,16.94,64.35,48.24,74,87.46q1.37,5.46,2.37,11.11C1134.3,384.41,1084.19,518.23,1040.82,611.12Z" transform="translate(-79.34 -172.91)" fill="#f2f2f2"/>
-      <path id="Path_300" data-name="Path 300" d="M576.36,618.52a95.21,95.21,0,0,1-1.87,11.12h93.7V618.52Zm-78.25,62.81,11.11-.09V653.77c-3.81-.17-7.52-.34-11.11-.52ZM265.19,618.52v11.12h198.5V618.52ZM1114.87,279h-74V191.51q-5.35-2.17-11.12-4V279H776.21V186.58c-3.73.73-7.43,1.5-11.12,2.28V279H509.22V236.15c-3.69-.11-7.4-.29-11.11-.53V279H242.24V217c-24.83,15.16-48.14,36.48-68.93,62h-.07v.08q-4.4,5.4-8.64,11h8.64V618.52h-83q1.66,5.63,3.6,11.12h79.39v93.62a87,87,0,0,0,12.2,2.79c1.21.19,2.43.35,3.65.49h0a87.991,87.991,0,0,0,10.79.54l42.33-.46v-97H498.11v94.21l11.11-.12V629.64H765.09V721l11.12-.12V629.64H1029.7v4.77c.76-1.58,1.54-3.16,2.32-4.77q2.63-5.45,5.33-11.12,1.73-3.64,3.47-7.4v-321h76.42Q1116.23,284.43,1114.87,279ZM242.24,618.52V290.08H498.11V618.52Zm267,0V290.08H765.09V618.52Zm520.48,0H776.21V290.08H1029.7Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
-      <path id="Path_301" data-name="Path 301" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" fill="#65617d"/>
-      <path id="Path_302" data-name="Path 302" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" opacity="0.2"/>
-      <path id="Path_303" data-name="Path 303" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
-      <path id="Path_304" data-name="Path 304" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
-      <path id="Path_305" data-name="Path 305" d="M377.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
-      <rect id="Rectangle_137" data-name="Rectangle 137" width="47.17" height="31.5" transform="translate(680.92 483.65)" fill="#3f3d56"/>
-      <rect id="Rectangle_138" data-name="Rectangle 138" width="47.17" height="31.5" transform="translate(680.92 483.65)" opacity="0.1"/>
-      <rect id="Rectangle_139" data-name="Rectangle 139" width="47.17" height="31.5" transform="translate(678.92 483.65)" fill="#3f3d56"/>
-      <path id="Path_306" data-name="Path 306" d="M298.09,483.65v4.97l-47.17,1.26v-6.23Z" opacity="0.1"/>
-      <path id="Path_307" data-name="Path 307" d="M460.69,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6a4,4,0,0,1,3.95,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
-      <path id="Path_308" data-name="Path 308" d="M265.19,481.32v181.2h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
-      <path id="Path_309" data-name="Path 309" d="M194.59,319.15h177.5V467.4l-177.5,4Z" fill="#39374d"/>
-      <path id="Path_310" data-name="Path 310" d="M726.09,483.65v6.41l-47.17-1.26v-5.15Z" opacity="0.1"/>
-      <path id="Path_311" data-name="Path 311" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0L672,657.42a4,4,0,0,1-3.85-3.95V485.27a4,4,0,0,1,3.95-3.95H863.7a4,4,0,0,1,3.99,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
-      <path id="Path_312" data-name="Path 312" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0V481.32h0a4,4,0,0,1,4,3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
-      <path id="Path_313" data-name="Path 313" d="M775.59,319.15H598.09V467.4l177.5,4Z" fill="#39374d"/>
-      <path id="Path_314" data-name="Path 314" d="M663.19,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h0a4,4,0,0,1-4-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6A4,4,0,0,1,663.19,485.27Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
-      <path id="Path_315" data-name="Path 315" d="M397.09,319.15h177.5V467.4l-177.5,4Z" fill="#4267b2"/>
-      <path id="Path_316" data-name="Path 316" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l202.51-1.33h.48l40.99-.28h.19l283.08-1.87h.29l.17-.01h.47l4.79-.03h1.46l74.49-.5,4.4-.02.98-.01Z" opacity="0.1"/>
-      <circle id="Ellipse_111" data-name="Ellipse 111" cx="51.33" cy="51.33" r="51.33" transform="translate(435.93 246.82)" fill="#fbbebe"/>
-      <path id="Path_317" data-name="Path 317" d="M617.94,550.07s-99.5,12-90,0c3.44-4.34,4.39-17.2,4.2-31.85-.06-4.45-.22-9.06-.45-13.65-1.1-22-3.75-43.5-3.75-43.5s87-41,77-8.5c-4,13.13-2.69,31.57.35,48.88.89,5.05,1.92,10,3,14.7a344.66,344.66,0,0,0,9.65,33.92Z" transform="translate(-79.34 -172.91)" fill="#fbbebe"/>
-      <path id="Path_318" data-name="Path 318" d="M585.47,546c11.51-2.13,23.7-6,34.53-1.54,2.85,1.17,5.47,2.88,8.39,3.86s6.12,1.22,9.16,1.91c10.68,2.42,19.34,10.55,24.9,20s8.44,20.14,11.26,30.72l6.9,25.83c6,22.45,12,45.09,13.39,68.3a2437.506,2437.506,0,0,1-250.84,1.43c5.44-10.34,11-21.31,10.54-33s-7.19-23.22-4.76-34.74c1.55-7.34,6.57-13.39,9.64-20.22,8.75-19.52,1.94-45.79,17.32-60.65,6.92-6.68,17-9.21,26.63-8.89,12.28.41,24.85,4.24,37,6.11C555.09,547.48,569.79,548.88,585.47,546Z" transform="translate(-79.34 -172.91)" fill="#ff6584"/>
-      <path id="Path_319" data-name="Path 319" d="M716.37,657.17l-.1,1.43v.1l-.17,2.3-1.33,18.51-1.61,22.3-.46,6.28-1,13.44v.17l-107,1-175.59,1.9v.84h-.14v-1.12l.45-14.36.86-28.06.74-23.79.07-2.37a10.53,10.53,0,0,1,11.42-10.17c4.72.4,10.85.89,18.18,1.41l3,.22c42.33,2.94,120.56,6.74,199.5,2,1.66-.09,3.33-.19,5-.31,12.24-.77,24.47-1.76,36.58-3a10.53,10.53,0,0,1,11.6,11.23Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
-      <path id="Path_320" data-name="Path 320" d="M429.08,725.44v-.84l175.62-1.91,107-1h.3v-.17l1-13.44.43-6,1.64-22.61,1.29-17.9v-.44a10.617,10.617,0,0,0-.11-2.47.3.3,0,0,0,0-.1,10.391,10.391,0,0,0-2-4.64,10.54,10.54,0,0,0-9.42-4c-12.11,1.24-24.34,2.23-36.58,3-1.67.12-3.34.22-5,.31-78.94,4.69-157.17.89-199.5-2l-3-.22c-7.33-.52-13.46-1-18.18-1.41a10.54,10.54,0,0,0-11.24,8.53,11,11,0,0,0-.18,1.64l-.68,22.16L429.54,710l-.44,14.36v1.12Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
-      <path id="Path_321" data-name="Path 321" d="M716.67,664.18l-1.23,15.33-1.83,22.85-.46,5.72-1,12.81-.06.64v.17h0l-.15,1.48.11-1.48h-.29l-107,1-175.65,1.9v-.28l.49-14.36,1-28.06.64-18.65A6.36,6.36,0,0,1,434.3,658a6.25,6.25,0,0,1,3.78-.9c2.1.17,4.68.37,7.69.59,4.89.36,10.92.78,17.94,1.22,13,.82,29.31,1.7,48,2.42,52,2,122.2,2.67,188.88-3.17,3-.26,6.1-.55,9.13-.84a6.26,6.26,0,0,1,3.48.66,5.159,5.159,0,0,1,.86.54,6.14,6.14,0,0,1,2,2.46,3.564,3.564,0,0,1,.25.61A6.279,6.279,0,0,1,716.67,664.18Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
-      <path id="Path_322" data-name="Path 322" d="M377.44,677.87v3.19a6.13,6.13,0,0,1-3.5,5.54l-40.1.77a6.12,6.12,0,0,1-3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
-      <path id="Path_323" data-name="Path 323" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
-      <path id="Path_324" data-name="Path 324" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" opacity="0.1"/>
-      <path id="Path_325" data-name="Path 325" d="M300.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
-      <path id="Path_326" data-name="Path 326" d="M758.56,679.87v3.19a6.13,6.13,0,0,0,3.5,5.54l40.1.77a6.12,6.12,0,0,0,3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
-      <path id="Path_327" data-name="Path 327" d="M678.72,517.57l52.25,1V509.9l-52.25-1Z" opacity="0.1"/>
-      <path id="Path_328" data-name="Path 328" d="M676.72,517.57l52.25,1V509.9l-52.25-1Z" fill="#3f3d56"/>
-      <path id="Path_329" data-name="Path 329" d="M534.13,486.79c.08,7-3.16,13.6-5.91,20.07a163.491,163.491,0,0,0-12.66,74.71c.73,11,2.58,22,.73,32.9s-8.43,21.77-19,24.9c17.53,10.45,41.26,9.35,57.76-2.66,8.79-6.4,15.34-15.33,21.75-24.11a97.86,97.86,0,0,1-13.31,44.75A103.43,103.43,0,0,0,637,616.53c4.31-5.81,8.06-12.19,9.72-19.23,3.09-13-1.22-26.51-4.51-39.5a266.055,266.055,0,0,1-6.17-33c-.43-3.56-.78-7.22.1-10.7,1-4.07,3.67-7.51,5.64-11.22,5.6-10.54,5.73-23.3,2.86-34.88s-8.49-22.26-14.06-32.81c-4.46-8.46-9.3-17.31-17.46-22.28-5.1-3.1-11-4.39-16.88-5.64l-25.37-5.43c-5.55-1.19-11.26-2.38-16.87-1.51-9.47,1.48-16.14,8.32-22,15.34-4.59,5.46-15.81,15.71-16.6,22.86-.72,6.59,5.1,17.63,6.09,24.58,1.3,9,2.22,6,7.3,11.52C532,478.05,534.07,482,534.13,486.79Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
-    </g>
-    <g id="docusaurus_keytar" transform="translate(670.271 615.768)">
-      <path id="Path_40" data-name="Path 40" d="M99,52h43.635V69.662H99Z" transform="translate(-49.132 -33.936)" fill="#fff" fill-rule="evenodd"/>
-      <path id="Path_41" data-name="Path 41" d="M13.389,158.195A10.377,10.377,0,0,1,4.4,153a10.377,10.377,0,0,0,8.988,15.584H23.779V158.195Z" transform="translate(-3 -82.47)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_42" data-name="Path 42" d="M66.967,38.083l36.373-2.273V30.615A10.389,10.389,0,0,0,92.95,20.226H46.2l-1.3-2.249a1.5,1.5,0,0,0-2.6,0L41,20.226l-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-.034,0-2.152-2.151a1.5,1.5,0,0,0-2.508.672L25.21,21.4l-2.7-.723a1.5,1.5,0,0,0-1.836,1.837l.722,2.7-2.65.71a1.5,1.5,0,0,0-.673,2.509l2.152,2.152c0,.011,0,.022,0,.033l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6L20.226,41l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3A10.389,10.389,0,0,0,30.615,103.34H92.95A10.389,10.389,0,0,0,103.34,92.95V51.393L66.967,49.12a5.53,5.53,0,0,1,0-11.038" transform="translate(-9.836 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_43" data-name="Path 43" d="M143,163.779h15.584V143H143Z" transform="translate(-70.275 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_44" data-name="Path 44" d="M173.779,148.389a2.582,2.582,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-75.08 -75.262)" fill="#44d860" fill-rule="evenodd"/>
-      <path id="Path_45" data-name="Path 45" d="M153,113.389h15.584V103H153Z" transform="translate(-75.08 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_46" data-name="Path 46" d="M183.389,108.944a1.3,1.3,0,1,0,0-2.6,1.336,1.336,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.337,1.337,0,0,0,.166.017" transform="translate(-84.691 -57.894)" fill="#44d860" fill-rule="evenodd"/>
-      <path id="Path_47" data-name="Path 47" d="M52.188,48.292a1.3,1.3,0,0,1-1.3-1.3,3.9,3.9,0,0,0-7.792,0,1.3,1.3,0,1,1-2.6,0,6.493,6.493,0,0,1,12.987,0,1.3,1.3,0,0,1-1.3,1.3" transform="translate(-21.02 -28.41)" fill-rule="evenodd"/>
-      <path id="Path_48" data-name="Path 48" d="M103,139.752h31.168a10.389,10.389,0,0,0,10.389-10.389V93H113.389A10.389,10.389,0,0,0,103,103.389Z" transform="translate(-51.054 -53.638)" fill="#ffff50" fill-rule="evenodd"/>
-      <path id="Path_49" data-name="Path 49" d="M141.1,94.017H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0-25.877H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.293H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m7.782-47.993c-.006,0-.011,0-.018,0-1.605.055-2.365,1.66-3.035,3.077-.7,1.48-1.24,2.443-2.126,2.414-.981-.035-1.542-1.144-2.137-2.317-.683-1.347-1.462-2.876-3.1-2.819-1.582.054-2.344,1.451-3.017,2.684-.715,1.313-1.2,2.112-2.141,2.075-1-.036-1.533-.938-2.149-1.981-.686-1.162-1.479-2.467-3.084-2.423-1.555.053-2.319,1.239-2.994,2.286-.713,1.106-1.213,1.781-2.164,1.741-1.025-.036-1.554-.784-2.167-1.65-.688-.973-1.463-2.074-3.062-2.021a3.815,3.815,0,0,0-2.959,1.879c-.64.812-1.14,1.456-2.2,1.415a.52.52,0,0,0-.037,1.039,3.588,3.588,0,0,0,3.05-1.811c.611-.777,1.139-1.448,2.178-1.483,1-.043,1.47.579,2.179,1.582.674.953,1.438,2.033,2.977,2.089,1.612.054,2.387-1.151,3.074-2.217.614-.953,1.144-1.775,2.156-1.81.931-.035,1.438.7,2.153,1.912.674,1.141,1.437,2.434,3.006,2.491,1.623.056,2.407-1.361,3.09-2.616.592-1.085,1.15-2.109,2.14-2.143.931-.022,1.417.829,2.135,2.249.671,1.326,1.432,2.828,3.026,2.886l.088,0c1.592,0,2.347-1.6,3.015-3.01.592-1.252,1.152-2.431,2.113-2.479Z" transform="translate(-55.378 -38.552)" fill-rule="evenodd"/>
-      <path id="Path_50" data-name="Path 50" d="M83,163.779h20.779V143H83Z" transform="translate(-41.443 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 51.971, 43.3)">
-        <rect id="Rectangle_3" data-name="Rectangle 3" width="43.906" height="17.333" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
-        <g id="Group_2" data-name="Group 2" transform="translate(0.728 10.948)">
-          <rect id="Rectangle_4" data-name="Rectangle 4" width="2.537" height="2.537" rx="1" transform="translate(7.985 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_5" data-name="Rectangle 5" width="2.537" height="2.537" rx="1" transform="translate(10.991 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_6" data-name="Rectangle 6" width="2.537" height="2.537" rx="1" transform="translate(13.997 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_7" data-name="Rectangle 7" width="2.537" height="2.537" rx="1" transform="translate(17.003 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_8" data-name="Rectangle 8" width="2.537" height="2.537" rx="1" transform="translate(20.009 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_9" data-name="Rectangle 9" width="2.537" height="2.537" rx="1" transform="translate(23.015 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_10" data-name="Rectangle 10" width="2.537" height="2.537" rx="1" transform="translate(26.021 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_11" data-name="Rectangle 11" width="2.537" height="2.537" rx="1" transform="translate(29.028 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_12" data-name="Rectangle 12" width="2.537" height="2.537" rx="1" transform="translate(32.034 0)" fill="#4a4a4a"/>
-          <path id="Path_51" data-name="Path 51" d="M.519,0H6.9A.519.519,0,0,1,7.421.52v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0ZM35.653,0h6.383a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H35.652a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,35.652,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-        </g>
-        <g id="Group_3" data-name="Group 3" transform="translate(0.728 4.878)">
-          <path id="Path_52" data-name="Path 52" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-          <rect id="Rectangle_13" data-name="Rectangle 13" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_14" data-name="Rectangle 14" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_15" data-name="Rectangle 15" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_16" data-name="Rectangle 16" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_17" data-name="Rectangle 17" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_18" data-name="Rectangle 18" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_19" data-name="Rectangle 19" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_20" data-name="Rectangle 20" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_21" data-name="Rectangle 21" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_22" data-name="Rectangle 22" width="2.537" height="2.537" rx="1" transform="translate(31 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_23" data-name="Rectangle 23" width="2.537" height="2.537" rx="1" transform="translate(34.006 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_24" data-name="Rectangle 24" width="2.537" height="2.537" rx="1" transform="translate(37.012 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_25" data-name="Rectangle 25" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
-        </g>
-        <g id="Group_4" data-name="Group 4" transform="translate(43.283 4.538) rotate(180)">
-          <path id="Path_53" data-name="Path 53" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-          <rect id="Rectangle_26" data-name="Rectangle 26" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_27" data-name="Rectangle 27" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_28" data-name="Rectangle 28" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_29" data-name="Rectangle 29" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_30" data-name="Rectangle 30" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_31" data-name="Rectangle 31" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_32" data-name="Rectangle 32" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_33" data-name="Rectangle 33" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_34" data-name="Rectangle 34" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_35" data-name="Rectangle 35" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_36" data-name="Rectangle 36" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_37" data-name="Rectangle 37" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_38" data-name="Rectangle 38" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_39" data-name="Rectangle 39" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_40" data-name="Rectangle 40" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_41" data-name="Rectangle 41" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_42" data-name="Rectangle 42" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_43" data-name="Rectangle 43" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_44" data-name="Rectangle 44" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_45" data-name="Rectangle 45" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_46" data-name="Rectangle 46" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_47" data-name="Rectangle 47" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_48" data-name="Rectangle 48" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_49" data-name="Rectangle 49" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_50" data-name="Rectangle 50" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_51" data-name="Rectangle 51" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
-        </g>
-        <g id="Group_6" data-name="Group 6" transform="translate(0.728 7.883)">
-          <path id="Path_54" data-name="Path 54" d="M.519,0h3.47a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-          <g id="Group_5" data-name="Group 5" transform="translate(5.073 0)">
-            <rect id="Rectangle_52" data-name="Rectangle 52" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_53" data-name="Rectangle 53" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_54" data-name="Rectangle 54" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_55" data-name="Rectangle 55" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_56" data-name="Rectangle 56" width="2.537" height="2.537" rx="1" transform="translate(12.025 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_57" data-name="Rectangle 57" width="2.537" height="2.537" rx="1" transform="translate(15.031 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_58" data-name="Rectangle 58" width="2.537" height="2.537" rx="1" transform="translate(18.037 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_59" data-name="Rectangle 59" width="2.537" height="2.537" rx="1" transform="translate(21.042 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_60" data-name="Rectangle 60" width="2.537" height="2.537" rx="1" transform="translate(24.049 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_61" data-name="Rectangle 61" width="2.537" height="2.537" rx="1" transform="translate(27.055 0)" fill="#4a4a4a"/>
-            <rect id="Rectangle_62" data-name="Rectangle 62" width="2.537" height="2.537" rx="1" transform="translate(30.061 0)" fill="#4a4a4a"/>
-          </g>
-          <path id="Path_55" data-name="Path 55" d="M.52,0H3.8a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(38.234 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-        </g>
-        <g id="Group_7" data-name="Group 7" transform="translate(0.728 14.084)">
-          <rect id="Rectangle_63" data-name="Rectangle 63" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_64" data-name="Rectangle 64" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_65" data-name="Rectangle 65" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_66" data-name="Rectangle 66" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
-          <path id="Path_56" data-name="Path 56" d="M.519,0H14.981A.519.519,0,0,1,15.5.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.018V.519A.519.519,0,0,1,.519,0Zm15.97,0h1.874a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H16.489a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,16.489,0Z" transform="translate(12.024 0)" fill="#4a4a4a" fill-rule="evenodd"/>
-          <rect id="Rectangle_67" data-name="Rectangle 67" width="2.537" height="2.537" rx="1" transform="translate(31.376 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_68" data-name="Rectangle 68" width="2.537" height="2.537" rx="1" transform="translate(34.382 0)" fill="#4a4a4a"/>
-          <rect id="Rectangle_69" data-name="Rectangle 69" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
-          <path id="Path_57" data-name="Path 57" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(39.736 1.08) rotate(180)" fill="#4a4a4a"/>
-          <path id="Path_58" data-name="Path 58" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(37.2 1.456)" fill="#4a4a4a"/>
-        </g>
-        <rect id="Rectangle_70" data-name="Rectangle 70" width="42.273" height="1.127" rx="0.564" transform="translate(0.915 0.556)" fill="#4a4a4a"/>
-        <rect id="Rectangle_71" data-name="Rectangle 71" width="2.37" height="0.752" rx="0.376" transform="translate(1.949 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_72" data-name="Rectangle 72" width="2.37" height="0.752" rx="0.376" transform="translate(5.193 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_73" data-name="Rectangle 73" width="2.37" height="0.752" rx="0.376" transform="translate(7.688 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_74" data-name="Rectangle 74" width="2.37" height="0.752" rx="0.376" transform="translate(10.183 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_75" data-name="Rectangle 75" width="2.37" height="0.752" rx="0.376" transform="translate(12.679 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_76" data-name="Rectangle 76" width="2.37" height="0.752" rx="0.376" transform="translate(15.797 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_77" data-name="Rectangle 77" width="2.37" height="0.752" rx="0.376" transform="translate(18.292 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_78" data-name="Rectangle 78" width="2.37" height="0.752" rx="0.376" transform="translate(20.788 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_79" data-name="Rectangle 79" width="2.37" height="0.752" rx="0.376" transform="translate(23.283 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_80" data-name="Rectangle 80" width="2.37" height="0.752" rx="0.376" transform="translate(26.402 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_81" data-name="Rectangle 81" width="2.37" height="0.752" rx="0.376" transform="translate(28.897 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_82" data-name="Rectangle 82" width="2.37" height="0.752" rx="0.376" transform="translate(31.393 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_83" data-name="Rectangle 83" width="2.37" height="0.752" rx="0.376" transform="translate(34.512 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_84" data-name="Rectangle 84" width="2.37" height="0.752" rx="0.376" transform="translate(37.007 0.744)" fill="#d8d8d8" opacity="0.136"/>
-        <rect id="Rectangle_85" data-name="Rectangle 85" width="2.37" height="0.752" rx="0.376" transform="translate(39.502 0.744)" fill="#d8d8d8" opacity="0.136"/>
-      </g>
-      <path id="Path_59" data-name="Path 59" d="M123.779,148.389a2.583,2.583,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-51.054 -75.262)" fill="#44d860" fill-rule="evenodd"/>
-      <path id="Path_60" data-name="Path 60" d="M83,113.389h20.779V103H83Z" transform="translate(-41.443 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
-      <path id="Path_61" data-name="Path 61" d="M123.389,108.944a1.3,1.3,0,1,0,0-2.6,1.338,1.338,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.335,1.335,0,0,0,.166.017" transform="translate(-55.859 -57.894)" fill="#44d860" fill-rule="evenodd"/>
-      <path id="Path_62" data-name="Path 62" d="M141.8,38.745a1.41,1.41,0,0,1-.255-.026,1.309,1.309,0,0,1-.244-.073,1.349,1.349,0,0,1-.224-.119,1.967,1.967,0,0,1-.2-.161,1.52,1.52,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.41,1.41,0,0,1,.026-.255,1.5,1.5,0,0,1,.072-.244,1.364,1.364,0,0,1,.12-.223,1.252,1.252,0,0,1,.358-.358,1.349,1.349,0,0,1,.224-.119,1.309,1.309,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.968,1.968,0,0,1,.2.161,1.908,1.908,0,0,1,.161.2,1.322,1.322,0,0,1,.12.223,1.361,1.361,0,0,1,.1.5,1.317,1.317,0,0,1-.379.919,1.968,1.968,0,0,1-.2.161,1.346,1.346,0,0,1-.223.119,1.332,1.332,0,0,1-.5.1m10.389-.649a1.326,1.326,0,0,1-.92-.379,1.979,1.979,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.326,1.326,0,0,1,.379-.919,1.967,1.967,0,0,1,.2-.161,1.351,1.351,0,0,1,.224-.119,1.308,1.308,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.967,1.967,0,0,1,.2.161,1.326,1.326,0,0,1,.379.919,1.461,1.461,0,0,1-.026.255,1.323,1.323,0,0,1-.073.244,1.847,1.847,0,0,1-.119.223,1.911,1.911,0,0,1-.161.2,1.967,1.967,0,0,1-.2.161,1.294,1.294,0,0,1-.722.218" transform="translate(-69.074 -26.006)" fill-rule="evenodd"/>
-    </g>
-    <g id="React-icon" transform="translate(906.3 541.56)">
-      <path id="Path_330" data-name="Path 330" d="M263.668,117.179c0-5.827-7.3-11.35-18.487-14.775,2.582-11.4,1.434-20.477-3.622-23.382a7.861,7.861,0,0,0-4.016-1v4a4.152,4.152,0,0,1,2.044.466c2.439,1.4,3.5,6.724,2.672,13.574-.2,1.685-.52,3.461-.914,5.272a86.9,86.9,0,0,0-11.386-1.954,87.469,87.469,0,0,0-7.459-8.965c5.845-5.433,11.332-8.41,15.062-8.41V78h0c-4.931,0-11.386,3.514-17.913,9.611-6.527-6.061-12.982-9.539-17.913-9.539v4c3.712,0,9.216,2.959,15.062,8.356a84.687,84.687,0,0,0-7.405,8.947,83.732,83.732,0,0,0-11.4,1.972c-.412-1.793-.717-3.532-.932-5.2-.843-6.85.2-12.175,2.618-13.592a3.991,3.991,0,0,1,2.062-.466v-4h0a8,8,0,0,0-4.052,1c-5.039,2.9-6.168,11.96-3.568,23.328-11.153,3.443-18.415,8.947-18.415,14.757,0,5.828,7.3,11.35,18.487,14.775-2.582,11.4-1.434,20.477,3.622,23.382a7.882,7.882,0,0,0,4.034,1c4.931,0,11.386-3.514,17.913-9.611,6.527,6.061,12.982,9.539,17.913,9.539a8,8,0,0,0,4.052-1c5.039-2.9,6.168-11.96,3.568-23.328C256.406,128.511,263.668,122.988,263.668,117.179Zm-23.346-11.96c-.663,2.313-1.488,4.7-2.421,7.083-.735-1.434-1.506-2.869-2.349-4.3-.825-1.434-1.7-2.833-2.582-4.2C235.517,104.179,237.974,104.645,240.323,105.219Zm-8.212,19.1c-1.4,2.421-2.833,4.716-4.321,6.85-2.672.233-5.379.359-8.1.359-2.708,0-5.415-.126-8.069-.341q-2.232-3.2-4.339-6.814-2.044-3.523-3.73-7.136c1.112-2.4,2.367-4.805,3.712-7.154,1.4-2.421,2.833-4.716,4.321-6.85,2.672-.233,5.379-.359,8.1-.359,2.708,0,5.415.126,8.069.341q2.232,3.2,4.339,6.814,2.044,3.523,3.73,7.136C234.692,119.564,233.455,121.966,232.11,124.315Zm5.792-2.331c.968,2.4,1.793,4.805,2.474,7.136-2.349.574-4.823,1.058-7.387,1.434.879-1.381,1.757-2.8,2.582-4.25C236.4,124.871,237.167,123.419,237.9,121.984ZM219.72,141.116a73.921,73.921,0,0,1-4.985-5.738c1.614.072,3.263.126,4.931.126,1.685,0,3.353-.036,4.985-.126A69.993,69.993,0,0,1,219.72,141.116ZM206.38,130.555c-2.546-.377-5-.843-7.352-1.417.663-2.313,1.488-4.7,2.421-7.083.735,1.434,1.506,2.869,2.349,4.3S205.5,129.192,206.38,130.555ZM219.63,93.241a73.924,73.924,0,0,1,4.985,5.738c-1.614-.072-3.263-.126-4.931-.126-1.686,0-3.353.036-4.985.126A69.993,69.993,0,0,1,219.63,93.241ZM206.362,103.8c-.879,1.381-1.757,2.8-2.582,4.25-.825,1.434-1.6,2.869-2.331,4.3-.968-2.4-1.793-4.805-2.474-7.136C201.323,104.663,203.8,104.179,206.362,103.8Zm-16.227,22.449c-6.348-2.708-10.454-6.258-10.454-9.073s4.106-6.383,10.454-9.073c1.542-.663,3.228-1.255,4.967-1.811a86.122,86.122,0,0,0,4.034,10.92,84.9,84.9,0,0,0-3.981,10.866C193.38,127.525,191.694,126.915,190.134,126.252Zm9.647,25.623c-2.439-1.4-3.5-6.724-2.672-13.574.2-1.686.52-3.461.914-5.272a86.9,86.9,0,0,0,11.386,1.954,87.465,87.465,0,0,0,7.459,8.965c-5.845,5.433-11.332,8.41-15.062,8.41A4.279,4.279,0,0,1,199.781,151.875Zm42.532-13.663c.843,6.85-.2,12.175-2.618,13.592a3.99,3.99,0,0,1-2.062.466c-3.712,0-9.216-2.959-15.062-8.356a84.689,84.689,0,0,0,7.405-8.947,83.731,83.731,0,0,0,11.4-1.972A50.194,50.194,0,0,1,242.313,138.212Zm6.9-11.96c-1.542.663-3.228,1.255-4.967,1.811a86.12,86.12,0,0,0-4.034-10.92,84.9,84.9,0,0,0,3.981-10.866c1.775.556,3.461,1.165,5.039,1.829,6.348,2.708,10.454,6.258,10.454,9.073C259.67,119.994,255.564,123.562,249.216,126.252Z" fill="#61dafb"/>
-      <path id="Path_331" data-name="Path 331" d="M320.8,78.4Z" transform="translate(-119.082 -0.328)" fill="#61dafb"/>
-      <circle id="Ellipse_112" data-name="Ellipse 112" cx="8.194" cy="8.194" r="8.194" transform="translate(211.472 108.984)" fill="#61dafb"/>
-      <path id="Path_332" data-name="Path 332" d="M520.5,78.1Z" transform="translate(-282.975 -0.082)" fill="#61dafb"/>
-    </g>
-  </g>
-</svg>
diff --git a/docs/old-docs/static/img/undraw_docusaurus_tree.svg b/docs/old-docs/static/img/undraw_docusaurus_tree.svg
deleted file mode 100644
index d9161d33..00000000
--- a/docs/old-docs/static/img/undraw_docusaurus_tree.svg
+++ /dev/null
@@ -1,40 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="1129" height="663" viewBox="0 0 1129 663">
-  <title>Focus on What Matters</title>
-  <circle cx="321" cy="321" r="321" fill="#f2f2f2" />
-  <ellipse cx="559" cy="635.49998" rx="514" ry="27.50002" fill="#3f3d56" />
-  <ellipse cx="558" cy="627" rx="460" ry="22" opacity="0.2" />
-  <rect x="131" y="152.5" width="840" height="50" fill="#3f3d56" />
-  <path d="M166.5,727.3299A21.67009,21.67009,0,0,0,188.1701,749H984.8299A21.67009,21.67009,0,0,0,1006.5,727.3299V296h-840Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
-  <path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
-  <path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" opacity="0.2" />
-  <circle cx="181" cy="147.5" r="13" fill="#3f3d56" />
-  <circle cx="217" cy="147.5" r="13" fill="#3f3d56" />
-  <circle cx="253" cy="147.5" r="13" fill="#3f3d56" />
-  <rect x="168" y="213.5" width="337" height="386" rx="5.33505" fill="#606060" />
-  <rect x="603" y="272.5" width="284" height="22" rx="5.47638" fill="#2e8555" />
-  <rect x="537" y="352.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
-  <rect x="537" y="396.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
-  <rect x="537" y="440.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
-  <rect x="537" y="484.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
-  <rect x="865" y="552.5" width="88" height="26" rx="7.02756" fill="#3ecc5f" />
-  <path d="M1088.60287,624.61594a30.11371,30.11371,0,0,0,3.98291-15.266c0-13.79652-8.54358-24.98081-19.08256-24.98081s-19.08256,11.18429-19.08256,24.98081a30.11411,30.11411,0,0,0,3.98291,15.266,31.248,31.248,0,0,0,0,30.53213,31.248,31.248,0,0,0,0,30.53208,31.248,31.248,0,0,0,0,30.53208,30.11408,30.11408,0,0,0-3.98291,15.266c0,13.79652,8.54353,24.98081,19.08256,24.98081s19.08256-11.18429,19.08256-24.98081a30.11368,30.11368,0,0,0-3.98291-15.266,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53213Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
-  <ellipse cx="1038.00321" cy="460.31783" rx="19.08256" ry="24.9808" fill="#3f3d56" />
-  <ellipse cx="1038.00321" cy="429.78574" rx="19.08256" ry="24.9808" fill="#3f3d56" />
-  <path d="M1144.93871,339.34489a91.61081,91.61081,0,0,0,7.10658-10.46092l-50.141-8.23491,54.22885.4033a91.566,91.566,0,0,0,1.74556-72.42605l-72.75449,37.74139,67.09658-49.32086a91.41255,91.41255,0,1,0-150.971,102.29805,91.45842,91.45842,0,0,0-10.42451,16.66946l65.0866,33.81447-69.40046-23.292a91.46011,91.46011,0,0,0,14.73837,85.83669,91.40575,91.40575,0,1,0,143.68892,0,91.41808,91.41808,0,0,0,0-113.02862Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
-  <path d="M981.6885,395.8592a91.01343,91.01343,0,0,0,19.56129,56.51431,91.40575,91.40575,0,1,0,143.68892,0C1157.18982,436.82067,981.6885,385.60008,981.6885,395.8592Z" transform="translate(-35.5 -118.5)" opacity="0.1" />
-  <path d="M365.62,461.43628H477.094v45.12043H365.62Z" transform="translate(-35.5 -118.5)" fill="#fff" fill-rule="evenodd" />
-  <path d="M264.76252,608.74122a26.50931,26.50931,0,0,1-22.96231-13.27072,26.50976,26.50976,0,0,0,22.96231,39.81215H291.304V608.74122Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
-  <path d="M384.17242,468.57061l92.92155-5.80726V449.49263a26.54091,26.54091,0,0,0-26.54143-26.54143H331.1161l-3.31768-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622-3.31767-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622L301.257,417.205a3.83043,3.83043,0,0,0-6.63536,0L291.304,422.9512c-.02919,0-.05573.004-.08625.004l-5.49674-5.49541a3.8293,3.8293,0,0,0-6.4071,1.71723l-1.81676,6.77338L270.607,424.1031a3.82993,3.82993,0,0,0-4.6912,4.69253l1.84463,6.89148-6.77072,1.81411a3.8315,3.8315,0,0,0-1.71988,6.40975l5.49673,5.49673c0,.02787-.004.05574-.004.08493l-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74621,3.31768L259.0163,466.081a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768L259.0163,558.976a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768A26.54091,26.54091,0,0,0,291.304,635.28265H450.55254A26.5409,26.5409,0,0,0,477.094,608.74122V502.5755l-92.92155-5.80727a14.12639,14.12639,0,0,1,0-28.19762" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
-  <path d="M424.01111,635.28265h39.81214V582.19979H424.01111Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
-  <path d="M490.36468,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15393-.59852A6.62668,6.62668,0,1,0,482.80568,590.21q-.2203-.22491-.44457-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39414-.10218-.59056-.15262a6.63957,6.63957,0,1,0-13.10086,0c-.1964.05042-.39414.09687-.59056.15262a6.62767,6.62767,0,1,0-11.39688,6.56369,26.52754,26.52754,0,1,0,44.23127,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
-  <path d="M437.28182,555.65836H477.094V529.11693H437.28182Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
-  <path d="M490.36468,545.70532a3.31768,3.31768,0,0,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
-  <path d="M317.84538,466.081a3.31768,3.31768,0,0,1-3.31767-3.31768,9.953,9.953,0,1,0-19.90608,0,3.31768,3.31768,0,1,1-6.63535,0,16.58839,16.58839,0,1,1,33.17678,0,3.31768,3.31768,0,0,1-3.31768,3.31768" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
-  <path d="M370.92825,635.28265h79.62429A26.5409,26.5409,0,0,0,477.094,608.74122v-92.895H397.46968a26.54091,26.54091,0,0,0-26.54143,26.54143Z" transform="translate(-35.5 -118.5)" fill="#ffff50" fill-rule="evenodd" />
-  <path d="M457.21444,556.98543H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0-66.10674H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.29459H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414M477.094,474.19076c-.01592,0-.0292-.008-.04512-.00663-4.10064.13934-6.04083,4.24132-7.75274,7.86024-1.78623,3.78215-3.16771,6.24122-5.43171,6.16691-2.50685-.09024-3.94007-2.92222-5.45825-5.91874-1.74377-3.44243-3.73438-7.34667-7.91333-7.20069-4.04227.138-5.98907,3.70784-7.70631,6.857-1.82738,3.35484-3.07084,5.39455-5.46887,5.30033-2.55727-.09289-3.91619-2.39536-5.48877-5.06013-1.75306-2.96733-3.77951-6.30359-7.8775-6.18946-3.97326.13669-5.92537,3.16507-7.64791,5.83912-1.82207,2.82666-3.09872,4.5492-5.52725,4.447-2.61832-.09289-3.9706-2.00388-5.53522-4.21611-1.757-2.4856-3.737-5.299-7.82308-5.16231-3.88567.13271-5.83779,2.61434-7.559,4.80135-1.635,2.07555-2.9116,3.71846-5.61218,3.615a1.32793,1.32793,0,1,0-.09555,2.65414c4.00377.134,6.03154-2.38873,7.79257-4.6275,1.562-1.9853,2.91027-3.69855,5.56441-3.78879,2.55594-.10882,3.75429,1.47968,5.56707,4.04093,1.7212,2.43385,3.67465,5.19416,7.60545,5.33616,4.11789.138,6.09921-2.93946,7.8536-5.66261,1.56861-2.43385,2.92221-4.53461,5.50734-4.62352,2.37944-.08892,3.67466,1.79154,5.50072,4.885,1.72121,2.91557,3.67069,6.21865,7.67977,6.36463,4.14709.14332,6.14965-3.47693,7.89475-6.68181,1.51155-2.77092,2.93814-5.38791,5.46621-5.4755,2.37944-.05573,3.62025,2.11668,5.45558,5.74622,1.71459,3.388,3.65875,7.22591,7.73019,7.37321l.22429.004c4.06614,0,5.99571-4.08074,7.70364-7.68905,1.51154-3.19825,2.94211-6.21069,5.3972-6.33411Z" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
-  <path d="M344.38682,635.28265h53.08286V582.19979H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
-  <path d="M424.01111,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15394-.59852A6.62667,6.62667,0,1,0,416.45211,590.21q-.2203-.22491-.44458-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39413-.10218-.59054-.15262a6.63957,6.63957,0,1,0-13.10084,0c-.19641.05042-.39414.09687-.59055.15262a6.62767,6.62767,0,1,0-11.39689,6.56369,26.52755,26.52755,0,1,0,44.2313,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
-  <path d="M344.38682,555.65836h53.08286V529.11693H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
-  <path d="M410.74039,545.70532a3.31768,3.31768,0,1,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
-  <path d="M424.01111,447.8338a3.60349,3.60349,0,0,1-.65028-.06636,3.34415,3.34415,0,0,1-.62372-.18579,3.44679,3.44679,0,0,1-.572-.30522,5.02708,5.02708,0,0,1-.50429-.4114,3.88726,3.88726,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.60248,3.60248,0,0,1,.06636-.65027,3.82638,3.82638,0,0,1,.18447-.62373,3.48858,3.48858,0,0,1,.30656-.57064,3.197,3.197,0,0,1,.91436-.91568,3.44685,3.44685,0,0,1,.572-.30523,3.344,3.344,0,0,1,.62372-.18578,3.06907,3.06907,0,0,1,1.30053,0,3.22332,3.22332,0,0,1,1.19436.491,5.02835,5.02835,0,0,1,.50429.41139,4.8801,4.8801,0,0,1,.41139.50429,3.38246,3.38246,0,0,1,.30522.57064,3.47806,3.47806,0,0,1,.25215,1.274A3.36394,3.36394,0,0,1,426.36,446.865a5.02708,5.02708,0,0,1-.50429.4114,3.3057,3.3057,0,0,1-1.84463.55737m26.54143-1.65884a3.38754,3.38754,0,0,1-2.35024-.96877,5.04185,5.04185,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.38659,3.38659,0,0,1,.96744-2.34892,5.02559,5.02559,0,0,1,.50429-.41139,3.44685,3.44685,0,0,1,.572-.30523,3.3432,3.3432,0,0,1,.62373-.18579,3.06952,3.06952,0,0,1,1.30052,0,3.22356,3.22356,0,0,1,1.19436.491,5.02559,5.02559,0,0,1,.50429.41139,3.38792,3.38792,0,0,1,.96876,2.34892,3.72635,3.72635,0,0,1-.06636.65026,3.37387,3.37387,0,0,1-.18579.62373,4.71469,4.71469,0,0,1-.30522.57064,4.8801,4.8801,0,0,1-.41139.50429,5.02559,5.02559,0,0,1-.50429.41139,3.30547,3.30547,0,0,1-1.84463.55737" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
-</svg>
diff --git a/docs/old-docs/static/img/verified-in.png b/docs/old-docs/static/img/verified-in.png
deleted file mode 100644
index 2035a421..00000000
Binary files a/docs/old-docs/static/img/verified-in.png and /dev/null differ
diff --git a/go.mod b/go.mod
index 7cc7e2a5..6ca16161 100644
--- a/go.mod
+++ b/go.mod
@@ -42,6 +42,7 @@ require (
 	github.com/jferrl/go-githubauth v1.2.0
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/klauspost/compress v1.18.0
+	github.com/ktrysmt/go-bitbucket v0.9.81
 	github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0
 	github.com/oklog/ulid/v2 v2.1.0
 	github.com/otiai10/copy v1.14.1
@@ -182,6 +183,7 @@ require (
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/moby/locker v1.0.1 // indirect
 	github.com/moby/spdystream v0.5.0 // indirect
diff --git a/go.sum b/go.sum
index 3af6e48e..fe4bbbd5 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,7 @@ cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
 cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
 cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
 code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
@@ -119,6 +120,7 @@ github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmr
 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -314,6 +316,8 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
+github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
@@ -324,10 +328,17 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/ktrysmt/go-bitbucket v0.9.81 h1:PQxJsFcGdblDOv5PhFA03uNgXMiJfpLo03oYIUdQ2h0=
+github.com/ktrysmt/go-bitbucket v0.9.81/go.mod h1:eWIy5+e1l2eDf9xxwCEmK7oPvNKR91vwYocJWIUQISQ=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
@@ -366,6 +377,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
@@ -413,6 +426,7 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
 github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -445,6 +459,8 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb
 github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ=
 github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
 github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
@@ -470,12 +486,18 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/technosophos/moniker v0.0.0-20210218184952-3ea787d3943b h1:fo0GUa0B+vxSZ8bgnL3fpCPHReM/QPlALdak9T/Zw5Y=
@@ -562,11 +584,14 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
 golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
 golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -577,8 +602,13 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
 golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
 golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -587,6 +617,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
 golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -603,17 +634,29 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 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=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
 golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
 golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
 golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
@@ -623,6 +666,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
 golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -644,8 +688,10 @@ google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9x
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
 gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
diff --git a/internal/controller/git/bare_repo.go b/internal/controller/git/bare_repo.go
index a05e2229..c5473518 100644
--- a/internal/controller/git/bare_repo.go
+++ b/internal/controller/git/bare_repo.go
@@ -104,7 +104,7 @@ func CloneBare(
 			url:     repoURL,
 		},
 	}
-	if err = b.setupClient(clientOpts); err != nil {
+	if err = b.setupClient(homeDir, clientOpts); err != nil {
 		return nil, err
 	}
 	if err = b.clone(); err != nil {
@@ -146,7 +146,7 @@ func LoadBareRepo(path string, opts *LoadBareRepoOptions) (BareRepo, error) {
 		return nil,
 			fmt.Errorf(`error reading URL of remote "origin" from config: %w`, err)
 	}
-	if err := b.setupAuth(); err != nil {
+	if err := b.setupAuth(b.homeDir); err != nil {
 		return nil, fmt.Errorf("error configuring the credentials: %w", err)
 	}
 	return b, nil
diff --git a/internal/controller/git/base_repo.go b/internal/controller/git/base_repo.go
index 7ace1464..e59036cc 100644
--- a/internal/controller/git/base_repo.go
+++ b/internal/controller/git/base_repo.go
@@ -1,6 +1,7 @@
 package git
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"net/url"
@@ -10,6 +11,7 @@ import (
 	"strings"
 
 	libExec "github.com/akuity/kargo/internal/exec"
+	"github.com/akuity/kargo/internal/logging"
 )
 
 const (
@@ -40,24 +42,29 @@ type ClientOptions struct {
 	InsecureSkipTLSVerify bool
 }
 
-// setupClient configures the git CLI for authentication using either SSH or
-// the "store" (username/password-based) credential helper.
-func (b *baseRepo) setupClient(opts *ClientOptions) error {
+// setupClient sets up "global" git configuration with author and authentication
+// details in the specified virtual home directory.
+func (b *baseRepo) setupClient(homeDir string, opts *ClientOptions) error {
 	if opts == nil {
 		opts = &ClientOptions{}
 	}
 
-	if err := b.setupAuthor(opts.User); err != nil {
+	if err := b.setupAuthor(homeDir, opts.User); err != nil {
 		return fmt.Errorf("error configuring the author: %w", err)
 	}
 
-	if err := b.setupAuth(); err != nil {
+	if err := b.setupAuth(homeDir); err != nil {
 		return fmt.Errorf("error configuring the credentials: %w", err)
 	}
 
 	if opts.InsecureSkipTLSVerify {
 		cmd := b.buildGitCommand("config", "--global", "http.sslVerify", "false")
-		cmd.Dir = b.homeDir // Override the cmd.Dir that's set by b.buildGitCommand()
+		// Override the home directory set by b.buildGitCommand().
+		b.setCmdHome(cmd, homeDir)
+		// Override the cmd.Dir that's set by b.buildGitCommand(). It's normally the
+		// repository's path, but if this method was called as part of the cloning
+		// process, that path may not exist yet.
+		cmd.Dir = homeDir
 		if _, err := libExec.Exec(cmd); err != nil {
 			return fmt.Errorf("error configuring http.sslVerify: %w", err)
 		}
@@ -84,8 +91,10 @@ type User struct {
 
 // setupAuthor configures the git CLI with a default commit author.
 // Optionally, the author can have an associated signing key. When using GPG
-// signing, the name and email must match the GPG key identity.
-func (b *baseRepo) setupAuthor(author *User) error {
+// signing, the name and email must match the GPG key identity. The directory
+// specified by homeDir is used as a virtual home directory for all commands
+// executed by this method.
+func (b *baseRepo) setupAuthor(homeDir string, author *User) error {
 	if author == nil {
 		author = &User{}
 	}
@@ -95,7 +104,12 @@ func (b *baseRepo) setupAuthor(author *User) error {
 	}
 
 	cmd := b.buildGitCommand("config", "--global", "user.name", author.Name)
-	cmd.Dir = b.homeDir // Override the cmd.Dir that's set by r.buildGitCommand()
+	// Override the home directory set by b.buildGitCommand().
+	b.setCmdHome(cmd, homeDir)
+	// Override the cmd.Dir that's set by b.buildGitCommand(). It's normally the
+	// repository's path, but if this method was called as part of the cloning
+	// process, that path may not exist yet.
+	cmd.Dir = homeDir
 	if _, err := libExec.Exec(cmd); err != nil {
 		return fmt.Errorf("error configuring git user name: %w", err)
 	}
@@ -105,15 +119,22 @@ func (b *baseRepo) setupAuthor(author *User) error {
 	}
 
 	cmd = b.buildGitCommand("config", "--global", "user.email", author.Email)
-	cmd.Dir = b.homeDir // Override the cmd.Dir that's set by r.buildGitCommand()
+	// Override the home directory set by b.buildGitCommand().
+	b.setCmdHome(cmd, homeDir)
+	// Override the cmd.Dir that's set by b.buildGitCommand(). It's normally the
+	// repository's path, but if this method was called as part of the cloning
+	// process, that path may not exist yet.
+	cmd.Dir = homeDir
 	if _, err := libExec.Exec(cmd); err != nil {
 		return fmt.Errorf("error configuring git user email: %w", err)
 	}
 
-	if author.SigningKeyType == SigningKeyTypeGPG {
+	// For now, since only GPG signing is supported, we will assume GPG if the
+	// signing key type is not specified.
+	if author.SigningKeyType == SigningKeyTypeGPG || author.SigningKeyType == "" {
 
 		if author.SigningKey != "" {
-			author.SigningKeyPath = filepath.Join(b.homeDir, "signing-key.asc")
+			author.SigningKeyPath = filepath.Join(homeDir, "signing-key.asc")
 			if err := os.WriteFile(
 				author.SigningKeyPath,
 				[]byte(author.SigningKey),
@@ -121,40 +142,56 @@ func (b *baseRepo) setupAuthor(author *User) error {
 			); err != nil {
 				return fmt.Errorf("error writing signing key to %q: %w", author.SigningKeyPath, err)
 			}
+			defer func() {
+				if err := os.Remove(author.SigningKeyPath); err != nil {
+					logging.LoggerFromContext(context.TODO()).Error(
+						err,
+						"error removing file",
+						"file", author.SigningKeyPath,
+					)
+				}
+			}()
 		}
 
 		if author.SigningKeyPath != "" {
 			cmd = b.buildGitCommand("config", "--global", "commit.gpgsign", "true")
-			cmd.Dir = b.homeDir // Override the cmd.Dir that's set by b.buildGitCommand()
+			// Override the home directory set by b.buildGitCommand().
+			b.setCmdHome(cmd, homeDir)
+			// Override the cmd.Dir that's set by b.buildGitCommand(). It's normally the
+			// repository's path, but if this method was called as part of the cloning
+			// process, that path may not exist yet.
+			cmd.Dir = homeDir
 			if _, err := libExec.Exec(cmd); err != nil {
 				return fmt.Errorf("error configuring commit gpg signing: %w", err)
 			}
 
 			cmd = b.buildCommand("gpg", "--import", author.SigningKeyPath)
-			cmd.Dir = b.homeDir // Override the cmd.Dir that's set by b.buildCommand()
+			// Override the home directory set by b.buildCommand().
+			b.setCmdHome(cmd, homeDir)
+			// Override the cmd.Dir that's set by b.buildCommand(). It's normally the
+			// repository's path, but if this method was called as part of the cloning
+			// process, that path may not exist yet.
+			cmd.Dir = homeDir
 			if _, err := libExec.Exec(cmd); err != nil {
 				return fmt.Errorf("error importing gpg key %q: %w", author.SigningKeyPath, err)
 			}
 		}
 
-		if author.SigningKey != "" {
-			if err := os.Remove(author.SigningKeyPath); err != nil {
-				return fmt.Errorf("error removing file %q: %w", author.SigningKeyPath, err)
-			}
-		}
-
 	}
 
 	return nil
 }
 
-func (b *baseRepo) setupAuth() error {
+// setupAuth configures the git CLI with authentication information. The
+// directory specified by homeDir is used as a virtual home directory for
+// storing ssh keys if applicable.
+func (b *baseRepo) setupAuth(homeDir string) error {
 	if b.creds == nil {
 		return nil
 	}
 	// If an SSH key was provided, use that.
 	if b.creds.SSHPrivateKey != "" {
-		sshPath := filepath.Join(b.homeDir, ".ssh")
+		sshPath := filepath.Join(homeDir, ".ssh")
 		if err := os.MkdirAll(sshPath, 0700); err != nil {
 			return fmt.Errorf("error creating SSH directory %q: %w", sshPath, err)
 		}
@@ -307,3 +344,11 @@ func (b *baseRepo) RemoteBranchExists(branch string) (bool, error) {
 func (b *baseRepo) URL() string {
 	return b.url
 }
+
+func (b *baseRepo) setCmdHome(cmd *exec.Cmd, homeDir string) {
+	if cmd.Env == nil {
+		cmd.Env = []string{fmt.Sprintf("HOME=%s", homeDir)}
+	} else {
+		cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", homeDir))
+	}
+}
diff --git a/internal/controller/git/repo.go b/internal/controller/git/repo.go
index b16023c8..7efa5c99 100644
--- a/internal/controller/git/repo.go
+++ b/internal/controller/git/repo.go
@@ -97,7 +97,7 @@ func Clone(
 			baseRepo: baseRepo,
 		},
 	}
-	if err = r.setupClient(clientOpts); err != nil {
+	if err = r.setupClient(homeDir, clientOpts); err != nil {
 		return nil, err
 	}
 	if err = r.clone(cloneOpts); err != nil {
@@ -157,7 +157,7 @@ func LoadRepo(path string, opts *LoadRepoOptions) (Repo, error) {
 		return nil,
 			fmt.Errorf(`error reading URL of remote "origin" from config: %w`, err)
 	}
-	if err := r.setupAuth(); err != nil {
+	if err := r.setupAuth(r.homeDir); err != nil {
 		return nil, fmt.Errorf("error configuring the credentials: %w", err)
 	}
 	return r, nil
diff --git a/internal/controller/git/work_tree.go b/internal/controller/git/work_tree.go
index 214691b4..e08d04d1 100644
--- a/internal/controller/git/work_tree.go
+++ b/internal/controller/git/work_tree.go
@@ -3,6 +3,7 @@ package git
 import (
 	"bufio"
 	"bytes"
+	"context"
 	"errors"
 	"fmt"
 	"os"
@@ -11,7 +12,10 @@ import (
 	"strings"
 	"time"
 
+	"github.com/google/uuid"
+
 	libExec "github.com/akuity/kargo/internal/exec"
+	"github.com/akuity/kargo/internal/logging"
 )
 
 // WorkTree is an interface for interacting with any working tree of a Git
@@ -22,7 +26,7 @@ type WorkTree interface {
 	// AddAllAndCommit is a convenience function that stages pending changes for
 	// commit to the current branch and then commits them using the provided
 	// commit message.
-	AddAllAndCommit(message string) error
+	AddAllAndCommit(message string, commitOpts *CommitOptions) error
 	// Clean cleans the working tree.
 	Clean() error
 	// Clear executes `git rm -rf .` to remove all files from the working tree.
@@ -123,7 +127,7 @@ func LoadWorkTree(path string, opts *LoadWorkTreeOptions) (WorkTree, error) {
 		return nil,
 			fmt.Errorf(`error reading URL of remote "origin" from config: %w`, err)
 	}
-	if err = w.setupAuth(); err != nil {
+	if err = w.setupAuth(w.homeDir); err != nil {
 		return nil, fmt.Errorf("error configuring the credentials: %w", err)
 	}
 	br, err := LoadBareRepo(repoPath, &LoadBareRepoOptions{
@@ -143,11 +147,11 @@ func (w *workTree) AddAll() error {
 	return nil
 }
 
-func (w *workTree) AddAllAndCommit(message string) error {
+func (w *workTree) AddAllAndCommit(message string, commitOpts *CommitOptions) error {
 	if err := w.AddAll(); err != nil {
 		return err
 	}
-	return w.Commit(message, nil)
+	return w.Commit(message, commitOpts)
 }
 
 func (w *workTree) Clean() error {
@@ -203,23 +207,45 @@ func (w *workTree) Commit(message string, opts *CommitOptions) error {
 	if opts == nil {
 		opts = &CommitOptions{}
 	}
-	cmdTokens := []string{"commit", "-m", message}
+
+	var homeDir string
 	if opts.Author != nil {
-		name := opts.Author.Name
-		if name == "" {
-			name = defaultUsername
+		// This author information is specific to this commit, so we will override
+		// repository-level author information by creating a temporary home
+		// directory, configuring the author information "globally" within it, and
+		// then ensuring the git commit command uses that home directory.
+		homeDir = filepath.Join(w.homeDir, uuid.New().String())
+		if err := os.MkdirAll(homeDir, 0700); err != nil {
+			return fmt.Errorf(
+				"error creating virtual home directory %q for commit command: %w",
+				homeDir, err,
+			)
 		}
-		email := opts.Author.Email
-		if email == "" {
-			email = defaultEmail
+		defer func() {
+			if err := os.RemoveAll(homeDir); err != nil {
+				logging.LoggerFromContext(context.TODO()).
+					Error(err, "error removing virtual home directory", "path", homeDir)
+			}
+		}()
+		err := w.setupAuthor(homeDir, opts.Author)
+		if err != nil {
+			return fmt.Errorf(
+				"error setting up author information for commit command: %w", err,
+			)
 		}
-		cmdTokens = append(cmdTokens, "--author", fmt.Sprintf("%s <%s>", name, email))
 	}
+
+	cmdTokens := []string{"commit", "-m", message}
 	if opts.AllowEmpty {
 		cmdTokens = append(cmdTokens, "--allow-empty")
 	}
 
-	if _, err := libExec.Exec(w.buildGitCommand(cmdTokens...)); err != nil {
+	cmd := w.buildGitCommand(cmdTokens...)
+	if homeDir != "" {
+		// Override the home directory set by b.buildGitCommand().
+		w.setCmdHome(cmd, homeDir)
+	}
+	if _, err := libExec.Exec(cmd); err != nil {
 		return fmt.Errorf("error committing changes: %w", err)
 	}
 	return nil
diff --git a/internal/controller/management/projects/event_handlers.go b/internal/controller/management/projects/event_handlers.go
new file mode 100644
index 00000000..5c834244
--- /dev/null
+++ b/internal/controller/management/projects/event_handlers.go
@@ -0,0 +1,156 @@
+package projects
+
+import (
+	"context"
+
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/util/workqueue"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+	"github.com/akuity/kargo/internal/conditions"
+	"github.com/akuity/kargo/internal/logging"
+)
+
+// projectWarehouseHealthEnqueuer enqueues a Project for reconciliation when the
+// health condition of a Warehouse within that Project changes.
+type projectWarehouseHealthEnqueuer[T any] struct{}
+
+// Create implements TypedEventHandler.
+func (e *projectWarehouseHealthEnqueuer[T]) Create(
+	context.Context,
+	event.TypedCreateEvent[T],
+	workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+	// No-op
+}
+
+// Delete implements TypedEventHandler.
+func (e *projectWarehouseHealthEnqueuer[T]) Delete(
+	context.Context,
+	event.TypedDeleteEvent[T],
+	workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+	// No-op
+}
+
+// Generic implements TypedEventHandler.
+func (e *projectWarehouseHealthEnqueuer[T]) Generic(
+	context.Context,
+	event.TypedGenericEvent[T],
+	workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+	// No-op
+}
+
+// Update implements TypedEventHandler.
+func (e *projectWarehouseHealthEnqueuer[T]) Update(
+	ctx context.Context,
+	evt event.TypedUpdateEvent[T],
+	wq workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+	logger := logging.LoggerFromContext(ctx)
+
+	oldWarehouse, ok := any(evt.ObjectOld).(*kargoapi.Warehouse)
+	if !ok {
+		return
+	}
+	newWarehouse, ok := any(evt.ObjectNew).(*kargoapi.Warehouse)
+	if !ok {
+		return
+	}
+
+	if oldWarehouse == nil || newWarehouse == nil {
+		logger.Error(
+			nil, "Update event has no old or new object to update",
+			"event", evt,
+		)
+		return
+	}
+
+	oldCond := conditions.Get(&oldWarehouse.Status, kargoapi.ConditionTypeHealthy)
+	newCond := conditions.Get(&newWarehouse.Status, kargoapi.ConditionTypeHealthy)
+	switch {
+	case oldCond == nil && newCond == nil:
+		return
+	case oldCond == nil || newCond == nil:
+		fallthrough
+	case oldCond.Status != newCond.Status:
+		logger.Info("Warehouse health changed, enqueueing Project")
+		wq.Add(reconcile.Request{
+			NamespacedName: types.NamespacedName{Name: newWarehouse.Namespace},
+		})
+	}
+}
+
+// projectStageHealthEnqueuer enqueues a Project for reconciliation when the
+// health condition of a Stage within that Project changes.
+type projectStageHealthEnqueuer[T any] struct{}
+
+// Create implements TypedEventHandler.
+func (e *projectStageHealthEnqueuer[T]) Create(
+	context.Context,
+	event.TypedCreateEvent[T],
+	workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+	// No-op
+}
+
+// Delete implements TypedEventHandler.
+func (e *projectStageHealthEnqueuer[T]) Delete(
+	context.Context,
+	event.TypedDeleteEvent[T],
+	workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+	// No-op
+}
+
+// Generic implements TypedEventHandler.
+func (e *projectStageHealthEnqueuer[T]) Generic(
+	context.Context,
+	event.TypedGenericEvent[T],
+	workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+	// No-op
+}
+
+// Update implements TypedEventHandler.
+func (e *projectStageHealthEnqueuer[T]) Update(
+	ctx context.Context,
+	evt event.TypedUpdateEvent[T],
+	wq workqueue.TypedRateLimitingInterface[reconcile.Request],
+) {
+	logger := logging.LoggerFromContext(ctx)
+
+	oldStage, ok := any(evt.ObjectOld).(*kargoapi.Stage)
+	if !ok {
+		return
+	}
+	newStage, ok := any(evt.ObjectNew).(*kargoapi.Stage)
+	if !ok {
+		return
+	}
+
+	if oldStage == nil || newStage == nil {
+		logger.Error(
+			nil, "Update event has no old or new object to update",
+			"event", evt,
+		)
+		return
+	}
+
+	oldCond := conditions.Get(&oldStage.Status, kargoapi.ConditionTypeHealthy)
+	newCond := conditions.Get(&newStage.Status, kargoapi.ConditionTypeHealthy)
+	switch {
+	case oldCond == nil && newCond == nil:
+		return
+	case oldCond == nil || newCond == nil:
+		fallthrough
+	case oldCond.Status != newCond.Status:
+		logger.Info("Warehouse health changed, enqueueing Project")
+		wq.Add(reconcile.Request{
+			NamespacedName: types.NamespacedName{Name: oldStage.Namespace},
+		})
+	}
+}
diff --git a/internal/controller/management/projects/projects.go b/internal/controller/management/projects/projects.go
index 121ce728..95ba2134 100644
--- a/internal/controller/management/projects/projects.go
+++ b/internal/controller/management/projects/projects.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"time"
 
 	"github.com/kelseyhightower/envconfig"
 	corev1 "k8s.io/api/core/v1"
@@ -18,6 +19,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/manager"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
+	"sigs.k8s.io/controller-runtime/pkg/source"
 
 	rbacapi "github.com/akuity/kargo/api/rbac/v1alpha1"
 	rolloutsapi "github.com/akuity/kargo/api/stubs/rollouts/v1alpha1"
@@ -62,21 +64,12 @@ type reconciler struct {
 		string,
 	) (*kargoapi.Project, error)
 
-	syncProjectFn func(
+	reconcileFn func(
 		context.Context,
 		*kargoapi.Project,
-	) (kargoapi.ProjectStatus, error)
+	) (kargoapi.ProjectStatus, bool, error)
 
-	ensureNamespaceFn func(
-		context.Context,
-		*kargoapi.Project,
-	) (kargoapi.ProjectStatus, error)
-
-	updateProjectFn func(
-		context.Context,
-		client.Object,
-		...client.UpdateOption,
-	) error
+	ensureNamespaceFn func(context.Context, *kargoapi.Project) error
 
 	patchProjectStatusFn func(
 		context.Context,
@@ -113,7 +106,7 @@ type reconciler struct {
 
 	ensureControllerPermissionsFn func(context.Context, *kargoapi.Project) error
 
-	ensureDefaultProjectRolesFn func(context.Context, *kargoapi.Project) error
+	ensureDefaultUserRolesFn func(context.Context, *kargoapi.Project) error
 
 	createServiceAccountFn func(
 		context.Context,
@@ -132,12 +125,6 @@ type reconciler struct {
 		client.Object,
 		...client.CreateOption,
 	) error
-
-	createProjectConfigFn func(
-		context.Context,
-		client.Object,
-		...client.CreateOption,
-	) error
 }
 
 // SetupReconcilerWithManager initializes a reconciler for Project resources and
@@ -147,7 +134,7 @@ func SetupReconcilerWithManager(
 	kargoMgr manager.Manager,
 	cfg ReconcilerConfig,
 ) error {
-	err := ctrl.NewControllerManagedBy(kargoMgr).
+	c, err := ctrl.NewControllerManagedBy(kargoMgr).
 		For(&kargoapi.Project{}).
 		WithEventFilter(
 			predicate.Funcs{
@@ -158,15 +145,38 @@ func SetupReconcilerWithManager(
 			},
 		).
 		WithOptions(controller.CommonOptions(cfg.MaxConcurrentReconciles)).
-		Complete(newReconciler(kargoMgr.GetClient(), cfg))
+		Build(newReconciler(kargoMgr.GetClient(), cfg))
+	if err != nil {
+		return fmt.Errorf("error creating Project reconciler: %w", err)
+	}
 
-	if err == nil {
-		logging.LoggerFromContext(ctx).Info(
-			"Initialized Project reconciler",
-			"maxConcurrentReconciles", cfg.MaxConcurrentReconciles,
-		)
+	// Watch for Warehouses for which the health condition has changed.
+	if err = c.Watch(
+		source.Kind(
+			kargoMgr.GetCache(),
+			&kargoapi.Warehouse{},
+			&projectWarehouseHealthEnqueuer[*kargoapi.Warehouse]{},
+		),
+	); err != nil {
+		return fmt.Errorf("unable to watch Warehouses: %w", err)
 	}
 
+	// Watch for Stages for which the health condition has changed.
+	if err = c.Watch(
+		source.Kind(
+			kargoMgr.GetCache(),
+			&kargoapi.Stage{},
+			&projectStageHealthEnqueuer[*kargoapi.Stage]{},
+		),
+	); err != nil {
+		return fmt.Errorf("unable to watch Stages: %w", err)
+	}
+
+	logging.LoggerFromContext(ctx).Info(
+		"Initialized Project reconciler",
+		"maxConcurrentReconciles", cfg.MaxConcurrentReconciles,
+	)
+
 	return err
 }
 
@@ -176,9 +186,8 @@ func newReconciler(kubeClient client.Client, cfg ReconcilerConfig) *reconciler {
 		client: kubeClient,
 	}
 	r.getProjectFn = api.GetProject
-	r.syncProjectFn = r.syncProject
+	r.reconcileFn = r.reconcile
 	r.ensureNamespaceFn = r.ensureNamespace
-	r.updateProjectFn = r.client.Update
 	r.patchProjectStatusFn = r.patchProjectStatus
 	r.getNamespaceFn = r.client.Get
 	r.createNamespaceFn = r.client.Create
@@ -186,11 +195,10 @@ func newReconciler(kubeClient client.Client, cfg ReconcilerConfig) *reconciler {
 	r.ensureFinalizerFn = api.EnsureFinalizer
 	r.ensureAPIAdminPermissionsFn = r.ensureAPIAdminPermissions
 	r.ensureControllerPermissionsFn = r.ensureControllerPermissions
-	r.ensureDefaultProjectRolesFn = r.ensureDefaultProjectRoles
+	r.ensureDefaultUserRolesFn = r.ensureDefaultUserRoles
 	r.createServiceAccountFn = r.client.Create
 	r.createRoleFn = r.client.Create
 	r.createRoleBindingFn = r.client.Create
-	r.createProjectConfigFn = r.client.Create
 	return r
 }
 
@@ -204,7 +212,6 @@ func (r *reconciler) Reconcile(
 		"project", req.NamespacedName.Name,
 	)
 	ctx = logging.ContextWithLogger(ctx, logger)
-	logger.Debug("reconciling Project")
 
 	// Find the Project
 	project, err := r.getProjectFn(ctx, r.client, req.NamespacedName.Name)
@@ -222,86 +229,159 @@ func (r *reconciler) Reconcile(
 		return ctrl.Result{}, nil
 	}
 
-	if newStatus, ok := migratePhaseToConditions(project); ok {
-		logger.Debug("migrated Project phase to conditions")
-		patchErr := r.patchProjectStatusFn(ctx, project, newStatus)
-		return ctrl.Result{Requeue: true}, patchErr
-	}
+	logger.Debug("reconciling Project")
+	newStatus, needsRequeue, reconcileErr := r.reconcileFn(ctx, project)
+	logger.Debug("done reconciling Project")
 
-	// TODO(hidde): Remove this migration code when the spec field is removed
-	// from the Project.
-	if projectCfg, ok := migrateSpecToProjectConfig(project); ok {
-		// If we receive a ProjectConfig, we need to create it.
-		if projectCfg != nil {
-			if err = r.createProjectConfigFn(ctx, projectCfg); err != nil {
-				// If the ProjectConfig already exists, we can ignore the error.
-				// This can for example happen if the ProjectConfig was created
-				// by the user without them removing the spec from the Project.
-				if !kubeerr.IsAlreadyExists(err) {
-					return ctrl.Result{}, err
-				}
-				logger.Debug("ProjectConfig already exists")
-			} else {
-				logger.Debug(fmt.Sprintf(
-					"migrated Project spec to ProjectConfig %q",
-					projectCfg.Name,
-				))
-			}
+	// Patch the status of the Project.
+	if err := kubeclient.PatchStatus(ctx, r.client, project, func(status *kargoapi.ProjectStatus) {
+		*status = newStatus
+	}); err != nil {
+		// Prioritize the reconcile error if it exists.
+		if reconcileErr != nil {
+			logger.Error(err, "failed to update Project status after reconciliation error")
+			return ctrl.Result{}, reconcileErr
 		}
+		return ctrl.Result{}, fmt.Errorf("failed to update Project status: %w", err)
+	}
 
-		// Remove the spec from the Project.
-		if err = r.updateProjectFn(ctx, project); err != nil {
-			return ctrl.Result{}, err
-		}
-		logger.Debug("removed deprecated spec from Project")
+	// Return the reconcile error if it exists.
+	if reconcileErr != nil {
+		return ctrl.Result{}, reconcileErr
+	}
+	// Immediate requeue if needed.
+	if needsRequeue {
 		return ctrl.Result{Requeue: true}, nil
 	}
+	// Otherwise, requeue after a delay.
+	// TODO: Make the requeue delay configurable.
+	return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
+}
 
-	if reason, ok := mustReconcileProject(project); !ok {
-		logger.Debug("nothing to do", "reason", reason)
-		return ctrl.Result{}, nil
-	}
+func (r *reconciler) reconcile(
+	ctx context.Context,
+	project *kargoapi.Project,
+) (kargoapi.ProjectStatus, bool, error) {
+	logger := logging.LoggerFromContext(ctx)
+	status := *project.Status.DeepCopy()
 
-	newStatus, err := r.syncProjectFn(ctx, project)
-	if err != nil {
-		logger.Error(err, "error syncing Project")
+	var requestRequeue bool
+	subReconcilers := []struct {
+		name string
+		// Returns updated status, whether to stop the loop, and possibly an error
+		reconcile func() (kargoapi.ProjectStatus, bool, error)
+	}{
+		{
+			name: "migrate phase to conditions",
+			reconcile: func() (kargoapi.ProjectStatus, bool, error) {
+				// TODO(krancour): Remove this migration code when the Phase field is
+				// removed from ProjectStatus.
+				if newStatus, migrated := migratePhaseToConditions(project); migrated {
+					logger.Debug("migrated Project phase to conditions")
+					requestRequeue = true
+					return newStatus, true, nil // Stop the loop
+				}
+				return status, false, nil // Continue
+			},
+		},
+		{
+			name: "migrate spec to ProjectConfig",
+			reconcile: func() (kargoapi.ProjectStatus, bool, error) {
+				// TODO(hidde): Remove this migration code when the spec field is
+				// removed from Project.
+				migrated, err := r.migrateSpecToProjectConfig(ctx, project)
+				if err != nil {
+					return status, true, err // Stop the loop
+				}
+				if migrated {
+					logger.Debug("migrated Project spec to ProjectConfig")
+					requestRequeue = true
+					return status, true, nil // Stop the loop
+				}
+				return status, false, nil // Continue
+			},
+		},
+		{
+			name: "syncing project resources",
+			reconcile: func() (kargoapi.ProjectStatus, bool, error) {
+				newStatus, err := r.syncProject(ctx, project)
+				if err != nil {
+					return newStatus, true, err // Stop the loop
+				}
+				return newStatus, false, nil // Continue
+			},
+		},
+		{
+			name: "collecting project stats",
+			reconcile: func() (kargoapi.ProjectStatus, bool, error) {
+				newStatus, err := r.collectStats(ctx, project)
+				if err != nil {
+					logger.Error(err, "error collecting project stats")
+					return newStatus, true, err // Stop the loop
+				}
+				return newStatus, false, nil // Continue
+			},
+		},
 	}
+	for _, subR := range subReconcilers {
+		logger.Debug(subR.name)
 
-	patchErr := r.patchProjectStatusFn(ctx, project, newStatus)
-	if patchErr != nil {
-		logger.Error(patchErr, "error updating Project status")
-	}
+		// Reconcile the Project with the sub-reconciler.
+		var err error
+		var shouldBreak bool
+		status, shouldBreak, err = subR.reconcile()
+
+		// If an error occurred during the sub-reconciler, then we should
+		// return the error which will cause the Project to be requeued.
+		if err != nil {
+			return status, false, err
+		}
+
+		// Patch the status of the Project after each sub-reconciler to show
+		// progress.
+		if err = kubeclient.PatchStatus(ctx, r.client, project, func(st *kargoapi.ProjectStatus) {
+			*st = status
+		}); err != nil {
+			logger.Error(err, fmt.Sprintf("failed to update Project status after %s", subR.name))
+		}
 
-	// If we had no error, but couldn't patch, then we DO have an error. But we
-	// do it this way so that a failure to patch is never counted as THE failure
-	// when something else more serious occurred first.
-	if err == nil {
-		err = patchErr
+		if shouldBreak {
+			break
+		}
 	}
-	logger.Debug("done reconciling Project")
 
-	// Controller runtime automatically gives us a progressive backoff if err is
-	// not nil
-	return ctrl.Result{}, err
+	return status, requestRequeue, nil
 }
 
+// syncProject ensures the existence of the Project's namespace and any
+// resources that are required for the Project to function properly. It
+// returns an updated ProjectStatus.
 func (r *reconciler) syncProject(
 	ctx context.Context,
 	project *kargoapi.Project,
 ) (kargoapi.ProjectStatus, error) {
-	conditions.Set(&project.Status, &metav1.Condition{
+	status := project.Status.DeepCopy()
+
+	conditions.Set(status, &metav1.Condition{
 		Type:               kargoapi.ConditionTypeReconciling,
 		Status:             metav1.ConditionTrue,
-		Reason:             "Initializing",
-		Message:            "Creating project namespace and initializing permissions",
+		Reason:             "Syncing",
+		Message:            "Ensuring project namespace and permissions",
+		ObservedGeneration: project.GetGeneration(),
+	})
+	conditions.Set(status, &metav1.Condition{
+		Type:               kargoapi.ConditionTypeReady,
+		Status:             metav1.ConditionFalse,
+		Reason:             "Syncing",
+		Message:            "Ensuring project namespace and permissions",
 		ObservedGeneration: project.GetGeneration(),
 	})
 
-	status, err := r.ensureNamespaceFn(ctx, project)
-	if err != nil {
+	if err := r.ensureNamespaceFn(ctx, project); err != nil {
 		if errors.Is(err, errProjectNamespaceExists) {
-			conditions.Delete(&status, kargoapi.ConditionTypeReconciling)
-			conditions.Set(&status, &metav1.Condition{
+			// Stalled is a special condition because this won't be resolved without
+			// user intervention.
+			conditions.Set(status, &metav1.Condition{
 				Type:   kargoapi.ConditionTypeStalled,
 				Status: metav1.ConditionTrue,
 				Reason: "ExistingNamespaceMissingLabel",
@@ -312,65 +392,68 @@ func (r *reconciler) syncProject(
 				),
 				ObservedGeneration: project.GetGeneration(),
 			})
+		} else {
+			conditions.Delete(status, kargoapi.ConditionTypeStalled)
 		}
-		conditions.Set(&status, &metav1.Condition{
+		conditions.Set(status, &metav1.Condition{
 			Type:               kargoapi.ConditionTypeReady,
 			Status:             metav1.ConditionFalse,
-			Reason:             "NamespaceInitializationFailed",
-			Message:            "Failed to initialize project namespace: " + err.Error(),
+			Reason:             "EnsuringNamespaceFailed",
+			Message:            "Failed to ensure existence of project namespace: " + err.Error(),
 			ObservedGeneration: project.GetGeneration(),
 		})
-		return status, fmt.Errorf("error ensuring namespace: %w", err)
+		return *status, fmt.Errorf("error ensuring namespace: %w", err)
 	}
 
-	if err = r.ensureAPIAdminPermissionsFn(ctx, project); err != nil {
-		conditions.Set(&status, &metav1.Condition{
+	if err := r.ensureAPIAdminPermissionsFn(ctx, project); err != nil {
+		conditions.Set(status, &metav1.Condition{
 			Type:               kargoapi.ConditionTypeReady,
 			Status:             metav1.ConditionFalse,
-			Reason:             "PermissionsInitializationFailed",
-			Message:            "Failed to initialize project admin permissions: " + err.Error(),
+			Reason:             "EnsuringAPIServerPermissionsFailed",
+			Message:            "Failed to ensure project permissions for API server: " + err.Error(),
 			ObservedGeneration: project.GetGeneration(),
 		})
-		return status, fmt.Errorf("error ensuring project admin permissions: %w", err)
+		return *status, fmt.Errorf("error ensuring API server permissions: %w", err)
 	}
 
 	if r.cfg.ManageControllerRoleBindings {
-		if err = r.ensureControllerPermissionsFn(ctx, project); err != nil {
-			return status, fmt.Errorf("error ensuring controller permissions: %w", err)
+		if err := r.ensureControllerPermissionsFn(ctx, project); err != nil {
+			conditions.Set(status, &metav1.Condition{
+				Type:               kargoapi.ConditionTypeReady,
+				Status:             metav1.ConditionFalse,
+				Reason:             "EnsuringControllerPermissionsFailed",
+				Message:            "Failed to ensure project permissions for controller: " + err.Error(),
+				ObservedGeneration: project.GetGeneration(),
+			})
+			return *status, fmt.Errorf("error ensuring controller permissions: %w", err)
 		}
 	}
 
-	if err = r.ensureDefaultProjectRolesFn(ctx, project); err != nil {
-		conditions.Set(&status, &metav1.Condition{
-			Type:               kargoapi.ConditionTypeReady,
-			Status:             metav1.ConditionFalse,
-			Reason:             "RolesInitializationFailed",
-			Message:            "Failed to initialize default project roles: " + err.Error(),
+	if err := r.ensureDefaultUserRolesFn(ctx, project); err != nil {
+		conditions.Set(status, &metav1.Condition{
+			Type:   kargoapi.ConditionTypeReady,
+			Status: metav1.ConditionFalse,
+			Reason: "EnsuringDefaultUserRoles",
+			Message: "Failed to ensure existence of default project " +
+				"ServiceAccount, Roles, and RoleBindings: " + err.Error(),
 			ObservedGeneration: project.GetGeneration(),
 		})
-		return status, fmt.Errorf("error ensuring default project roles: %w", err)
+		return *status, fmt.Errorf("error ensuring default project roles: %w", err)
 	}
 
-	conditions.Delete(&status, kargoapi.ConditionTypeReconciling)
-	conditions.Set(&status, &metav1.Condition{
+	conditions.Delete(status, kargoapi.ConditionTypeReconciling)
+	conditions.Set(status, &metav1.Condition{
 		Type:               kargoapi.ConditionTypeReady,
 		Status:             metav1.ConditionTrue,
-		Reason:             "Initialized",
-		Message:            "Project is initialized and ready for use",
+		Reason:             "Synced",
+		Message:            "Project is synced and ready for use",
 		ObservedGeneration: project.GetGeneration(),
 	})
-	return status, nil
+	return *status, nil
 }
 
-func (r *reconciler) ensureNamespace(
-	ctx context.Context,
-	project *kargoapi.Project,
-) (kargoapi.ProjectStatus, error) {
-	status := *project.Status.DeepCopy()
-
-	logger := logging.LoggerFromContext(ctx).WithValues(
-		"project", project.Name,
-	)
+func (r *reconciler) ensureNamespace(ctx context.Context, project *kargoapi.Project) error {
+	logger := logging.LoggerFromContext(ctx).WithValues("project", project.Name)
 
 	ownerRef := metav1.NewControllerRef(
 		project,
@@ -384,20 +467,20 @@ func (r *reconciler) ensureNamespace(
 		types.NamespacedName{Name: project.Name},
 		ns,
 	); err != nil && !kubeerr.IsNotFound(err) {
-		return status, fmt.Errorf("error getting namespace %q: %w", project.Name, err)
+		return fmt.Errorf("error getting namespace %q: %w", project.Name, err)
 	} else if err == nil {
 		// We found an existing namespace with the same name as the Project. It's
 		// only a problem if it is not labeled as a Project namespace.
 		if ns.Labels[kargoapi.ProjectLabelKey] != kargoapi.LabelTrueValue {
-			return status, fmt.Errorf(
-				"failed to initialize Project %q with namespace %q: %w",
+			return fmt.Errorf(
+				"failed to sync Project %q with namespace %q: %w",
 				project.Name, project.Name, errProjectNamespaceExists,
 			)
 		}
 		for _, ownerRef := range ns.OwnerReferences {
 			if ownerRef.UID == project.UID {
 				logger.Debug("namespace exists and is already owned by this Project")
-				return status, nil
+				return nil
 			}
 		}
 		// If we get to here, the Project is not already an owner of the existing
@@ -412,14 +495,14 @@ func (r *reconciler) ensureNamespace(
 		// internal policies. Such a controller might already own the namespace.
 		updated, err := r.ensureFinalizerFn(ctx, r.client, ns)
 		if err != nil {
-			return status, fmt.Errorf("error ensuring finalizer on namespace %q: %w", project.Name, err)
+			return fmt.Errorf("error ensuring finalizer on namespace %q: %w", project.Name, err)
 		}
 		if updated {
 			logger.Debug("added finalizer to namespace")
 		}
 		ns.OwnerReferences = append(ns.OwnerReferences, *ownerRef)
 		if err = r.patchOwnerReferencesFn(ctx, r.client, ns); err != nil {
-			return status, fmt.Errorf(
+			return fmt.Errorf(
 				"error patching namespace %q with project %q as owner: %w",
 				project.Name,
 				project.Name,
@@ -428,7 +511,7 @@ func (r *reconciler) ensureNamespace(
 		}
 		logger.Debug("patched namespace with Project as owner")
 
-		return status, nil
+		return nil
 	}
 
 	// If we get to here, we had a not found error and we can proceed with
@@ -452,11 +535,11 @@ func (r *reconciler) ensureNamespace(
 	// the Project.
 	controllerutil.AddFinalizer(ns, kargoapi.FinalizerName)
 	if err := r.createNamespaceFn(ctx, ns); err != nil {
-		return status, fmt.Errorf("error creating namespace %q: %w", project.Name, err)
+		return fmt.Errorf("error creating namespace %q: %w", project.Name, err)
 	}
 	logger.Debug("created namespace")
 
-	return status, nil
+	return nil
 }
 
 func (r *reconciler) ensureAPIAdminPermissions(
@@ -593,7 +676,7 @@ func (r *reconciler) ensureControllerPermissions(
 	return nil
 }
 
-func (r *reconciler) ensureDefaultProjectRoles(
+func (r *reconciler) ensureDefaultUserRoles(
 	ctx context.Context,
 	project *kargoapi.Project,
 ) error {
@@ -803,7 +886,7 @@ func (r *reconciler) patchProjectStatus(
 
 // migratePhaseToConditions migrates the Project's Phase and Message fields to
 // Conditions. It returns the updated ProjectStatus and a boolean indicating
-// whether the Project status was updated.
+// whether the ProjectStatus was updated.
 func migratePhaseToConditions(project *kargoapi.Project) (kargoapi.ProjectStatus, bool) {
 	status := *project.Status.DeepCopy()
 	if project.Status.Phase == "" { // nolint:staticcheck
@@ -816,15 +899,15 @@ func migratePhaseToConditions(project *kargoapi.Project) (kargoapi.ProjectStatus
 		conditions.Set(&status, &metav1.Condition{
 			Type:               kargoapi.ConditionTypeReconciling,
 			Status:             metav1.ConditionTrue,
-			Reason:             "Initializing",
-			Message:            "Creating project namespace and initializing permissions",
+			Reason:             "Syncing",
+			Message:            "Ensuring project namespace and permissions",
 			ObservedGeneration: project.GetGeneration(),
 		})
 		conditions.Set(&status, &metav1.Condition{
 			Type:               kargoapi.ConditionTypeReady,
 			Status:             metav1.ConditionFalse,
-			Reason:             "Initializing",
-			Message:            "Creating project namespace and initializing permissions",
+			Reason:             "Syncing",
+			Message:            "Ensuring project namespace and permissions",
 			ObservedGeneration: project.GetGeneration(),
 		})
 	case kargoapi.ProjectPhaseInitializationFailed:
@@ -842,18 +925,22 @@ func migratePhaseToConditions(project *kargoapi.Project) (kargoapi.ProjectStatus
 			ObservedGeneration: project.GetGeneration(),
 		})
 		conditions.Set(&status, &metav1.Condition{
-			Type:               kargoapi.ConditionTypeReady,
-			Status:             metav1.ConditionFalse,
-			Reason:             "NamespaceInitializationFailed",
-			Message:            "Failed to initialize project namespace: " + errProjectNamespaceExists.Error(),
+			Type:   kargoapi.ConditionTypeReady,
+			Status: metav1.ConditionFalse,
+			Reason: "ExistingNamespaceMissingLabel",
+			Message: fmt.Sprintf(
+				"Namespace %q already exists but is not labeled as a Project namespace using label %q",
+				project.Name,
+				kargoapi.ProjectLabelKey,
+			),
 			ObservedGeneration: project.GetGeneration(),
 		})
 	case kargoapi.ProjectPhaseReady:
 		conditions.Set(&status, &metav1.Condition{
 			Type:               kargoapi.ConditionTypeReady,
 			Status:             metav1.ConditionTrue,
-			Reason:             "Initialized",
-			Message:            "Project is initialized and ready for use",
+			Reason:             "Synced",
+			Message:            "Project is synced and ready for use",
 			ObservedGeneration: project.GetGeneration(),
 		})
 	}
@@ -866,54 +953,55 @@ func migratePhaseToConditions(project *kargoapi.Project) (kargoapi.ProjectStatus
 }
 
 // migrateSpecToProjectConfig migrates the Project's Spec to a dedicated
-// ProjectConfig resource. It returns the new ProjectConfig and a boolean
-// indicating whether the Project's Spec was cleared.
-func migrateSpecToProjectConfig(
+// ProjectConfig resource if necessary. It returns a boolean indicating whether
+// the Project resource was updated.
+func (r *reconciler) migrateSpecToProjectConfig(
+	ctx context.Context,
 	project *kargoapi.Project,
-) (*kargoapi.ProjectConfig, bool) {
-	if project.Spec == nil { // nolint:staticcheck
-		return nil, false
-	}
+) (bool, error) {
+	logger := logging.LoggerFromContext(ctx)
 
-	// If the Project has no PromotionPolicies, we can remove the spec entirely.
-	if len(project.Spec.PromotionPolicies) == 0 { // nolint:staticcheck
-		project.Spec = nil // nolint:staticcheck
-		return nil, true
+	if project.Spec == nil { // nolint:staticcheck
+		return false, nil
 	}
 
-	// If the Project has PromotionPolicies, we need to migrate them to a
-	// ProjectConfig.
-	projectConfig := &kargoapi.ProjectConfig{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      project.Name,
-			Namespace: project.Name,
-		},
-		Spec: kargoapi.ProjectConfigSpec{
-			PromotionPolicies: project.Spec.PromotionPolicies, // nolint:staticcheck
-		},
+	if len(project.Spec.PromotionPolicies) != 0 { // nolint:staticcheck
+		projectCfg := &kargoapi.ProjectConfig{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      project.Name,
+				Namespace: project.Name,
+			},
+			Spec: kargoapi.ProjectConfigSpec{
+				PromotionPolicies: project.Spec.PromotionPolicies, // nolint:staticcheck
+			},
+		}
+		if err := r.client.Create(ctx, projectCfg); err != nil {
+			// If the ProjectConfig already exists, we can ignore the error. This
+			// could happen because the ProjectConfig was created by the user without
+			// them removing the spec from the Project. It could also be the result of
+			// a partial migration by a previous reconciliation attempt.
+			if !kubeerr.IsAlreadyExists(err) {
+				return false, fmt.Errorf(
+					"error creating ProjectConfig in project namespace %q: %w",
+					project.Name, err,
+				)
+			}
+			logger.Debug("ProjectConfig already exists")
+		} else {
+			logger.Debug("migrated Project spec to ProjectConfig")
+		}
 	}
 
-	// Clear the spec now that we've migrated it to a ProjectConfig.
+	// Now remove the spec entirely.
 	project.Spec = nil // nolint:staticcheck
-
-	return projectConfig, true
-}
-
-// mustReconcileProject returns if the Project should be reconciled, or if it
-// should be left alone, and the reason why.
-func mustReconcileProject(project *kargoapi.Project) (string, bool) {
-	if stalled := conditions.Get(&project.Status, kargoapi.ConditionTypeStalled); stalled != nil {
-		if stalled.Status == metav1.ConditionTrue {
-			return stalled.Reason, false
-		}
+	if err := r.client.Update(ctx, project); err != nil {
+		return false, fmt.Errorf(
+			"error updating Project %q to remove spec: %w",
+			project.Name, err,
+		)
 	}
 
-	if ready := conditions.Get(&project.Status, kargoapi.ConditionTypeReady); ready != nil {
-		if ready.Status == metav1.ConditionTrue {
-			return ready.Reason, false
-		}
-	}
-	return "", true
+	return true, nil
 }
 
 func getRoleBindingName(serviceAccountName string) string {
diff --git a/internal/controller/management/projects/stats.go b/internal/controller/management/projects/stats.go
new file mode 100644
index 00000000..209d0b3a
--- /dev/null
+++ b/internal/controller/management/projects/stats.go
@@ -0,0 +1,95 @@
+package projects
+
+import (
+	"context"
+	"fmt"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+	"github.com/akuity/kargo/internal/conditions"
+	"github.com/akuity/kargo/internal/logging"
+)
+
+// collectStats collects statistics about the current health state of all
+// Warehouses and Stages in the Project. It returns a ProjectStatus that's been
+// updated with the collected stats.
+func (r *reconciler) collectStats(
+	ctx context.Context,
+	project *kargoapi.Project,
+) (kargoapi.ProjectStatus, error) {
+	if cond := conditions.Get(
+		&project.Status,
+		kargoapi.ConditionTypeReady,
+	); cond == nil || cond.Status != metav1.ConditionTrue {
+		logging.LoggerFromContext(ctx).Debug("Project is not ready; won't collect stats")
+		return project.Status, nil
+	}
+
+	status := *project.Status.DeepCopy()
+
+	warehouses := &kargoapi.WarehouseList{}
+	if err := r.client.List(
+		ctx,
+		warehouses,
+		client.InNamespace(project.Name),
+	); err != nil {
+		conditions.Set(&status, &metav1.Condition{
+			Type:               kargoapi.ConditionTypeHealthy,
+			Status:             metav1.ConditionFalse,
+			Reason:             "CollectingWarehouseStatsFailed",
+			Message:            "Failed to collect Warehouse stats: " + err.Error(),
+			ObservedGeneration: project.GetGeneration(),
+		})
+		return status, fmt.Errorf("error listing Warehouses: %w", err)
+	}
+
+	stages := &kargoapi.StageList{}
+	if err := r.client.List(
+		ctx,
+		stages,
+		client.InNamespace(project.Name),
+	); err != nil {
+		conditions.Set(&status, &metav1.Condition{
+			Type:               kargoapi.ConditionTypeHealthy,
+			Status:             metav1.ConditionFalse,
+			Reason:             "CollectingStageStatsFailed",
+			Message:            "Failed to collect Stage stats: " + err.Error(),
+			ObservedGeneration: project.GetGeneration(),
+		})
+		return status, fmt.Errorf("error listing Stages: %w", err)
+	}
+
+	stats := kargoapi.ProjectStats{
+		Warehouses: kargoapi.WarehouseStats{
+			Count: int64(len(warehouses.Items)),
+		},
+		Stages: kargoapi.StageStats{
+			Count: int64(len(stages.Items)),
+		},
+	}
+
+	for _, warehouse := range warehouses.Items {
+		if health := conditions.Get(
+			&warehouse.Status,
+			kargoapi.ConditionTypeHealthy,
+		); health != nil && health.Status == metav1.ConditionTrue {
+			stats.Warehouses.Health.Healthy++
+		}
+	}
+
+	for _, stage := range stages.Items {
+		if health := conditions.Get(
+			&stage.Status,
+			kargoapi.ConditionTypeHealthy,
+		); health != nil && health.Status == metav1.ConditionTrue {
+			stats.Stages.Health.Healthy++
+		}
+	}
+
+	status.Stats = &stats
+
+	conditions.Delete(&status, kargoapi.ConditionTypeHealthy)
+	return status, nil
+}
diff --git a/internal/gitprovider/bitbucket/bitbucket.go b/internal/gitprovider/bitbucket/bitbucket.go
new file mode 100644
index 00000000..807fce1b
--- /dev/null
+++ b/internal/gitprovider/bitbucket/bitbucket.go
@@ -0,0 +1,407 @@
+package bitbucket
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/hashicorp/go-cleanhttp"
+	"github.com/ktrysmt/go-bitbucket"
+
+	"github.com/akuity/kargo/internal/git"
+	"github.com/akuity/kargo/internal/gitprovider"
+)
+
+const (
+	ProviderName = "bitbucket"
+
+	// supportedHost is the hostname of the Bitbucket instance that this provider
+	// supports. As of now, this provider only supports Bitbucket "Cloud", and not
+	// self-hosted Bitbucket "Datacenter" instances.
+	supportedHost = "bitbucket.org"
+
+	// prStateOpen is the state of an open pull request.
+	prStateOpen = "OPEN"
+	// prStateMerged is the state of a merged pull request.
+	prStateMerged = "MERGED"
+	// prStateDeclined is the state of a declined pull request. This is also
+	// known as "closed" in other Git providers.
+	prStateDeclined = "DECLINED"
+	// prStateSuperseded is the state of a superseded pull request. This is also
+	// known as "closed" in other Git providers.
+	prStateSuperseded = "SUPERSEDED"
+)
+
+var registration = gitprovider.Registration{
+	Predicate: func(repoURL string) bool {
+		u, err := url.Parse(repoURL)
+		if err != nil {
+			return false
+		}
+		return u.Hostname() == supportedHost
+	},
+	NewProvider: func(
+		repoURL string,
+		opts *gitprovider.Options,
+	) (gitprovider.Interface, error) {
+		return NewProvider(repoURL, opts)
+	},
+}
+
+func init() {
+	gitprovider.Register(ProviderName, registration)
+}
+
+// pullRequestClient defines the interface for pull request operations.
+type pullRequestClient interface {
+	CreatePullRequest(opt *bitbucket.PullRequestsOptions) (any, error)
+	ListPullRequests(opt *bitbucket.PullRequestsOptions) (any, error)
+	GetPullRequest(opt *bitbucket.PullRequestsOptions) (any, error)
+	GetCommit(opt *bitbucket.CommitsOptions) (any, error)
+}
+
+// provider is a Bitbucket-based implementation of gitprovider.Interface.
+type provider struct {
+	owner    string
+	repoSlug string
+	client   pullRequestClient
+}
+
+// NewProvider returns a Bitbucket-based implementation of gitprovider.Interface.
+func NewProvider(
+	repoURL string,
+	opts *gitprovider.Options,
+) (gitprovider.Interface, error) {
+	if opts == nil {
+		opts = &gitprovider.Options{}
+	}
+
+	host, owner, repoSlug, err := parseRepoURL(repoURL)
+	if err != nil {
+		return nil, err
+	}
+
+	// The provider only supports Bitbucket "Cloud", and not self-hosted
+	// Bitbucket "Datacenter" instances — these require a different API client.
+	if host != supportedHost {
+		return nil, fmt.Errorf("unsupported Bitbucket host %q", host)
+	}
+
+	client := bitbucket.NewOAuthbearerToken(opts.Token)
+	client.HttpClient = cleanhttp.DefaultClient()
+
+	return &provider{
+		owner:    owner,
+		repoSlug: repoSlug,
+		client:   &clientWrapper{client},
+	}, nil
+}
+
+// clientWrapper wraps a bitbucket.Client to implement the prClient interface
+type clientWrapper struct {
+	client *bitbucket.Client
+}
+
+func (w *clientWrapper) CreatePullRequest(
+	opt *bitbucket.PullRequestsOptions,
+) (any, error) {
+	return w.client.Repositories.PullRequests.Create(opt)
+}
+
+func (w *clientWrapper) ListPullRequests(
+	opt *bitbucket.PullRequestsOptions,
+) (any, error) {
+	return w.client.Repositories.PullRequests.Gets(opt)
+}
+
+func (w *clientWrapper) GetPullRequest(
+	opt *bitbucket.PullRequestsOptions,
+) (any, error) {
+	return w.client.Repositories.PullRequests.Get(opt)
+}
+
+func (w *clientWrapper) GetCommit(
+	opt *bitbucket.CommitsOptions,
+) (any, error) {
+	return w.client.Repositories.Commits.GetCommit(opt)
+}
+
+// CreatePullRequest implements gitprovider.Interface.
+func (p *provider) CreatePullRequest(
+	ctx context.Context,
+	opts *gitprovider.CreatePullRequestOpts,
+) (*gitprovider.PullRequest, error) {
+	if opts == nil {
+		opts = &gitprovider.CreatePullRequestOpts{}
+	}
+
+	createOpts := &bitbucket.PullRequestsOptions{
+		Owner:             p.owner,
+		RepoSlug:          p.repoSlug,
+		Title:             opts.Title,
+		Description:       opts.Description,
+		SourceBranch:      opts.Head,
+		DestinationBranch: opts.Base,
+	}
+	createOpts.WithContext(ctx)
+
+	resp, err := p.client.CreatePullRequest(createOpts)
+	if err != nil {
+		return nil, err
+	}
+
+	pr, err := toBitbucketPR(resp)
+	if err != nil {
+		return nil, err
+	}
+
+	if pr.MergeCommit.Hash != "" {
+		fullSHA, err := p.getFullCommitSHA(ctx, pr.MergeCommit.Hash)
+		if err != nil {
+			return nil, err
+		}
+		pr.MergeCommit.Hash = fullSHA
+	}
+
+	return toProviderPR(pr, resp), nil
+}
+
+// GetPullRequest implements gitprovider.Interface.
+func (p *provider) GetPullRequest(
+	ctx context.Context,
+	id int64,
+) (*gitprovider.PullRequest, error) {
+	getOpts := &bitbucket.PullRequestsOptions{
+		Owner:    p.owner,
+		RepoSlug: p.repoSlug,
+		ID:       strconv.FormatInt(id, 10),
+	}
+	getOpts.WithContext(ctx)
+
+	resp, err := p.client.GetPullRequest(getOpts)
+	if err != nil {
+		return nil, err
+	}
+
+	pr, err := toBitbucketPR(resp)
+	if err != nil {
+		return nil, err
+	}
+
+	if pr.MergeCommit.Hash != "" {
+		fullSHA, err := p.getFullCommitSHA(ctx, pr.MergeCommit.Hash)
+		if err != nil {
+			return nil, err
+		}
+		pr.MergeCommit.Hash = fullSHA
+	}
+
+	return toProviderPR(pr, resp), nil
+}
+
+// ListPullRequests implements gitprovider.Interface.
+func (p *provider) ListPullRequests(
+	ctx context.Context,
+	opts *gitprovider.ListPullRequestOptions,
+) ([]gitprovider.PullRequest, error) {
+	if opts == nil {
+		opts = &gitprovider.ListPullRequestOptions{}
+	}
+	if opts.State == "" {
+		opts.State = gitprovider.PullRequestStateOpen
+	}
+
+	listOpts := &bitbucket.PullRequestsOptions{
+		Owner:    p.owner,
+		RepoSlug: p.repoSlug,
+	}
+	listOpts.WithContext(ctx)
+
+	switch opts.State {
+	case gitprovider.PullRequestStateAny:
+		listOpts.States = []string{prStateOpen, prStateMerged, prStateDeclined, prStateSuperseded}
+	case gitprovider.PullRequestStateClosed:
+		listOpts.States = []string{prStateMerged, prStateDeclined, prStateSuperseded}
+	case gitprovider.PullRequestStateOpen:
+		listOpts.States = []string{prStateOpen}
+	default:
+		return nil, fmt.Errorf("unknown pull request state %q", opts.State)
+	}
+
+	resp, err := p.client.ListPullRequests(listOpts)
+	if err != nil {
+		return nil, err
+	}
+
+	list, ok := resp.(map[string]any)
+	if !ok {
+		return nil, fmt.Errorf("unexpected type for list response: %T", resp)
+	}
+
+	rawPRs, ok := list["values"]
+	if !ok {
+		return nil, fmt.Errorf("list response missing 'values' field")
+	}
+
+	prList, ok := rawPRs.([]any)
+	if !ok {
+		return nil, fmt.Errorf("unexpected type for list values: %T", rawPRs)
+	}
+
+	// NB: The Bitbucket API doesn't support filtering by source/destination
+	// branch or commit hash, so we have to filter client-side, which is
+	// highly inefficient.
+	var prs []gitprovider.PullRequest
+	for _, pr := range prList {
+		bbPR, err := toBitbucketPR(pr)
+		if err != nil {
+			continue
+		}
+
+		if opts.HeadBranch != "" && bbPR.Source.Branch.Name != opts.HeadBranch {
+			continue
+		}
+
+		if opts.BaseBranch != "" && bbPR.Destination.Branch.Name != opts.BaseBranch {
+			continue
+		}
+
+		if opts.HeadCommit != "" && bbPR.Source.Commit.Hash != opts.HeadCommit {
+			continue
+		}
+
+		if bbPR.MergeCommit.Hash != "" {
+			fullSHA, err := p.getFullCommitSHA(ctx, bbPR.MergeCommit.Hash)
+			if err != nil {
+				return nil, err
+			}
+			bbPR.MergeCommit.Hash = fullSHA
+		}
+
+		if converted := toProviderPR(bbPR, pr); converted != nil {
+			prs = append(prs, *converted)
+		}
+	}
+	return prs, nil
+}
+
+func (p *provider) getFullCommitSHA(ctx context.Context, shortSHA string) (string, error) {
+	if shortSHA == "" {
+		return "", nil
+	}
+
+	commitOpts := &bitbucket.CommitsOptions{
+		Owner:    p.owner,
+		RepoSlug: p.repoSlug,
+		Revision: shortSHA,
+	}
+	commitOpts.WithContext(ctx)
+
+	resp, err := p.client.GetCommit(commitOpts)
+	if err != nil {
+		return "", err
+	}
+
+	commitResp, ok := resp.(map[string]any)
+	if !ok {
+		return "", fmt.Errorf("unexpected commit response type: %T", resp)
+	}
+
+	hash, ok := commitResp["hash"].(string)
+	if !ok || hash == "" {
+		return "", fmt.Errorf("commit response missing 'hash' field")
+	}
+	return hash, nil
+}
+
+// bitbucketPR represents the structure of a Bitbucket pull request.
+// See: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-pull-request-id-get
+// nolint:lll
+type bitbucketPR struct {
+	ID    int64  `json:"id"`
+	State string `json:"state"`
+	Links struct {
+		HTML struct {
+			Href string `json:"href"`
+		} `json:"html"`
+	} `json:"links"`
+	Source struct {
+		Branch struct {
+			Name string `json:"name"`
+		} `json:"branch"`
+		Commit struct {
+			Hash string `json:"hash"`
+		} `json:"commit"`
+	} `json:"source"`
+	Destination struct {
+		Branch struct {
+			Name string `json:"name"`
+		} `json:"branch"`
+		Commit struct {
+			Hash string `json:"hash"`
+		} `json:"commit"`
+	} `json:"destination"`
+	MergeCommit struct {
+		Hash string `json:"hash"`
+	} `json:"merge_commit"`
+	CreatedOn string `json:"created_on"`
+}
+
+// toBitbucketPR converts a raw response to a bitbucketPR type.
+func toBitbucketPR(resp any) (*bitbucketPR, error) {
+	b, err := json.Marshal(resp)
+	if err != nil {
+		return nil, fmt.Errorf("marshal PR response: %w", err)
+	}
+	var pr bitbucketPR
+	if err = json.Unmarshal(b, &pr); err != nil {
+		return nil, fmt.Errorf("unmarshal PR response: %w", err)
+	}
+	return &pr, nil
+}
+
+// toProviderPR converts a bitbucketPR to a gitprovider.PullRequest.
+func toProviderPR(pr *bitbucketPR, raw any) *gitprovider.PullRequest {
+	if pr == nil {
+		return nil
+	}
+
+	var createdAt *time.Time
+	if ts, err := time.Parse("2006-01-02T15:04:05Z", pr.CreatedOn); err == nil {
+		createdAt = &ts
+	}
+
+	return &gitprovider.PullRequest{
+		Number: pr.ID,
+		URL:    pr.Links.HTML.Href,
+		Open:   pr.State == prStateOpen,
+		Merged: pr.State == prStateMerged,
+		// NB: As a sign of true craftsmanship, or lack thereof, the Bitbucket
+		// API returns a short commit SHA as merge commit hash. To get the full
+		// commit SHA, we need to fetch the commit details separately.
+		MergeCommitSHA: pr.MergeCommit.Hash,
+		HeadSHA:        pr.Source.Commit.Hash,
+		CreatedAt:      createdAt,
+		Object:         raw,
+	}
+}
+
+// parseRepoURL extracts host, owner and repo slug from a repository URL
+func parseRepoURL(repoURL string) (host, owner, slug string, err error) {
+	u, err := url.Parse(git.NormalizeURL(repoURL))
+	if err != nil {
+		return "", "", "", fmt.Errorf("parse Bitbucket URL %q: %w", repoURL, err)
+	}
+
+	path := strings.TrimPrefix(u.Path, "/")
+	parts := strings.Split(path, "/")
+	if len(parts) != 2 {
+		return "", "", "", fmt.Errorf("invalid repository path in URL %q", u)
+	}
+
+	return u.Hostname(), parts[0], parts[1], nil
+}
diff --git a/internal/gitprovider/gitprovider.go b/internal/gitprovider/gitprovider.go
index da5e8f46..bfa931af 100644
--- a/internal/gitprovider/gitprovider.go
+++ b/internal/gitprovider/gitprovider.go
@@ -43,7 +43,7 @@ type Interface interface {
 	// CreatePullRequest creates a pull request.
 	CreatePullRequest(context.Context, *CreatePullRequestOpts) (*PullRequest, error)
 
-	// Get gets an existing pull request by ID
+	// GetPullRequest gets an existing pull request by ID
 	GetPullRequest(context.Context, int64) (*PullRequest, error)
 
 	// ListPullRequests lists pull requests by the given options. Implementations
diff --git a/internal/promotion/runner/builtin/git_pr_opener.go b/internal/promotion/runner/builtin/git_pr_opener.go
index d85ea99b..9e569b4d 100644
--- a/internal/promotion/runner/builtin/git_pr_opener.go
+++ b/internal/promotion/runner/builtin/git_pr_opener.go
@@ -17,6 +17,7 @@ import (
 	"github.com/akuity/kargo/pkg/x/promotion/runner/builtin"
 
 	_ "github.com/akuity/kargo/internal/gitprovider/azure"  // Azure provider registration
+	_ "github.com/akuity/kargo/internal/gitprovider/bitbucket" // Bitbucket provider registration
 	_ "github.com/akuity/kargo/internal/gitprovider/gitea"  // Gitea provider registration
 	_ "github.com/akuity/kargo/internal/gitprovider/github" // GitHub provider registration
 	_ "github.com/akuity/kargo/internal/gitprovider/gitlab" // GitLab provider registration
diff --git a/internal/promotion/runner/builtin/git_pr_waiter.go b/internal/promotion/runner/builtin/git_pr_waiter.go
index 084d97d3..de6146dc 100644
--- a/internal/promotion/runner/builtin/git_pr_waiter.go
+++ b/internal/promotion/runner/builtin/git_pr_waiter.go
@@ -13,6 +13,12 @@ import (
 	"github.com/akuity/kargo/pkg/promotion"
 	promoPkg "github.com/akuity/kargo/pkg/promotion"
 	"github.com/akuity/kargo/pkg/x/promotion/runner/builtin"
+
+	_ "github.com/akuity/kargo/internal/gitprovider/azure"  // Azure provider registration
+	_ "github.com/akuity/kargo/internal/gitprovider/bitbucket" // Bitbucket provider registration
+	_ "github.com/akuity/kargo/internal/gitprovider/gitea"  // Gitea provider registration
+	_ "github.com/akuity/kargo/internal/gitprovider/github" // GitHub provider registration
+	_ "github.com/akuity/kargo/internal/gitprovider/gitlab" // GitLab provider registration
 )
 
 // gitPRWaiter is an implementation of the promotion.StepRunner interface that
diff --git a/internal/promotion/runner/builtin/schemas/git-open-pr-config.json b/internal/promotion/runner/builtin/schemas/git-open-pr-config.json
index b812aa46..02087e23 100644
--- a/internal/promotion/runner/builtin/schemas/git-open-pr-config.json
+++ b/internal/promotion/runner/builtin/schemas/git-open-pr-config.json
@@ -15,8 +15,8 @@
     },
     "provider": {
       "type": "string",
-      "description": "The name of the Git provider to use. Currently only 'github', 'gitlab', 'gitea' and 'azure' are supported. Kargo will try to infer the provider if it is not explicitly specified.",
-      "enum": ["github", "gitlab", "azure", "gitea"]
+      "description": "The name of the Git provider to use. Currently 'azure', 'bitbucket', 'gitea', 'github', and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly specified.",
+      "enum": ["azure", "bitbucket", "gitea", "github", "gitlab"]
     },
     "repoURL": {
       "type": "string",
diff --git a/internal/promotion/runner/builtin/schemas/git-wait-for-pr-config.json b/internal/promotion/runner/builtin/schemas/git-wait-for-pr-config.json
index 60f0d96c..e309fb9b 100644
--- a/internal/promotion/runner/builtin/schemas/git-wait-for-pr-config.json
+++ b/internal/promotion/runner/builtin/schemas/git-wait-for-pr-config.json
@@ -11,8 +11,8 @@
     },
     "provider": {
       "type": "string",
-      "description": "The name of the Git provider to use. Currently only 'github', 'gitlab', 'gitea' and 'azure' are supported. Kargo will try to infer the provider if it is not explicitly specified.",
-      "enum": ["github", "gitlab", "azure", "gitea"]
+      "description": "The name of the Git provider to use. Currently 'azure', 'bitbucket', 'gitea', 'github', and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly specified.",
+      "enum": ["azure", "bitbucket", "gitea", "github", "gitlab"]
     },
     "prNumber": {
       "type": "integer",
diff --git a/pkg/x/promotion/runner/builtin/zz_config_types.go b/pkg/x/promotion/runner/builtin/zz_config_types.go
index 032881e2..fca3afa9 100644
--- a/pkg/x/promotion/runner/builtin/zz_config_types.go
+++ b/pkg/x/promotion/runner/builtin/zz_config_types.go
@@ -156,8 +156,8 @@ type GitOpenPRConfig struct {
 	InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
 	// Labels to add to the pull request.
 	Labels []string `json:"labels,omitempty"`
-	// The name of the Git provider to use. Currently only 'github', 'gitlab', 'gitea' and
-	// 'azure' are supported. Kargo will try to infer the provider if it is not explicitly
+	// The name of the Git provider to use. Currently 'azure', 'bitbucket', 'gitea', 'github',
+	// and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly
 	// specified.
 	Provider *Provider `json:"provider,omitempty"`
 	// The URL of a remote Git repository to clone.
@@ -196,8 +196,8 @@ type GitWaitForPRConfig struct {
 	InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
 	// The number of the pull request to wait for.
 	PRNumber int64 `json:"prNumber"`
-	// The name of the Git provider to use. Currently only 'github', 'gitlab', 'gitea' and
-	// 'azure' are supported. Kargo will try to infer the provider if it is not explicitly
+	// The name of the Git provider to use. Currently 'azure', 'bitbucket', 'gitea', 'github',
+	// and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly
 	// specified.
 	Provider *Provider `json:"provider,omitempty"`
 	// The URL of a remote Git repository to clone.
@@ -413,14 +413,15 @@ type YAMLUpdate struct {
 	Value string `json:"value"`
 }
 
-// The name of the Git provider to use. Currently only 'github', 'gitlab', 'gitea' and
-// 'azure' are supported. Kargo will try to infer the provider if it is not explicitly
+// The name of the Git provider to use. Currently 'azure', 'bitbucket', 'gitea', 'github',
+// and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly
 // specified.
 type Provider string
 
 const (
-	Azure  Provider = "azure"
-	Gitea  Provider = "gitea"
-	Github Provider = "github"
-	Gitlab Provider = "gitlab"
+	Azure     Provider = "azure"
+	Bitbucket Provider = "bitbucket"
+	Gitea     Provider = "gitea"
+	Github    Provider = "github"
+	Gitlab    Provider = "gitlab"
 )
diff --git a/ui/src/gen/api/v1alpha1/generated_pb.ts b/ui/src/gen/api/v1alpha1/generated_pb.ts
index a9935b7f..7bf0ee05 100644
--- a/ui/src/gen/api/v1alpha1/generated_pb.ts
+++ b/ui/src/gen/api/v1alpha1/generated_pb.ts
@@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf";
  * Describes the file api/v1alpha1/generated.proto.
  */
 export const file_api_v1alpha1_generated: GenFile = /*@__PURE__*/
-  fileDesc("ChxhcGkvdjFhbHBoYTEvZ2VuZXJhdGVkLnByb3RvEiRnaXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEiMgoTQW5hbHlzaXNSdW5Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIrACChNBbmFseXNpc1J1bk1ldGFkYXRhElUKBmxhYmVscxgBIAMoCzJFLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhLkxhYmVsc0VudHJ5El8KC2Fubm90YXRpb25zGAIgAygLMkouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGEuQW5ub3RhdGlvbnNFbnRyeRotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJGChRBbmFseXNpc1J1blJlZmVyZW5jZRIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRINCgVwaGFzZRgDIAEoCSI3ChlBbmFseXNpc1RlbXBsYXRlUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSJPCg1BcHByb3ZlZFN0YWdlEj4KCmFwcHJvdmVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSI4ChVBcmdvQ0RBcHBIZWFsdGhTdGF0dXMSDgoGc3RhdHVzGAEgASgJEg8KB21lc3NhZ2UYAiABKAki1AEKD0FyZ29DREFwcFN0YXR1cxIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRJRCgxoZWFsdGhTdGF0dXMYAyABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwSGVhbHRoU3RhdHVzEk0KCnN5bmNTdGF0dXMYBCABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwU3luY1N0YXR1cyJKChNBcmdvQ0RBcHBTeW5jU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIQCghyZXZpc2lvbhgCIAEoCRIRCglyZXZpc2lvbnMYAyADKAkiNwoFQ2hhcnQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiYQoUQ2hhcnREaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSEAoIdmVyc2lvbnMYBCADKAkiZAoRQ2hhcnRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSFgoOZGlzY292ZXJ5TGltaXQYBCABKAUioQEKFENsdXN0ZXJQcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKnAQoYQ2x1c3RlclByb21vdGlvblRhc2tMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEkkKBWl0ZW1zGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNsdXN0ZXJQcm9tb3Rpb25UYXNrIkkKDEN1cnJlbnRTdGFnZRI5CgVzaW5jZRgBIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIrYCChNEaXNjb3ZlcmVkQXJ0aWZhY3RzEkAKDGRpc2NvdmVyZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEkUKA2dpdBgBIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXREaXNjb3ZlcnlSZXN1bHQSSgoGaW1hZ2VzGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlRGlzY292ZXJ5UmVzdWx0EkoKBmNoYXJ0cxgDIAMoCzI6LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydERpc2NvdmVyeVJlc3VsdCKwAQoQRGlzY292ZXJlZENvbW1pdBIKCgJpZBgBIAEoCRIOCgZicmFuY2gYAiABKAkSCwoDdGFnGAMgASgJEg8KB3N1YmplY3QYBCABKAkSDgoGYXV0aG9yGAUgASgJEhEKCWNvbW1pdHRlchgGIAEoCRI/CgtjcmVhdG9yRGF0ZRgHIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIqQCChhEaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2USCwoDdGFnGAEgASgJEg4KBmRpZ2VzdBgCIAEoCRJkCgthbm5vdGF0aW9ucxgFIAMoCzJPLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2UuQW5ub3RhdGlvbnNFbnRyeRISCgpnaXRSZXBvVVJMGAMgASgJEj0KCWNyZWF0ZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChJFeHByZXNzaW9uVmFyaWFibGUSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSKiAwoHRnJlaWdodBJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEg0KBWFsaWFzGAcgASgJEkMKBm9yaWdpbhgJIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkAKB2NvbW1pdHMYAyADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0Q29tbWl0EjsKBmltYWdlcxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZRI7CgZjaGFydHMYBSADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQ2hhcnQSQwoGc3RhdHVzGAYgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMirQIKEUZyZWlnaHRDb2xsZWN0aW9uEgoKAmlkGAMgASgJElEKBWl0ZW1zGAEgAygLMkIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uLkl0ZW1zRW50cnkSUwoTdmVyaWZpY2F0aW9uSGlzdG9yeRgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5WZXJpZmljYXRpb25JbmZvGmQKCkl0ZW1zRW50cnkSCwoDa2V5GAEgASgJEkUKBXZhbHVlGAIgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2U6AjgBIo0BCgtGcmVpZ2h0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0IisKDUZyZWlnaHRPcmlnaW4SDAoEa2luZBgBIAEoCRIMCgRuYW1lGAIgASgJIqECChBGcmVpZ2h0UmVmZXJlbmNlEgwKBG5hbWUYASABKAkSQwoGb3JpZ2luGAggASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SQAoHY29tbWl0cxgCIAMoCzIvLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXRDb21taXQSOwoGaW1hZ2VzGAMgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlEjsKBmNoYXJ0cxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydCKcAQoORnJlaWdodFJlcXVlc3QSQwoGb3JpZ2luGAEgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SRQoHc291cmNlcxgCIAEoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0U291cmNlcyKYAQoORnJlaWdodFNvdXJjZXMSDgoGZGlyZWN0GAEgASgIEg4KBnN0YWdlcxgCIAMoCRJIChByZXF1aXJlZFNvYWtUaW1lGAMgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhwKFGF2YWlsYWJpbGl0eVN0cmF0ZWd5GAQgASgJItcECg1GcmVpZ2h0U3RhdHVzElkKC2N1cnJlbnRseUluGAMgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQ3VycmVudGx5SW5FbnRyeRJXCgp2ZXJpZmllZEluGAEgAygLMkMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuVmVyaWZpZWRJbkVudHJ5ElkKC2FwcHJvdmVkRm9yGAIgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQXBwcm92ZWRGb3JFbnRyeRpmChBDdXJyZW50bHlJbkVudHJ5EgsKA2tleRgBIAEoCRJBCgV2YWx1ZRgCIAEoCzIyLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DdXJyZW50U3RhZ2U6AjgBGmYKD1ZlcmlmaWVkSW5FbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpZWRTdGFnZToCOAEaZwoQQXBwcm92ZWRGb3JFbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXBwcm92ZWRTdGFnZToCOAEieQoJR2l0Q29tbWl0Eg8KB3JlcG9VUkwYASABKAkSCgoCaWQYAiABKAkSDgoGYnJhbmNoGAMgASgJEgsKA3RhZxgEIAEoCRIPCgdtZXNzYWdlGAYgASgJEg4KBmF1dGhvchgHIAEoCRIRCgljb21taXR0ZXIYCCABKAkibgoSR2l0RGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSRwoHY29tbWl0cxgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQ29tbWl0Io4CCg9HaXRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIfChdjb21taXRTZWxlY3Rpb25TdHJhdGVneRgCIAEoCRIOCgZicmFuY2gYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgLIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgHIAEoCBIUCgxpbmNsdWRlUGF0aHMYCCADKAkSFAoMZXhjbHVkZVBhdGhzGAkgAygJEhYKDmRpc2NvdmVyeUxpbWl0GAogASgFIsgBCgZIZWFsdGgSDgoGc3RhdHVzGAEgASgJEg4KBmlzc3VlcxgCIAMoCRJOCgZjb25maWcYBCABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OEk4KBm91dHB1dBgFIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibwoPSGVhbHRoQ2hlY2tTdGVwEgwKBHVzZXMYASABKAkSTgoGY29uZmlnGAIgASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiLQAQoFSW1hZ2USDwoHcmVwb1VSTBgBIAEoCRISCgpnaXRSZXBvVVJMGAIgASgJEgsKA3RhZxgDIAEoCRIOCgZkaWdlc3QYBCABKAkSUQoLYW5ub3RhdGlvbnMYBSADKAsyPC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuSW1hZ2UuQW5ub3RhdGlvbnNFbnRyeRoyChBBbm5vdGF0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEijQEKFEltYWdlRGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSEAoIcGxhdGZvcm0YAiABKAkSUgoKcmVmZXJlbmNlcxgDIAMoCzI+LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2Ui+QEKEUltYWdlU3Vic2NyaXB0aW9uEg8KB3JlcG9VUkwYASABKAkSEgoKZ2l0UmVwb1VSTBgCIAEoCRIeChZpbWFnZVNlbGVjdGlvblN0cmF0ZWd5GAMgASgJEhUKDXN0cmljdFNlbXZlcnMYCiABKAgSGAoQc2VtdmVyQ29uc3RyYWludBgEIAEoCRIRCglhbGxvd1RhZ3MYBSABKAkSEgoKaWdub3JlVGFncxgGIAMoCRIQCghwbGF0Zm9ybRgHIAEoCRIdChVpbnNlY3VyZVNraXBUTFNWZXJpZnkYCCABKAgSFgoOZGlzY292ZXJ5TGltaXQYCSABKAUi2QEKB1Byb2plY3QSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRJFCgRzcGVjGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RDb25maWdTcGVjEkMKBnN0YXR1cxgDIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0U3RhdHVzIpoBCg1Qcm9qZWN0Q29uZmlnEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0Q29uZmlnU3BlYyKZAQoRUHJvamVjdENvbmZpZ0xpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESQgoFaXRlbXMYAiADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvamVjdENvbmZpZyJlChFQcm9qZWN0Q29uZmlnU3BlYxJQChFwcm9tb3Rpb25Qb2xpY2llcxgBIAMoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25Qb2xpY3kijQEKC1Byb2plY3RMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEjwKBWl0ZW1zGAIgAygLMi0uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3QidAoNUHJvamVjdFN0YXR1cxJDCgpjb25kaXRpb25zGAMgAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhINCgVwaGFzZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJItkBCglQcm9tb3Rpb24SQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRJBCgRzcGVjGAIgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblNwZWMSRQoGc3RhdHVzGAMgASgLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0YXR1cyKRAQoNUHJvbW90aW9uTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI+CgVpdGVtcxgCIAMoCzIvLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb24iPgoPUHJvbW90aW9uUG9saWN5Eg0KBXN0YWdlGAEgASgJEhwKFGF1dG9Qcm9tb3Rpb25FbmFibGVkGAIgASgIIvIBChJQcm9tb3Rpb25SZWZlcmVuY2USDAoEbmFtZRgBIAEoCRJHCgdmcmVpZ2h0GAIgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2USRQoGc3RhdHVzGAMgASgLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0YXR1cxI+CgpmaW5pc2hlZEF0GAQgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUiuwEKDVByb21vdGlvblNwZWMSDQoFc3RhZ2UYASABKAkSDwoHZnJlaWdodBgCIAEoCRJGCgR2YXJzGAQgAygLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkV4cHJlc3Npb25WYXJpYWJsZRJCCgVzdGVwcxgDIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIrcECg9Qcm9tb3Rpb25TdGF0dXMSGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAQgASgJEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSRwoHZnJlaWdodBgFIAEoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVmZXJlbmNlElIKEWZyZWlnaHRDb2xsZWN0aW9uGAcgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uEksKDGhlYWx0aENoZWNrcxgIIAMoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5IZWFsdGhDaGVja1N0ZXASPgoKZmluaXNoZWRBdBgGIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEhMKC2N1cnJlbnRTdGVwGAkgASgDEloKFXN0ZXBFeGVjdXRpb25NZXRhZGF0YRgLIAMoCzI7LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGVwRXhlY3V0aW9uTWV0YWRhdGESTQoFc3RhdGUYCiABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OIuICCg1Qcm9tb3Rpb25TdGVwEgwKBHVzZXMYASABKAkSSgoEdGFzaxgFIAEoCzI8LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrUmVmZXJlbmNlEgoKAmFzGAIgASgJEgoKAmlmGAcgASgJEkcKBXJldHJ5GAQgASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0ZXBSZXRyeRJGCgR2YXJzGAYgAygLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkV4cHJlc3Npb25WYXJpYWJsZRJOCgZjb25maWcYAyABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OIm0KElByb21vdGlvblN0ZXBSZXRyeRI/Cgd0aW1lb3V0GAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhYKDmVycm9yVGhyZXNob2xkGAIgASgNIpoBCg1Qcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKZAQoRUHJvbW90aW9uVGFza0xpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESQgoFaXRlbXMYAiADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGFzayI0ChZQcm9tb3Rpb25UYXNrUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSKfAQoRUHJvbW90aW9uVGFza1NwZWMSRgoEdmFycxgBIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5FeHByZXNzaW9uVmFyaWFibGUSQgoFc3RlcHMYAiADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcCJeChFQcm9tb3Rpb25UZW1wbGF0ZRJJCgRzcGVjGAEgASgLMjsuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblRlbXBsYXRlU3BlYyKjAQoVUHJvbW90aW9uVGVtcGxhdGVTcGVjEkYKBHZhcnMYAiADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEkIKBXN0ZXBzGAEgAygLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0ZXAi5gEKEFJlcG9TdWJzY3JpcHRpb24SQgoDZ2l0GAEgASgLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkdpdFN1YnNjcmlwdGlvbhJGCgVpbWFnZRgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZVN1YnNjcmlwdGlvbhJGCgVjaGFydBgDIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydFN1YnNjcmlwdGlvbiLNAQoFU3RhZ2USQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRI9CgRzcGVjGAIgASgLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlU3BlYxJBCgZzdGF0dXMYAyABKAsyMS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RhZ2VTdGF0dXMiiQEKCVN0YWdlTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI6CgVpdGVtcxgCIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGFnZSLQAgoJU3RhZ2VTcGVjEg0KBXNoYXJkGAQgASgJEkYKBHZhcnMYByADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEk4KEHJlcXVlc3RlZEZyZWlnaHQYBSADKAsyNC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlcXVlc3QSUgoRcHJvbW90aW9uVGVtcGxhdGUYBiABKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGUSSAoMdmVyaWZpY2F0aW9uGAMgASgLMjIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlZlcmlmaWNhdGlvbiL2AwoLU3RhZ2VTdGF0dXMSQwoKY29uZGl0aW9ucxgNIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAsgASgJEg0KBXBoYXNlGAEgASgJEk8KDmZyZWlnaHRIaXN0b3J5GAQgAygLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uEhYKDmZyZWlnaHRTdW1tYXJ5GAwgASgJEjwKBmhlYWx0aBgIIAEoCzIsLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5IZWFsdGgSDwoHbWVzc2FnZRgJIAEoCRIaChJvYnNlcnZlZEdlbmVyYXRpb24YBiABKAMSUgoQY3VycmVudFByb21vdGlvbhgHIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2USTwoNbGFzdFByb21vdGlvbhgKIAEoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25SZWZlcmVuY2Ui2gEKFVN0ZXBFeGVjdXRpb25NZXRhZGF0YRINCgVhbGlhcxgBIAEoCRI9CglzdGFydGVkQXQYAiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRI+CgpmaW5pc2hlZEF0GAMgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSEgoKZXJyb3JDb3VudBgEIAEoDRIOCgZzdGF0dXMYBSABKAkSDwoHbWVzc2FnZRgGIAEoCSKLAgoMVmVyaWZpY2F0aW9uEloKEWFuYWx5c2lzVGVtcGxhdGVzGAEgAygLMj8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzVGVtcGxhdGVSZWZlcmVuY2USVgoTYW5hbHlzaXNSdW5NZXRhZGF0YRgCIAEoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhEkcKBGFyZ3MYAyADKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5Bcmd1bWVudCKdAgoQVmVyaWZpY2F0aW9uSW5mbxIKCgJpZBgEIAEoCRINCgVhY3RvchgHIAEoCRI9CglzdGFydFRpbWUYBSABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRINCgVwaGFzZRgBIAEoCRIPCgdtZXNzYWdlGAIgASgJEk8KC2FuYWx5c2lzUnVuGAMgASgLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuUmVmZXJlbmNlEj4KCmZpbmlzaFRpbWUYBiABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSKUAQoNVmVyaWZpZWRTdGFnZRI+Cgp2ZXJpZmllZEF0GAEgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSQwoLbG9uZ2VzdFNvYWsYAiABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuRHVyYXRpb24i2QEKCVdhcmVob3VzZRJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkEKBHNwZWMYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlU3BlYxJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlU3RhdHVzIpEBCg1XYXJlaG91c2VMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEj4KBWl0ZW1zGAIgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLldhcmVob3VzZSLOAQoNV2FyZWhvdXNlU3BlYxINCgVzaGFyZBgCIAEoCRJACghpbnRlcnZhbBgEIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5EdXJhdGlvbhIdChVmcmVpZ2h0Q3JlYXRpb25Qb2xpY3kYAyABKAkSTQoNc3Vic2NyaXB0aW9ucxgBIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5SZXBvU3Vic2NyaXB0aW9uIv0BCg9XYXJlaG91c2VTdGF0dXMSQwoKY29uZGl0aW9ucxgJIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAYgASgJEhoKEm9ic2VydmVkR2VuZXJhdGlvbhgEIAEoAxIVCg1sYXN0RnJlaWdodElEGAggASgJElYKE2Rpc2NvdmVyZWRBcnRpZmFjdHMYByABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRGlzY292ZXJlZEFydGlmYWN0c0KXAgooY29tLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMUIOR2VuZXJhdGVkUHJvdG9QAVokZ2l0aHViLmNvbS9ha3VpdHkva2FyZ28vYXBpL3YxYWxwaGExogIFR0NBS0GqAiRHaXRodWIuQ29tLkFrdWl0eS5LYXJnby5BcGkuVjFhbHBoYTHKAiRHaXRodWJcQ29tXEFrdWl0eVxLYXJnb1xBcGlcVjFhbHBoYTHiAjBHaXRodWJcQ29tXEFrdWl0eVxLYXJnb1xBcGlcVjFhbHBoYTFcR1BCTWV0YWRhdGHqAilHaXRodWI6OkNvbTo6QWt1aXR5OjpLYXJnbzo6QXBpOjpWMWFscGhhMQ", [file_k8s_io_apiextensions_apiserver_pkg_apis_apiextensions_v1_generated, file_k8s_io_apimachinery_pkg_apis_meta_v1_generated, file_k8s_io_apimachinery_pkg_runtime_generated, file_k8s_io_apimachinery_pkg_runtime_schema_generated]);
+  fileDesc("ChxhcGkvdjFhbHBoYTEvZ2VuZXJhdGVkLnByb3RvEiRnaXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEiMgoTQW5hbHlzaXNSdW5Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIrACChNBbmFseXNpc1J1bk1ldGFkYXRhElUKBmxhYmVscxgBIAMoCzJFLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bk1ldGFkYXRhLkxhYmVsc0VudHJ5El8KC2Fubm90YXRpb25zGAIgAygLMkouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGEuQW5ub3RhdGlvbnNFbnRyeRotCgtMYWJlbHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJGChRBbmFseXNpc1J1blJlZmVyZW5jZRIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRINCgVwaGFzZRgDIAEoCSI3ChlBbmFseXNpc1RlbXBsYXRlUmVmZXJlbmNlEgwKBG5hbWUYASABKAkSDAoEa2luZBgCIAEoCSJPCg1BcHByb3ZlZFN0YWdlEj4KCmFwcHJvdmVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSI4ChVBcmdvQ0RBcHBIZWFsdGhTdGF0dXMSDgoGc3RhdHVzGAEgASgJEg8KB21lc3NhZ2UYAiABKAki1AEKD0FyZ29DREFwcFN0YXR1cxIRCgluYW1lc3BhY2UYASABKAkSDAoEbmFtZRgCIAEoCRJRCgxoZWFsdGhTdGF0dXMYAyABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwSGVhbHRoU3RhdHVzEk0KCnN5bmNTdGF0dXMYBCABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXJnb0NEQXBwU3luY1N0YXR1cyJKChNBcmdvQ0RBcHBTeW5jU3RhdHVzEg4KBnN0YXR1cxgBIAEoCRIQCghyZXZpc2lvbhgCIAEoCRIRCglyZXZpc2lvbnMYAyADKAkiNwoFQ2hhcnQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiYQoUQ2hhcnREaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSEAoIdmVyc2lvbnMYBCADKAkiZAoRQ2hhcnRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIMCgRuYW1lGAIgASgJEhgKEHNlbXZlckNvbnN0cmFpbnQYAyABKAkSFgoOZGlzY292ZXJ5TGltaXQYBCABKAUioQEKFENsdXN0ZXJQcm9tb3Rpb25UYXNrEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESRQoEc3BlYxgCIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrU3BlYyKnAQoYQ2x1c3RlclByb21vdGlvblRhc2tMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEkkKBWl0ZW1zGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNsdXN0ZXJQcm9tb3Rpb25UYXNrIkkKDEN1cnJlbnRTdGFnZRI5CgVzaW5jZRgBIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIrYCChNEaXNjb3ZlcmVkQXJ0aWZhY3RzEkAKDGRpc2NvdmVyZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEkUKA2dpdBgBIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXREaXNjb3ZlcnlSZXN1bHQSSgoGaW1hZ2VzGAIgAygLMjouZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlRGlzY292ZXJ5UmVzdWx0EkoKBmNoYXJ0cxgDIAMoCzI6LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydERpc2NvdmVyeVJlc3VsdCKwAQoQRGlzY292ZXJlZENvbW1pdBIKCgJpZBgBIAEoCRIOCgZicmFuY2gYAiABKAkSCwoDdGFnGAMgASgJEg8KB3N1YmplY3QYBCABKAkSDgoGYXV0aG9yGAUgASgJEhEKCWNvbW1pdHRlchgGIAEoCRI/CgtjcmVhdG9yRGF0ZRgHIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIqQCChhEaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2USCwoDdGFnGAEgASgJEg4KBmRpZ2VzdBgCIAEoCRJkCgthbm5vdGF0aW9ucxgFIAMoCzJPLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkSW1hZ2VSZWZlcmVuY2UuQW5ub3RhdGlvbnNFbnRyeRISCgpnaXRSZXBvVVJMGAMgASgJEj0KCWNyZWF0ZWRBdBgEIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lGjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIxChJFeHByZXNzaW9uVmFyaWFibGUSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSKiAwoHRnJlaWdodBJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEg0KBWFsaWFzGAcgASgJEkMKBm9yaWdpbhgJIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0T3JpZ2luEkAKB2NvbW1pdHMYAyADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0Q29tbWl0EjsKBmltYWdlcxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZRI7CgZjaGFydHMYBSADKAsyKy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQ2hhcnQSQwoGc3RhdHVzGAYgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMirQIKEUZyZWlnaHRDb2xsZWN0aW9uEgoKAmlkGAMgASgJElEKBWl0ZW1zGAEgAygLMkIuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRDb2xsZWN0aW9uLkl0ZW1zRW50cnkSUwoTdmVyaWZpY2F0aW9uSGlzdG9yeRgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5WZXJpZmljYXRpb25JbmZvGmQKCkl0ZW1zRW50cnkSCwoDa2V5GAEgASgJEkUKBXZhbHVlGAIgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2U6AjgBIo0BCgtGcmVpZ2h0TGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRI8CgVpdGVtcxgCIAMoCzItLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0IisKDUZyZWlnaHRPcmlnaW4SDAoEa2luZBgBIAEoCRIMCgRuYW1lGAIgASgJIqECChBGcmVpZ2h0UmVmZXJlbmNlEgwKBG5hbWUYASABKAkSQwoGb3JpZ2luGAggASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SQAoHY29tbWl0cxgCIAMoCzIvLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5HaXRDb21taXQSOwoGaW1hZ2VzGAMgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlEjsKBmNoYXJ0cxgEIAMoCzIrLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DaGFydCKcAQoORnJlaWdodFJlcXVlc3QSQwoGb3JpZ2luGAEgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRPcmlnaW4SRQoHc291cmNlcxgCIAEoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0U291cmNlcyKYAQoORnJlaWdodFNvdXJjZXMSDgoGZGlyZWN0GAEgASgIEg4KBnN0YWdlcxgCIAMoCRJIChByZXF1aXJlZFNvYWtUaW1lGAMgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEhwKFGF2YWlsYWJpbGl0eVN0cmF0ZWd5GAQgASgJItcECg1GcmVpZ2h0U3RhdHVzElkKC2N1cnJlbnRseUluGAMgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQ3VycmVudGx5SW5FbnRyeRJXCgp2ZXJpZmllZEluGAEgAygLMkMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuVmVyaWZpZWRJbkVudHJ5ElkKC2FwcHJvdmVkRm9yGAIgAygLMkQuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRTdGF0dXMuQXBwcm92ZWRGb3JFbnRyeRpmChBDdXJyZW50bHlJbkVudHJ5EgsKA2tleRgBIAEoCRJBCgV2YWx1ZRgCIAEoCzIyLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5DdXJyZW50U3RhZ2U6AjgBGmYKD1ZlcmlmaWVkSW5FbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpZWRTdGFnZToCOAEaZwoQQXBwcm92ZWRGb3JFbnRyeRILCgNrZXkYASABKAkSQgoFdmFsdWUYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQXBwcm92ZWRTdGFnZToCOAEieQoJR2l0Q29tbWl0Eg8KB3JlcG9VUkwYASABKAkSCgoCaWQYAiABKAkSDgoGYnJhbmNoGAMgASgJEgsKA3RhZxgEIAEoCRIPCgdtZXNzYWdlGAYgASgJEg4KBmF1dGhvchgHIAEoCRIRCgljb21taXR0ZXIYCCABKAkibgoSR2l0RGlzY292ZXJ5UmVzdWx0Eg8KB3JlcG9VUkwYASABKAkSRwoHY29tbWl0cxgCIAMoCzI2LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5EaXNjb3ZlcmVkQ29tbWl0Io4CCg9HaXRTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRIfChdjb21taXRTZWxlY3Rpb25TdHJhdGVneRgCIAEoCRIOCgZicmFuY2gYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgLIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgHIAEoCBIUCgxpbmNsdWRlUGF0aHMYCCADKAkSFAoMZXhjbHVkZVBhdGhzGAkgAygJEhYKDmRpc2NvdmVyeUxpbWl0GAogASgFIsgBCgZIZWFsdGgSDgoGc3RhdHVzGAEgASgJEg4KBmlzc3VlcxgCIAMoCRJOCgZjb25maWcYBCABKAsyPi5rOHMuaW8uYXBpZXh0ZW5zaW9uc19hcGlzZXJ2ZXIucGtnLmFwaXMuYXBpZXh0ZW5zaW9ucy52MS5KU09OEk4KBm91dHB1dBgFIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibwoPSGVhbHRoQ2hlY2tTdGVwEgwKBHVzZXMYASABKAkSTgoGY29uZmlnGAIgASgLMj4uazhzLmlvLmFwaWV4dGVuc2lvbnNfYXBpc2VydmVyLnBrZy5hcGlzLmFwaWV4dGVuc2lvbnMudjEuSlNPTiIeCgtIZWFsdGhTdGF0cxIPCgdoZWFsdGh5GAEgASgDItABCgVJbWFnZRIPCgdyZXBvVVJMGAEgASgJEhIKCmdpdFJlcG9VUkwYAiABKAkSCwoDdGFnGAMgASgJEg4KBmRpZ2VzdBgEIAEoCRJRCgthbm5vdGF0aW9ucxgFIAMoCzI8LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5JbWFnZS5Bbm5vdGF0aW9uc0VudHJ5GjIKEEFubm90YXRpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASKNAQoUSW1hZ2VEaXNjb3ZlcnlSZXN1bHQSDwoHcmVwb1VSTBgBIAEoCRIQCghwbGF0Zm9ybRgCIAEoCRJSCgpyZWZlcmVuY2VzGAMgAygLMj4uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkRpc2NvdmVyZWRJbWFnZVJlZmVyZW5jZSL5AQoRSW1hZ2VTdWJzY3JpcHRpb24SDwoHcmVwb1VSTBgBIAEoCRISCgpnaXRSZXBvVVJMGAIgASgJEh4KFmltYWdlU2VsZWN0aW9uU3RyYXRlZ3kYAyABKAkSFQoNc3RyaWN0U2VtdmVycxgKIAEoCBIYChBzZW12ZXJDb25zdHJhaW50GAQgASgJEhEKCWFsbG93VGFncxgFIAEoCRISCgppZ25vcmVUYWdzGAYgAygJEhAKCHBsYXRmb3JtGAcgASgJEh0KFWluc2VjdXJlU2tpcFRMU1ZlcmlmeRgIIAEoCBIWCg5kaXNjb3ZlcnlMaW1pdBgJIAEoBSLZAQoHUHJvamVjdBJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkUKBHNwZWMYAiABKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvamVjdENvbmZpZ1NwZWMSQwoGc3RhdHVzGAMgASgLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RTdGF0dXMimgEKDVByb2plY3RDb25maWcSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRJFCgRzcGVjGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb2plY3RDb25maWdTcGVjIpkBChFQcm9qZWN0Q29uZmlnTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRJCCgVpdGVtcxgCIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0Q29uZmlnImUKEVByb2plY3RDb25maWdTcGVjElAKEXByb21vdGlvblBvbGljaWVzGAEgAygLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblBvbGljeSKNAQoLUHJvamVjdExpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESPAoFaXRlbXMYAiADKAsyLS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvamVjdCKaAQoMUHJvamVjdFN0YXRzEkgKCndhcmVob3VzZXMYASABKAsyNC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlU3RhdHMSQAoGc3RhZ2VzGAIgASgLMjAuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlU3RhdHMitwEKDVByb2plY3RTdGF0dXMSQwoKY29uZGl0aW9ucxgDIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SDQoFcGhhc2UYASABKAkSDwoHbWVzc2FnZRgCIAEoCRJBCgVzdGF0cxgEIAEoCzIyLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9qZWN0U3RhdHMi2QEKCVByb21vdGlvbhJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEkEKBHNwZWMYAiABKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3BlYxJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzIpEBCg1Qcm9tb3Rpb25MaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEj4KBWl0ZW1zGAIgAygLMi8uZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvbiI+Cg9Qcm9tb3Rpb25Qb2xpY3kSDQoFc3RhZ2UYASABKAkSHAoUYXV0b1Byb21vdGlvbkVuYWJsZWQYAiABKAgi8gEKElByb21vdGlvblJlZmVyZW5jZRIMCgRuYW1lGAEgASgJEkcKB2ZyZWlnaHQYAiABKAsyNi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodFJlZmVyZW5jZRJFCgZzdGF0dXMYAyABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RhdHVzEj4KCmZpbmlzaGVkQXQYBCABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZSK7AQoNUHJvbW90aW9uU3BlYxINCgVzdGFnZRgBIAEoCRIPCgdmcmVpZ2h0GAIgASgJEkYKBHZhcnMYBCADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEkIKBXN0ZXBzGAMgAygLMjMuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblN0ZXAitwQKD1Byb21vdGlvblN0YXR1cxIaChJsYXN0SGFuZGxlZFJlZnJlc2gYBCABKAkSDQoFcGhhc2UYASABKAkSDwoHbWVzc2FnZRgCIAEoCRJHCgdmcmVpZ2h0GAUgASgLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkZyZWlnaHRSZWZlcmVuY2USUgoRZnJlaWdodENvbGxlY3Rpb24YByABKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodENvbGxlY3Rpb24SSwoMaGVhbHRoQ2hlY2tzGAggAygLMjUuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkhlYWx0aENoZWNrU3RlcBI+CgpmaW5pc2hlZEF0GAYgASgLMiouazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLlRpbWUSEwoLY3VycmVudFN0ZXAYCSABKAMSWgoVc3RlcEV4ZWN1dGlvbk1ldGFkYXRhGAsgAygLMjsuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0ZXBFeGVjdXRpb25NZXRhZGF0YRJNCgVzdGF0ZRgKIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04i4gIKDVByb21vdGlvblN0ZXASDAoEdXNlcxgBIAEoCRJKCgR0YXNrGAUgASgLMjwuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblRhc2tSZWZlcmVuY2USCgoCYXMYAiABKAkSCgoCaWYYByABKAkSRwoFcmV0cnkYBCABKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcFJldHJ5EkYKBHZhcnMYBiADKAsyOC5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRXhwcmVzc2lvblZhcmlhYmxlEk4KBmNvbmZpZxgDIAEoCzI+Lms4cy5pby5hcGlleHRlbnNpb25zX2FwaXNlcnZlci5wa2cuYXBpcy5hcGlleHRlbnNpb25zLnYxLkpTT04ibQoSUHJvbW90aW9uU3RlcFJldHJ5Ej8KB3RpbWVvdXQYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuRHVyYXRpb24SFgoOZXJyb3JUaHJlc2hvbGQYAiABKA0imgEKDVByb21vdGlvblRhc2sSQgoIbWV0YWRhdGEYASABKAsyMC5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuT2JqZWN0TWV0YRJFCgRzcGVjGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblRhc2tTcGVjIpkBChFQcm9tb3Rpb25UYXNrTGlzdBJACghtZXRhZGF0YRgBIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5MaXN0TWV0YRJCCgVpdGVtcxgCIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UYXNrIjQKFlByb21vdGlvblRhc2tSZWZlcmVuY2USDAoEbmFtZRgBIAEoCRIMCgRraW5kGAIgASgJIp8BChFQcm9tb3Rpb25UYXNrU3BlYxJGCgR2YXJzGAEgAygLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkV4cHJlc3Npb25WYXJpYWJsZRJCCgVzdGVwcxgCIAMoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25TdGVwIl4KEVByb21vdGlvblRlbXBsYXRlEkkKBHNwZWMYASABKAsyOy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uVGVtcGxhdGVTcGVjIqMBChVQcm9tb3Rpb25UZW1wbGF0ZVNwZWMSRgoEdmFycxgCIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5FeHByZXNzaW9uVmFyaWFibGUSQgoFc3RlcHMYASADKAsyMy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuUHJvbW90aW9uU3RlcCLmAQoQUmVwb1N1YnNjcmlwdGlvbhJCCgNnaXQYASABKAsyNS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuR2l0U3Vic2NyaXB0aW9uEkYKBWltYWdlGAIgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkltYWdlU3Vic2NyaXB0aW9uEkYKBWNoYXJ0GAMgASgLMjcuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkNoYXJ0U3Vic2NyaXB0aW9uIs0BCgVTdGFnZRJCCghtZXRhZGF0YRgBIAEoCzIwLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5PYmplY3RNZXRhEj0KBHNwZWMYAiABKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuU3RhZ2VTcGVjEkEKBnN0YXR1cxgDIAEoCzIxLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5TdGFnZVN0YXR1cyKJAQoJU3RhZ2VMaXN0EkAKCG1ldGFkYXRhGAEgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkxpc3RNZXRhEjoKBWl0ZW1zGAIgAygLMisuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlN0YWdlItACCglTdGFnZVNwZWMSDQoFc2hhcmQYBCABKAkSRgoEdmFycxgHIAMoCzI4LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5FeHByZXNzaW9uVmFyaWFibGUSTgoQcmVxdWVzdGVkRnJlaWdodBgFIAMoCzI0LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5GcmVpZ2h0UmVxdWVzdBJSChFwcm9tb3Rpb25UZW1wbGF0ZRgGIAEoCzI3LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5Qcm9tb3Rpb25UZW1wbGF0ZRJICgx2ZXJpZmljYXRpb24YAyABKAsyMi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuVmVyaWZpY2F0aW9uIl4KClN0YWdlU3RhdHMSDQoFY291bnQYAiABKAMSQQoGaGVhbHRoGAEgASgLMjEuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkhlYWx0aFN0YXRzIvYDCgtTdGFnZVN0YXR1cxJDCgpjb25kaXRpb25zGA0gAygLMi8uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkNvbmRpdGlvbhIaChJsYXN0SGFuZGxlZFJlZnJlc2gYCyABKAkSDQoFcGhhc2UYASABKAkSTwoOZnJlaWdodEhpc3RvcnkYBCADKAsyNy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRnJlaWdodENvbGxlY3Rpb24SFgoOZnJlaWdodFN1bW1hcnkYDCABKAkSPAoGaGVhbHRoGAggASgLMiwuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkhlYWx0aBIPCgdtZXNzYWdlGAkgASgJEhoKEm9ic2VydmVkR2VuZXJhdGlvbhgGIAEoAxJSChBjdXJyZW50UHJvbW90aW9uGAcgASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblJlZmVyZW5jZRJPCg1sYXN0UHJvbW90aW9uGAogASgLMjguZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlByb21vdGlvblJlZmVyZW5jZSLaAQoVU3RlcEV4ZWN1dGlvbk1ldGFkYXRhEg0KBWFsaWFzGAEgASgJEj0KCXN0YXJ0ZWRBdBgCIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEj4KCmZpbmlzaGVkQXQYAyABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRISCgplcnJvckNvdW50GAQgASgNEg4KBnN0YXR1cxgFIAEoCRIPCgdtZXNzYWdlGAYgASgJIosCCgxWZXJpZmljYXRpb24SWgoRYW5hbHlzaXNUZW1wbGF0ZXMYASADKAsyPy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNUZW1wbGF0ZVJlZmVyZW5jZRJWChNhbmFseXNpc1J1bk1ldGFkYXRhGAIgASgLMjkuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkFuYWx5c2lzUnVuTWV0YWRhdGESRwoEYXJncxgDIAMoCzI5LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5BbmFseXNpc1J1bkFyZ3VtZW50Ip0CChBWZXJpZmljYXRpb25JbmZvEgoKAmlkGAQgASgJEg0KBWFjdG9yGAcgASgJEj0KCXN0YXJ0VGltZRgFIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lEg0KBXBoYXNlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSTwoLYW5hbHlzaXNSdW4YAyABKAsyOi5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuQW5hbHlzaXNSdW5SZWZlcmVuY2USPgoKZmluaXNoVGltZRgGIAEoCzIqLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5UaW1lIpQBCg1WZXJpZmllZFN0YWdlEj4KCnZlcmlmaWVkQXQYASABKAsyKi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuVGltZRJDCgtsb25nZXN0U29haxgCIAEoCzIuLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5EdXJhdGlvbiLZAQoJV2FyZWhvdXNlEkIKCG1ldGFkYXRhGAEgASgLMjAuazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLk9iamVjdE1ldGESQQoEc3BlYxgCIAEoCzIzLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTcGVjEkUKBnN0YXR1cxgDIAEoCzI1LmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMS5XYXJlaG91c2VTdGF0dXMikQEKDVdhcmVob3VzZUxpc3QSQAoIbWV0YWRhdGEYASABKAsyLi5rOHMuaW8uYXBpbWFjaGluZXJ5LnBrZy5hcGlzLm1ldGEudjEuTGlzdE1ldGESPgoFaXRlbXMYAiADKAsyLy5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuV2FyZWhvdXNlIs4BCg1XYXJlaG91c2VTcGVjEg0KBXNoYXJkGAIgASgJEkAKCGludGVydmFsGAQgASgLMi4uazhzLmlvLmFwaW1hY2hpbmVyeS5wa2cuYXBpcy5tZXRhLnYxLkR1cmF0aW9uEh0KFWZyZWlnaHRDcmVhdGlvblBvbGljeRgDIAEoCRJNCg1zdWJzY3JpcHRpb25zGAEgAygLMjYuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLlJlcG9TdWJzY3JpcHRpb24iYgoOV2FyZWhvdXNlU3RhdHMSDQoFY291bnQYAiABKAMSQQoGaGVhbHRoGAEgASgLMjEuZ2l0aHViLmNvbS5ha3VpdHkua2FyZ28uYXBpLnYxYWxwaGExLkhlYWx0aFN0YXRzIv0BCg9XYXJlaG91c2VTdGF0dXMSQwoKY29uZGl0aW9ucxgJIAMoCzIvLms4cy5pby5hcGltYWNoaW5lcnkucGtnLmFwaXMubWV0YS52MS5Db25kaXRpb24SGgoSbGFzdEhhbmRsZWRSZWZyZXNoGAYgASgJEhoKEm9ic2VydmVkR2VuZXJhdGlvbhgEIAEoAxIVCg1sYXN0RnJlaWdodElEGAggASgJElYKE2Rpc2NvdmVyZWRBcnRpZmFjdHMYByABKAsyOS5naXRodWIuY29tLmFrdWl0eS5rYXJnby5hcGkudjFhbHBoYTEuRGlzY292ZXJlZEFydGlmYWN0c0KXAgooY29tLmdpdGh1Yi5jb20uYWt1aXR5LmthcmdvLmFwaS52MWFscGhhMUIOR2VuZXJhdGVkUHJvdG9QAVokZ2l0aHViLmNvbS9ha3VpdHkva2FyZ28vYXBpL3YxYWxwaGExogIFR0NBS0GqAiRHaXRodWIuQ29tLkFrdWl0eS5LYXJnby5BcGkuVjFhbHBoYTHKAiRHaXRodWJcQ29tXEFrdWl0eVxLYXJnb1xBcGlcVjFhbHBoYTHiAjBHaXRodWJcQ29tXEFrdWl0eVxLYXJnb1xBcGlcVjFhbHBoYTFcR1BCTWV0YWRhdGHqAilHaXRodWI6OkNvbTo6QWt1aXR5OjpLYXJnbzo6QXBpOjpWMWFscGhhMQ", [file_k8s_io_apiextensions_apiserver_pkg_apis_apiextensions_v1_generated, file_k8s_io_apimachinery_pkg_apis_meta_v1_generated, file_k8s_io_apimachinery_pkg_runtime_generated, file_k8s_io_apimachinery_pkg_runtime_schema_generated]);
 
 /**
  * AnalysisRunArgument represents an argument to be added to an AnalysisRun.
@@ -1435,6 +1435,28 @@ export type HealthCheckStep = Message<"github.com.akuity.kargo.api.v1alpha1.Heal
 export const HealthCheckStepSchema: GenMessage<HealthCheckStep> = /*@__PURE__*/
   messageDesc(file_api_v1alpha1_generated, 30);
 
+/**
+ * HealthStats contains a summary of the collective health of a some resource
+ * type.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.HealthStats
+ */
+export type HealthStats = Message<"github.com.akuity.kargo.api.v1alpha1.HealthStats"> & {
+  /**
+   * Healthy contains the number of resources that are explicitly healthy.
+   *
+   * @generated from field: optional int64 healthy = 1;
+   */
+  healthy: bigint;
+};
+
+/**
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.HealthStats.
+ * Use `create(HealthStatsSchema)` to create a new message.
+ */
+export const HealthStatsSchema: GenMessage<HealthStats> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 31);
+
 /**
  * Image describes a specific version of a container image.
  *
@@ -1488,7 +1510,7 @@ export type Image = Message<"github.com.akuity.kargo.api.v1alpha1.Image"> & {
  * Use `create(ImageSchema)` to create a new message.
  */
 export const ImageSchema: GenMessage<Image> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 31);
+  messageDesc(file_api_v1alpha1_generated, 32);
 
 /**
  * ImageDiscoveryResult represents the result of an image discovery operation
@@ -1534,7 +1556,7 @@ export type ImageDiscoveryResult = Message<"github.com.akuity.kargo.api.v1alpha1
  * Use `create(ImageDiscoveryResultSchema)` to create a new message.
  */
 export const ImageDiscoveryResultSchema: GenMessage<ImageDiscoveryResult> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 32);
+  messageDesc(file_api_v1alpha1_generated, 33);
 
 /**
  * ImageSubscription defines a subscription to an image repository.
@@ -1683,7 +1705,7 @@ export type ImageSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.Im
  * Use `create(ImageSubscriptionSchema)` to create a new message.
  */
 export const ImageSubscriptionSchema: GenMessage<ImageSubscription> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 33);
+  messageDesc(file_api_v1alpha1_generated, 34);
 
 /**
  * Project is a resource type that reconciles to a specially labeled namespace
@@ -1721,7 +1743,7 @@ export type Project = Message<"github.com.akuity.kargo.api.v1alpha1.Project"> &
  * Use `create(ProjectSchema)` to create a new message.
  */
 export const ProjectSchema: GenMessage<Project> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 34);
+  messageDesc(file_api_v1alpha1_generated, 35);
 
 /**
  * ProjectConfig is a resource type that describes the configuration of a
@@ -1748,7 +1770,7 @@ export type ProjectConfig = Message<"github.com.akuity.kargo.api.v1alpha1.Projec
  * Use `create(ProjectConfigSchema)` to create a new message.
  */
 export const ProjectConfigSchema: GenMessage<ProjectConfig> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 35);
+  messageDesc(file_api_v1alpha1_generated, 36);
 
 /**
  * ProjectConfigList is a list of ProjectConfig resources.
@@ -1772,7 +1794,7 @@ export type ProjectConfigList = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(ProjectConfigListSchema)` to create a new message.
  */
 export const ProjectConfigListSchema: GenMessage<ProjectConfigList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 36);
+  messageDesc(file_api_v1alpha1_generated, 37);
 
 /**
  * ProjectSpec is a deprecated alias for ProjectConfigSpec. It is retained for
@@ -1797,7 +1819,7 @@ export type ProjectConfigSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(ProjectConfigSpecSchema)` to create a new message.
  */
 export const ProjectConfigSpecSchema: GenMessage<ProjectConfigSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 37);
+  messageDesc(file_api_v1alpha1_generated, 38);
 
 /**
  * ProjectList is a list of Project resources.
@@ -1821,7 +1843,37 @@ export type ProjectList = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectL
  * Use `create(ProjectListSchema)` to create a new message.
  */
 export const ProjectListSchema: GenMessage<ProjectList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 38);
+  messageDesc(file_api_v1alpha1_generated, 39);
+
+/**
+ * ProjectStats contains a summary of the of the collective state of a
+ * Project's constituent resources.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.ProjectStats
+ */
+export type ProjectStats = Message<"github.com.akuity.kargo.api.v1alpha1.ProjectStats"> & {
+  /**
+   * Warehouses contains a summary of the collective state of the Project's
+   * Warehouses.
+   *
+   * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.WarehouseStats warehouses = 1;
+   */
+  warehouses?: WarehouseStats;
+
+  /**
+   * Stages contains a summary of the collective state of the Project's Stages.
+   *
+   * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.StageStats stages = 2;
+   */
+  stages?: StageStats;
+};
+
+/**
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.ProjectStats.
+ * Use `create(ProjectStatsSchema)` to create a new message.
+ */
+export const ProjectStatsSchema: GenMessage<ProjectStats> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 40);
 
 /**
  * ProjectStatus describes a Project's current status.
@@ -1860,6 +1912,14 @@ export type ProjectStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Projec
    * @generated from field: optional string message = 2;
    */
   message: string;
+
+  /**
+   * Stats contains a summary of the of the collective state of a Project's
+   * constituent resources.
+   *
+   * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.ProjectStats stats = 4;
+   */
+  stats?: ProjectStats;
 };
 
 /**
@@ -1867,7 +1927,7 @@ export type ProjectStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Projec
  * Use `create(ProjectStatusSchema)` to create a new message.
  */
 export const ProjectStatusSchema: GenMessage<ProjectStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 39);
+  messageDesc(file_api_v1alpha1_generated, 41);
 
 /**
  * Promotion represents a request to transition a particular Stage into a
@@ -1905,7 +1965,7 @@ export type Promotion = Message<"github.com.akuity.kargo.api.v1alpha1.Promotion"
  * Use `create(PromotionSchema)` to create a new message.
  */
 export const PromotionSchema: GenMessage<Promotion> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 40);
+  messageDesc(file_api_v1alpha1_generated, 42);
 
 /**
  * PromotionList contains a list of Promotion
@@ -1929,7 +1989,7 @@ export type PromotionList = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionListSchema)` to create a new message.
  */
 export const PromotionListSchema: GenMessage<PromotionList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 41);
+  messageDesc(file_api_v1alpha1_generated, 43);
 
 /**
  * PromotionPolicy defines policies governing the promotion of Freight to a
@@ -1965,7 +2025,7 @@ export type PromotionPolicy = Message<"github.com.akuity.kargo.api.v1alpha1.Prom
  * Use `create(PromotionPolicySchema)` to create a new message.
  */
 export const PromotionPolicySchema: GenMessage<PromotionPolicy> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 42);
+  messageDesc(file_api_v1alpha1_generated, 44);
 
 /**
  * PromotionReference contains the relevant information about a Promotion
@@ -2008,7 +2068,7 @@ export type PromotionReference = Message<"github.com.akuity.kargo.api.v1alpha1.P
  * Use `create(PromotionReferenceSchema)` to create a new message.
  */
 export const PromotionReferenceSchema: GenMessage<PromotionReference> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 43);
+  messageDesc(file_api_v1alpha1_generated, 45);
 
 /**
  * PromotionSpec describes the desired transition of a specific Stage into a
@@ -2071,7 +2131,7 @@ export type PromotionSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionSpecSchema)` to create a new message.
  */
 export const PromotionSpecSchema: GenMessage<PromotionSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 44);
+  messageDesc(file_api_v1alpha1_generated, 46);
 
 /**
  * PromotionStatus describes the current state of the transition represented by
@@ -2169,7 +2229,7 @@ export type PromotionStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Prom
  * Use `create(PromotionStatusSchema)` to create a new message.
  */
 export const PromotionStatusSchema: GenMessage<PromotionStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 45);
+  messageDesc(file_api_v1alpha1_generated, 47);
 
 /**
  * PromotionStep describes a directive to be executed as part of a Promotion.
@@ -2244,7 +2304,7 @@ export type PromotionStep = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionStepSchema)` to create a new message.
  */
 export const PromotionStepSchema: GenMessage<PromotionStep> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 46);
+  messageDesc(file_api_v1alpha1_generated, 48);
 
 /**
  * PromotionStepRetry describes the retry policy for a PromotionStep.
@@ -2303,7 +2363,7 @@ export type PromotionStepRetry = Message<"github.com.akuity.kargo.api.v1alpha1.P
  * Use `create(PromotionStepRetrySchema)` to create a new message.
  */
 export const PromotionStepRetrySchema: GenMessage<PromotionStepRetry> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 47);
+  messageDesc(file_api_v1alpha1_generated, 49);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionTask
@@ -2330,7 +2390,7 @@ export type PromotionTask = Message<"github.com.akuity.kargo.api.v1alpha1.Promot
  * Use `create(PromotionTaskSchema)` to create a new message.
  */
 export const PromotionTaskSchema: GenMessage<PromotionTask> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 48);
+  messageDesc(file_api_v1alpha1_generated, 50);
 
 /**
  * PromotionTaskList contains a list of PromotionTasks.
@@ -2354,7 +2414,7 @@ export type PromotionTaskList = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(PromotionTaskListSchema)` to create a new message.
  */
 export const PromotionTaskListSchema: GenMessage<PromotionTaskList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 49);
+  messageDesc(file_api_v1alpha1_generated, 51);
 
 /**
  * PromotionTaskReference describes a reference to a PromotionTask.
@@ -2391,7 +2451,7 @@ export type PromotionTaskReference = Message<"github.com.akuity.kargo.api.v1alph
  * Use `create(PromotionTaskReferenceSchema)` to create a new message.
  */
 export const PromotionTaskReferenceSchema: GenMessage<PromotionTaskReference> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 50);
+  messageDesc(file_api_v1alpha1_generated, 52);
 
 /**
  * @generated from message github.com.akuity.kargo.api.v1alpha1.PromotionTaskSpec
@@ -2425,7 +2485,7 @@ export type PromotionTaskSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(PromotionTaskSpecSchema)` to create a new message.
  */
 export const PromotionTaskSpecSchema: GenMessage<PromotionTaskSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 51);
+  messageDesc(file_api_v1alpha1_generated, 53);
 
 /**
  * PromotionTemplate defines a template for a Promotion that can be used to
@@ -2445,7 +2505,7 @@ export type PromotionTemplate = Message<"github.com.akuity.kargo.api.v1alpha1.Pr
  * Use `create(PromotionTemplateSchema)` to create a new message.
  */
 export const PromotionTemplateSchema: GenMessage<PromotionTemplate> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 52);
+  messageDesc(file_api_v1alpha1_generated, 54);
 
 /**
  * PromotionTemplateSpec describes the (partial) specification of a Promotion
@@ -2481,7 +2541,7 @@ export type PromotionTemplateSpec = Message<"github.com.akuity.kargo.api.v1alpha
  * Use `create(PromotionTemplateSpecSchema)` to create a new message.
  */
 export const PromotionTemplateSpecSchema: GenMessage<PromotionTemplateSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 53);
+  messageDesc(file_api_v1alpha1_generated, 55);
 
 /**
  * RepoSubscription describes a subscription to ONE OF a Git repository, a
@@ -2517,7 +2577,7 @@ export type RepoSubscription = Message<"github.com.akuity.kargo.api.v1alpha1.Rep
  * Use `create(RepoSubscriptionSchema)` to create a new message.
  */
 export const RepoSubscriptionSchema: GenMessage<RepoSubscription> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 54);
+  messageDesc(file_api_v1alpha1_generated, 56);
 
 /**
  * Stage is the Kargo API's main type.
@@ -2553,7 +2613,7 @@ export type Stage = Message<"github.com.akuity.kargo.api.v1alpha1.Stage"> & {
  * Use `create(StageSchema)` to create a new message.
  */
 export const StageSchema: GenMessage<Stage> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 55);
+  messageDesc(file_api_v1alpha1_generated, 57);
 
 /**
  * StageList is a list of Stage resources.
@@ -2577,7 +2637,7 @@ export type StageList = Message<"github.com.akuity.kargo.api.v1alpha1.StageList"
  * Use `create(StageListSchema)` to create a new message.
  */
 export const StageListSchema: GenMessage<StageList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 56);
+  messageDesc(file_api_v1alpha1_generated, 58);
 
 /**
  * StageSpec describes the sources of Freight used by a Stage and how to
@@ -2644,7 +2704,36 @@ export type StageSpec = Message<"github.com.akuity.kargo.api.v1alpha1.StageSpec"
  * Use `create(StageSpecSchema)` to create a new message.
  */
 export const StageSpecSchema: GenMessage<StageSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 57);
+  messageDesc(file_api_v1alpha1_generated, 59);
+
+/**
+ * StageStats contains a summary of the collective state of the a Project's
+ * Stages.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.StageStats
+ */
+export type StageStats = Message<"github.com.akuity.kargo.api.v1alpha1.StageStats"> & {
+  /**
+   * Count contains the total number of Stages in the Project.
+   *
+   * @generated from field: optional int64 count = 2;
+   */
+  count: bigint;
+
+  /**
+   * Health contains a summary of the collective health of a Project's Stages.
+   *
+   * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.HealthStats health = 1;
+   */
+  health?: HealthStats;
+};
+
+/**
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.StageStats.
+ * Use `create(StageStatsSchema)` to create a new message.
+ */
+export const StageStatsSchema: GenMessage<StageStats> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 60);
 
 /**
  * StageStatus describes a Stages's current and recent Freight, health, and
@@ -2751,7 +2840,7 @@ export type StageStatus = Message<"github.com.akuity.kargo.api.v1alpha1.StageSta
  * Use `create(StageStatusSchema)` to create a new message.
  */
 export const StageStatusSchema: GenMessage<StageStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 58);
+  messageDesc(file_api_v1alpha1_generated, 61);
 
 /**
  * StepExecutionMetadata tracks metadata pertaining to the execution of
@@ -2810,7 +2899,7 @@ export type StepExecutionMetadata = Message<"github.com.akuity.kargo.api.v1alpha
  * Use `create(StepExecutionMetadataSchema)` to create a new message.
  */
 export const StepExecutionMetadataSchema: GenMessage<StepExecutionMetadata> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 59);
+  messageDesc(file_api_v1alpha1_generated, 62);
 
 /**
  * Verification describes how to verify that a Promotion has been successful
@@ -2849,7 +2938,7 @@ export type Verification = Message<"github.com.akuity.kargo.api.v1alpha1.Verific
  * Use `create(VerificationSchema)` to create a new message.
  */
 export const VerificationSchema: GenMessage<Verification> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 60);
+  messageDesc(file_api_v1alpha1_generated, 63);
 
 /**
  * VerificationInfo contains the details of an instance of a Verification
@@ -2919,7 +3008,7 @@ export type VerificationInfo = Message<"github.com.akuity.kargo.api.v1alpha1.Ver
  * Use `create(VerificationInfoSchema)` to create a new message.
  */
 export const VerificationInfoSchema: GenMessage<VerificationInfo> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 61);
+  messageDesc(file_api_v1alpha1_generated, 64);
 
 /**
  * VerifiedStage describes a Stage in which Freight has been verified.
@@ -2951,7 +3040,7 @@ export type VerifiedStage = Message<"github.com.akuity.kargo.api.v1alpha1.Verifi
  * Use `create(VerifiedStageSchema)` to create a new message.
  */
 export const VerifiedStageSchema: GenMessage<VerifiedStage> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 62);
+  messageDesc(file_api_v1alpha1_generated, 65);
 
 /**
  * Warehouse is a source of Freight.
@@ -2986,7 +3075,7 @@ export type Warehouse = Message<"github.com.akuity.kargo.api.v1alpha1.Warehouse"
  * Use `create(WarehouseSchema)` to create a new message.
  */
 export const WarehouseSchema: GenMessage<Warehouse> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 63);
+  messageDesc(file_api_v1alpha1_generated, 66);
 
 /**
  * WarehouseList is a list of Warehouse resources.
@@ -3010,7 +3099,7 @@ export type WarehouseList = Message<"github.com.akuity.kargo.api.v1alpha1.Wareho
  * Use `create(WarehouseListSchema)` to create a new message.
  */
 export const WarehouseListSchema: GenMessage<WarehouseList> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 64);
+  messageDesc(file_api_v1alpha1_generated, 67);
 
 /**
  * WarehouseSpec describes sources of versioned artifacts to be included in
@@ -3076,7 +3165,37 @@ export type WarehouseSpec = Message<"github.com.akuity.kargo.api.v1alpha1.Wareho
  * Use `create(WarehouseSpecSchema)` to create a new message.
  */
 export const WarehouseSpecSchema: GenMessage<WarehouseSpec> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 65);
+  messageDesc(file_api_v1alpha1_generated, 68);
+
+/**
+ * WarehouseStats contains a summary of the collective state of the a Project's
+ * Warehouses.
+ *
+ * @generated from message github.com.akuity.kargo.api.v1alpha1.WarehouseStats
+ */
+export type WarehouseStats = Message<"github.com.akuity.kargo.api.v1alpha1.WarehouseStats"> & {
+  /**
+   * Count contains the total number of Warehouses in the Project.
+   *
+   * @generated from field: optional int64 count = 2;
+   */
+  count: bigint;
+
+  /**
+   * Health contains a summary of the collective health of a Project's
+   * Warehouses.
+   *
+   * @generated from field: optional github.com.akuity.kargo.api.v1alpha1.HealthStats health = 1;
+   */
+  health?: HealthStats;
+};
+
+/**
+ * Describes the message github.com.akuity.kargo.api.v1alpha1.WarehouseStats.
+ * Use `create(WarehouseStatsSchema)` to create a new message.
+ */
+export const WarehouseStatsSchema: GenMessage<WarehouseStats> = /*@__PURE__*/
+  messageDesc(file_api_v1alpha1_generated, 69);
 
 /**
  * WarehouseStatus describes a Warehouse's most recently observed state.
@@ -3135,5 +3254,5 @@ export type WarehouseStatus = Message<"github.com.akuity.kargo.api.v1alpha1.Ware
  * Use `create(WarehouseStatusSchema)` to create a new message.
  */
 export const WarehouseStatusSchema: GenMessage<WarehouseStatus> = /*@__PURE__*/
-  messageDesc(file_api_v1alpha1_generated, 66);
+  messageDesc(file_api_v1alpha1_generated, 70);
 
diff --git a/ui/src/gen/directives/git-open-pr-config.json b/ui/src/gen/directives/git-open-pr-config.json
index fbafc918..772272c7 100644
--- a/ui/src/gen/directives/git-open-pr-config.json
+++ b/ui/src/gen/directives/git-open-pr-config.json
@@ -14,12 +14,13 @@
   },
   "provider": {
    "type": "string",
-   "description": "The name of the Git provider to use. Currently only 'github', 'gitlab', 'gitea' and 'azure' are supported. Kargo will try to infer the provider if it is not explicitly specified.",
+   "description": "The name of the Git provider to use. Currently 'azure', 'bitbucket', 'gitea', 'github', and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly specified.",
    "enum": [
-    "github",
-    "gitlab",
     "azure",
-    "gitea"
+    "bitbucket",
+    "gitea",
+    "github",
+    "gitlab"
    ]
   },
   "repoURL": {
diff --git a/ui/src/gen/directives/git-wait-for-pr-config.json b/ui/src/gen/directives/git-wait-for-pr-config.json
index 44f1bc54..bbdeabcc 100644
--- a/ui/src/gen/directives/git-wait-for-pr-config.json
+++ b/ui/src/gen/directives/git-wait-for-pr-config.json
@@ -10,12 +10,13 @@
   },
   "provider": {
    "type": "string",
-   "description": "The name of the Git provider to use. Currently only 'github', 'gitlab', 'gitea' and 'azure' are supported. Kargo will try to infer the provider if it is not explicitly specified.",
+   "description": "The name of the Git provider to use. Currently 'azure', 'bitbucket', 'gitea', 'github', and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly specified.",
    "enum": [
-    "github",
-    "gitlab",
     "azure",
-    "gitea"
+    "bitbucket",
+    "gitea",
+    "github",
+    "gitlab"
    ]
   },
   "prNumber": {
diff --git a/ui/src/gen/schema/projects.kargo.akuity.io_v1alpha1.json b/ui/src/gen/schema/projects.kargo.akuity.io_v1alpha1.json
index 8d6120c8..9974a8d4 100644
--- a/ui/src/gen/schema/projects.kargo.akuity.io_v1alpha1.json
+++ b/ui/src/gen/schema/projects.kargo.akuity.io_v1alpha1.json
@@ -111,6 +111,64 @@
         "phase": {
           "description": "Phase describes the Project's current phase.\n\nDeprecated: Use the Conditions field instead.",
           "type": "string"
+        },
+        "stats": {
+          "description": "Stats contains a summary of the of the collective state of a Project's\nconstituent resources.",
+          "properties": {
+            "stages": {
+              "description": "Stages contains a summary of the collective state of the Project's Stages.",
+              "properties": {
+                "count": {
+                  "description": "Count contains the total number of Stages in the Project.",
+                  "format": "int64",
+                  "maximum": 9223372036854776000,
+                  "minimum": -9223372036854776000,
+                  "type": "integer"
+                },
+                "health": {
+                  "description": "Health contains a summary of the collective health of a Project's Stages.",
+                  "properties": {
+                    "healthy": {
+                      "description": "Healthy contains the number of resources that are explicitly healthy.",
+                      "format": "int64",
+                      "maximum": 9223372036854776000,
+                      "minimum": -9223372036854776000,
+                      "type": "integer"
+                    }
+                  },
+                  "type": "object"
+                }
+              },
+              "type": "object"
+            },
+            "warehouses": {
+              "description": "Warehouses contains a summary of the collective state of the Project's\nWarehouses.",
+              "properties": {
+                "count": {
+                  "description": "Count contains the total number of Warehouses in the Project.",
+                  "format": "int64",
+                  "maximum": 9223372036854776000,
+                  "minimum": -9223372036854776000,
+                  "type": "integer"
+                },
+                "health": {
+                  "description": "Health contains a summary of the collective health of a Project's\nWarehouses.",
+                  "properties": {
+                    "healthy": {
+                      "description": "Healthy contains the number of resources that are explicitly healthy.",
+                      "format": "int64",
+                      "maximum": 9223372036854776000,
+                      "minimum": -9223372036854776000,
+                      "type": "integer"
+                    }
+                  },
+                  "type": "object"
+                }
+              },
+              "type": "object"
+            }
+          },
+          "type": "object"
         }
       },
       "type": "object"



Test patch:
diff --git a/internal/controller/git/bare_repo_test.go b/internal/controller/git/bare_repo_test.go
index 0d6b3c48..57ea6540 100644
--- a/internal/controller/git/bare_repo_test.go
+++ b/internal/controller/git/bare_repo_test.go
@@ -58,7 +58,7 @@ func TestBareRepo(t *testing.T) {
 	defer setupRep.Close()
 	err = os.WriteFile(fmt.Sprintf("%s/%s", setupRep.Dir(), "test.txt"), []byte("foo"), 0600)
 	require.NoError(t, err)
-	err = setupRep.AddAllAndCommit(fmt.Sprintf("initial commit %s", uuid.NewString()))
+	err = setupRep.AddAllAndCommit(fmt.Sprintf("initial commit %s", uuid.NewString()), nil)
 	require.NoError(t, err)
 	err = setupRep.Push(nil)
 	require.NoError(t, err)
diff --git a/internal/controller/git/repo_test.go b/internal/controller/git/repo_test.go
index 7403b92e..54f3732e 100644
--- a/internal/controller/git/repo_test.go
+++ b/internal/controller/git/repo_test.go
@@ -104,7 +104,7 @@ func TestRepo(t *testing.T) {
 	})
 
 	testCommitMessage := fmt.Sprintf("test commit %s", uuid.NewString())
-	err = rep.AddAllAndCommit(testCommitMessage)
+	err = rep.AddAllAndCommit(testCommitMessage, nil)
 	require.NoError(t, err)
 
 	t.Run("can commit", func(t *testing.T) {
diff --git a/internal/controller/git/work_tree_test.go b/internal/controller/git/work_tree_test.go
index d65ed6c0..ae136844 100644
--- a/internal/controller/git/work_tree_test.go
+++ b/internal/controller/git/work_tree_test.go
@@ -56,7 +56,7 @@ func TestWorkTree(t *testing.T) {
 	defer setupRep.Close()
 	err = os.WriteFile(fmt.Sprintf("%s/%s", setupRep.Dir(), "test.txt"), []byte("foo"), 0600)
 	require.NoError(t, err)
-	err = setupRep.AddAllAndCommit(fmt.Sprintf("initial commit %s", uuid.NewString()))
+	err = setupRep.AddAllAndCommit(fmt.Sprintf("initial commit %s", uuid.NewString()), nil)
 	require.NoError(t, err)
 	err = setupRep.Push(nil)
 	require.NoError(t, err)
diff --git a/internal/controller/management/projects/event_handlers_test.go b/internal/controller/management/projects/event_handlers_test.go
new file mode 100644
index 00000000..a76beed9
--- /dev/null
+++ b/internal/controller/management/projects/event_handlers_test.go
@@ -0,0 +1,197 @@
+package projects
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/util/workqueue"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllertest"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+)
+
+func Test_projectWarehouseHealthEnqueuer_Update(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, kargoapi.AddToScheme(scheme))
+
+	tests := []struct {
+		name             string
+		oldWarehouse     *kargoapi.Warehouse
+		newWarehouse     *kargoapi.Warehouse
+		expectedRequests []reconcile.Request
+	}{
+		{
+			name: "no health condition change",
+			oldWarehouse: &kargoapi.Warehouse{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "fake-project",
+				},
+				Status: kargoapi.WarehouseStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeHealthy,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+			newWarehouse: &kargoapi.Warehouse{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "fake-project",
+				},
+				Status: kargoapi.WarehouseStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeHealthy,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+		},
+		{
+			name: "health condition change",
+			oldWarehouse: &kargoapi.Warehouse{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "fake-project",
+				},
+				Status: kargoapi.WarehouseStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeHealthy,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+			newWarehouse: &kargoapi.Warehouse{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "fake-project",
+				},
+				Status: kargoapi.WarehouseStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeHealthy,
+						Status: metav1.ConditionFalse,
+					}},
+				},
+			},
+			expectedRequests: []reconcile.Request{{
+				NamespacedName: types.NamespacedName{Name: "fake-project"},
+			}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			enqueuer := &projectWarehouseHealthEnqueuer[*kargoapi.Warehouse]{}
+			queue := &controllertest.Queue{TypedInterface: workqueue.NewTyped[reconcile.Request]()}
+
+			enqueuer.Update(
+				context.Background(),
+				event.TypedUpdateEvent[*kargoapi.Warehouse]{
+					ObjectOld: tt.oldWarehouse,
+					ObjectNew: tt.newWarehouse,
+				},
+				queue,
+			)
+
+			var reqs []reconcile.Request
+			for queue.Len() > 0 {
+				req, _ := queue.Get()
+				reqs = append(reqs, req)
+				queue.Done(req)
+			}
+
+			require.ElementsMatch(t, tt.expectedRequests, reqs)
+		})
+	}
+}
+
+func Test_projectStageHealthEnqueuer_Update(t *testing.T) {
+	scheme := runtime.NewScheme()
+	require.NoError(t, kargoapi.AddToScheme(scheme))
+
+	tests := []struct {
+		name             string
+		oldStage         *kargoapi.Stage
+		newStage         *kargoapi.Stage
+		expectedRequests []reconcile.Request
+	}{
+		{
+			name: "no health condition change",
+			oldStage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "fake-project",
+				},
+				Status: kargoapi.StageStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeHealthy,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+			newStage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "fake-project",
+				},
+				Status: kargoapi.StageStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeHealthy,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+		},
+		{
+			name: "health condition change",
+			oldStage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "fake-project",
+				},
+				Status: kargoapi.StageStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeHealthy,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+			newStage: &kargoapi.Stage{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "fake-project",
+				},
+				Status: kargoapi.StageStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeHealthy,
+						Status: metav1.ConditionFalse,
+					}},
+				},
+			},
+			expectedRequests: []reconcile.Request{{
+				NamespacedName: types.NamespacedName{Name: "fake-project"},
+			}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			enqueuer := &projectStageHealthEnqueuer[*kargoapi.Stage]{}
+			queue := &controllertest.Queue{TypedInterface: workqueue.NewTyped[reconcile.Request]()}
+
+			enqueuer.Update(
+				context.Background(),
+				event.TypedUpdateEvent[*kargoapi.Stage]{
+					ObjectOld: tt.oldStage,
+					ObjectNew: tt.newStage,
+				},
+				queue,
+			)
+
+			var reqs []reconcile.Request
+			for queue.Len() > 0 {
+				req, _ := queue.Get()
+				reqs = append(reqs, req)
+				queue.Done(req)
+			}
+
+			require.ElementsMatch(t, tt.expectedRequests, reqs)
+		})
+	}
+}
diff --git a/internal/controller/management/projects/projects_test.go b/internal/controller/management/projects/projects_test.go
index 0b0819c9..ebd92c17 100644
--- a/internal/controller/management/projects/projects_test.go
+++ b/internal/controller/management/projects/projects_test.go
@@ -10,6 +10,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	rbacv1 "k8s.io/api/rbac/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	kubeerr "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -29,9 +30,8 @@ func TestNewReconciler(t *testing.T) {
 	require.Equal(t, testCfg, r.cfg)
 	require.NotNil(t, r.client)
 	require.NotNil(t, r.getProjectFn)
-	require.NotNil(t, r.syncProjectFn)
+	require.NotNil(t, r.reconcileFn)
 	require.NotNil(t, r.ensureNamespaceFn)
-	require.NotNil(t, r.updateProjectFn)
 	require.NotNil(t, r.patchProjectStatusFn)
 	require.NotNil(t, r.getNamespaceFn)
 	require.NotNil(t, r.createNamespaceFn)
@@ -39,14 +39,13 @@ func TestNewReconciler(t *testing.T) {
 	require.NotNil(t, r.ensureFinalizerFn)
 	require.NotNil(t, r.ensureAPIAdminPermissionsFn)
 	require.NotNil(t, r.ensureControllerPermissionsFn)
-	require.NotNil(t, r.ensureDefaultProjectRolesFn)
+	require.NotNil(t, r.ensureDefaultUserRolesFn)
 	require.NotNil(t, r.createServiceAccountFn)
 	require.NotNil(t, r.createRoleFn)
 	require.NotNil(t, r.createRoleBindingFn)
-	require.NotNil(t, r.createProjectConfigFn)
 }
 
-func TestReconcile(t *testing.T) {
+func TestReconciler_Reconcile(t *testing.T) {
 	testCases := []struct {
 		name       string
 		reconciler *reconciler
@@ -116,7 +115,7 @@ func TestReconcile(t *testing.T) {
 			},
 		},
 		{
-			name: "error syncing project",
+			name: "error running internal reconcile",
 			reconciler: &reconciler{
 				getProjectFn: func(
 					context.Context,
@@ -125,11 +124,11 @@ func TestReconcile(t *testing.T) {
 				) (*kargoapi.Project, error) {
 					return &kargoapi.Project{}, nil
 				},
-				syncProjectFn: func(
+				reconcileFn: func(
 					context.Context,
 					*kargoapi.Project,
-				) (kargoapi.ProjectStatus, error) {
-					return kargoapi.ProjectStatus{}, errors.New("something went wrong")
+				) (kargoapi.ProjectStatus, bool, error) {
+					return kargoapi.ProjectStatus{}, false, errors.New("something went wrong")
 				},
 				patchProjectStatusFn: func(
 					_ context.Context,
@@ -153,11 +152,11 @@ func TestReconcile(t *testing.T) {
 				) (*kargoapi.Project, error) {
 					return &kargoapi.Project{}, nil
 				},
-				syncProjectFn: func(
+				reconcileFn: func(
 					context.Context,
 					*kargoapi.Project,
-				) (kargoapi.ProjectStatus, error) {
-					return kargoapi.ProjectStatus{}, nil
+				) (kargoapi.ProjectStatus, bool, error) {
+					return kargoapi.ProjectStatus{}, false, nil
 				},
 				patchProjectStatusFn: func(
 					_ context.Context,
@@ -180,75 +179,353 @@ func TestReconcile(t *testing.T) {
 	}
 }
 
-func TestSyncProject(t *testing.T) {
+func TestReconciler_reconcile(t *testing.T) {
+	const testProject = "fake-project"
+
+	scheme := runtime.NewScheme()
+	require.NoError(t, kargoapi.AddToScheme(scheme))
+
+	tests := []struct {
+		name        string
+		reconciler  *reconciler
+		project     *kargoapi.Project
+		interceptor interceptor.Funcs
+		assertions  func(
+			t *testing.T,
+			status kargoapi.ProjectStatus,
+			requeue bool,
+			cl client.Client,
+			err error,
+		)
+	}{
+		{
+			name:       "success migrating phase to conditions",
+			reconciler: &reconciler{},
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
+				Status: kargoapi.ProjectStatus{
+					Phase: kargoapi.ProjectPhaseInitializing,
+				},
+			},
+			assertions: func(
+				t *testing.T,
+				status kargoapi.ProjectStatus,
+				requeue bool,
+				_ client.Client,
+				err error,
+			) {
+				require.NoError(t, err)
+
+				// Phase should be cleared
+				require.Empty(t, status.Phase) // nolint:staticcheck
+
+				// Conditions should be set
+				reconciling := conditions.Get(&status, kargoapi.ConditionTypeReconciling)
+				require.NotNil(t, reconciling)
+				require.Equal(t, metav1.ConditionTrue, reconciling.Status)
+				require.Equal(t, "Syncing", reconciling.Reason)
+
+				ready := conditions.Get(&status, kargoapi.ConditionTypeReady)
+				require.NotNil(t, ready)
+				require.Equal(t, metav1.ConditionFalse, ready.Status)
+				require.Equal(t, "Syncing", ready.Reason)
+
+				// Immediate requeue should be requested
+				require.True(t, requeue)
+			},
+		},
+		{
+			name:       "error migrating spec to ProjectConfig",
+			reconciler: &reconciler{},
+			// Requires no phase --> conditions migration.
+			// Does require spec --> ProjectConfig migration.
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
+				// Non-nil of spec is enough to trigger migration.
+				Spec: &kargoapi.ProjectSpec{}, // nolint:staticcheck
+			},
+			interceptor: interceptor.Funcs{
+				Update: func(
+					context.Context,
+					client.WithWatch,
+					client.Object,
+					...client.UpdateOption,
+				) error {
+					return fmt.Errorf("something went wrong")
+				},
+			},
+			assertions: func(
+				t *testing.T,
+				status kargoapi.ProjectStatus,
+				requeue bool,
+				_ client.Client,
+				err error,
+			) {
+				require.ErrorContains(t, err, "something went wrong")
+
+				// Doesn't impact conditions
+				require.Len(t, status.Conditions, 0)
+
+				// Immediate requeue should NOT be requested
+				require.False(t, requeue)
+			},
+		},
+		{
+			name:       "success migrating spec to ProjectConfig",
+			reconciler: &reconciler{},
+			// Requires no phase --> conditions migration.
+			// Does require spec --> ProjectConfig migration.
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
+				// Non-nil of spec is enough to trigger migration.
+				Spec: &kargoapi.ProjectSpec{}, // nolint:staticcheck
+			},
+			assertions: func(
+				t *testing.T,
+				status kargoapi.ProjectStatus,
+				requeue bool,
+				cl client.Client,
+				err error,
+			) {
+				require.NoError(t, err)
+
+				// Doesn't impact conditions
+				require.Len(t, status.Conditions, 0)
+
+				// Spec should be cleared
+				project := &kargoapi.Project{}
+				err = cl.Get(context.Background(), types.NamespacedName{Name: testProject}, project)
+				require.NoError(t, err)
+				require.Empty(t, project.Spec) // nolint:staticcheck
+
+				// Immediate requeue should be requested
+				require.True(t, requeue)
+			},
+		},
+		{
+			name: "error syncing project",
+			reconciler: &reconciler{
+				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
+					return fmt.Errorf("something went wrong")
+				},
+			},
+			// Requires no phase --> conditions migration.
+			// Requires no spec --> ProjectConfig migration.
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
+			},
+			assertions: func(
+				t *testing.T,
+				status kargoapi.ProjectStatus,
+				requeue bool,
+				_ client.Client,
+				err error,
+			) {
+				require.ErrorContains(t, err, "something went wrong")
+
+				// Still syncing because retry could succeed
+				require.Len(t, status.Conditions, 2)
+				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
+				require.NotNil(t, readyCondition)
+				require.Equal(t, metav1.ConditionFalse, readyCondition.Status)
+
+				reconcilingCondition := conditions.Get(&status, kargoapi.ConditionTypeReconciling)
+				require.NotNil(t, reconcilingCondition)
+				require.Equal(t, metav1.ConditionTrue, reconcilingCondition.Status)
+				require.Equal(t, "Syncing", reconcilingCondition.Reason)
+
+				// Immediate requeue should NOT be requested
+				require.False(t, requeue)
+			},
+		},
+		{
+			name: "error collecting Project stats",
+			reconciler: &reconciler{
+				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
+					return nil
+				},
+				ensureAPIAdminPermissionsFn: func(context.Context, *kargoapi.Project) error {
+					return nil
+				},
+				ensureDefaultUserRolesFn: func(context.Context, *kargoapi.Project) error {
+					return nil
+				},
+			},
+			// Requires no phase --> conditions migration.
+			// Requires no spec --> ProjectConfig migration.
+			// Is already initialized.
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
+				Status: kargoapi.ProjectStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeReady,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+			interceptor: interceptor.Funcs{
+				// Fail to list Warehouses
+				List: func(
+					context.Context,
+					client.WithWatch,
+					client.ObjectList,
+					...client.ListOption,
+				) error {
+					return fmt.Errorf("something went wrong")
+				},
+			},
+			assertions: func(
+				t *testing.T,
+				status kargoapi.ProjectStatus,
+				requeue bool,
+				_ client.Client,
+				err error,
+			) {
+				require.ErrorContains(t, err, "something went wrong")
+
+				require.Len(t, status.Conditions, 2)
+
+				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
+				require.NotNil(t, readyCondition)
+				require.Equal(t, metav1.ConditionTrue, readyCondition.Status)
+
+				healthCondition := conditions.Get(&status, kargoapi.ConditionTypeHealthy)
+				require.NotNil(t, healthCondition)
+				require.Equal(t, metav1.ConditionFalse, healthCondition.Status)
+
+				// Immediate requeue should NOT be requested
+				require.False(t, requeue)
+			},
+		},
+		{
+			name: "success collecting Project stats",
+			reconciler: &reconciler{
+				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
+					return nil
+				},
+				ensureAPIAdminPermissionsFn: func(context.Context, *kargoapi.Project) error {
+					return nil
+				},
+				ensureDefaultUserRolesFn: func(context.Context, *kargoapi.Project) error {
+					return nil
+				},
+			},
+			// Requires no phase --> conditions migration.
+			// Requires no spec --> ProjectConfig migration.
+			// Is already ready.
+			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
+				Status: kargoapi.ProjectStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeReady,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+			assertions: func(
+				t *testing.T,
+				status kargoapi.ProjectStatus,
+				requeue bool,
+				_ client.Client,
+				err error,
+			) {
+				require.NoError(t, err)
+
+				require.Len(t, status.Conditions, 1)
+
+				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
+				require.NotNil(t, readyCondition)
+				require.Equal(t, metav1.ConditionTrue, readyCondition.Status)
+
+				// Immediate requeue should NOT be requested
+				require.False(t, requeue)
+
+				// Status has stats
+				require.NotNil(t, status.Stats)
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.reconciler.client = fake.NewClientBuilder().
+				WithScheme(scheme).
+				WithObjects(tt.project).
+				WithInterceptorFuncs(tt.interceptor).
+				Build()
+			status, requeue, err := tt.reconciler.reconcile(context.Background(), tt.project)
+			tt.assertions(t, status, requeue, tt.reconciler.client, err)
+		})
+	}
+}
+
+func TestReconciler_syncProject(t *testing.T) {
 	testCases := []struct {
 		name       string
 		reconciler *reconciler
-		assertions func(*testing.T, kargoapi.ProjectStatus, error)
+		project    *kargoapi.Project
+		assertions func(*testing.T, kargoapi.ProjectStatus, error,
+		)
 	}{
 		{
 			name: "error ensuring namespace",
 			reconciler: &reconciler{
-				ensureNamespaceFn: func(
-					_ context.Context,
-					project *kargoapi.Project,
-				) (kargoapi.ProjectStatus, error) {
-					return *project.Status.DeepCopy(), errors.New("something went wrong")
+				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
+					return errors.New("something went wrong")
 				},
 			},
+			project: &kargoapi.Project{},
 			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
 				require.ErrorContains(t, err, "something went wrong")
 
-				// Still initializing because retry could succeed
+				// Still syncing because retry could succeed
 				require.Len(t, status.Conditions, 2)
 
 				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
 				require.NotNil(t, readyCondition)
 				require.Equal(t, metav1.ConditionFalse, readyCondition.Status)
-				require.Equal(t, "NamespaceInitializationFailed", readyCondition.Reason)
+				require.Equal(t, "EnsuringNamespaceFailed", readyCondition.Reason)
 
 				reconcilingCondition := conditions.Get(&status, kargoapi.ConditionTypeReconciling)
 				require.NotNil(t, reconcilingCondition)
 				require.Equal(t, metav1.ConditionTrue, reconcilingCondition.Status)
-				require.Equal(t, "Initializing", reconcilingCondition.Reason)
+				require.Equal(t, "Syncing", reconcilingCondition.Reason)
 			},
 		},
 		{
 			name: "fatal error ensuring namespace",
 			reconciler: &reconciler{
-				ensureNamespaceFn: func(
-					_ context.Context,
-					project *kargoapi.Project,
-				) (kargoapi.ProjectStatus, error) {
-					status := *project.Status.DeepCopy()
-					return status, errProjectNamespaceExists
+				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
+					return errProjectNamespaceExists
 				},
 			},
+			project: &kargoapi.Project{},
 			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
 				require.True(t, errors.Is(err, errProjectNamespaceExists))
 
-				// Failed because retry cannot possibly succeed
-				require.Len(t, status.Conditions, 2)
+				// Still syncing because retry could succeed
+				require.Len(t, status.Conditions, 3)
 
 				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
 				require.NotNil(t, readyCondition)
 				require.Equal(t, metav1.ConditionFalse, readyCondition.Status)
-				require.Equal(t, "NamespaceInitializationFailed", readyCondition.Reason)
+				require.Equal(t, "EnsuringNamespaceFailed", readyCondition.Reason)
 
 				stalledCondition := conditions.Get(&status, kargoapi.ConditionTypeStalled)
 				require.NotNil(t, stalledCondition)
 				require.Equal(t, metav1.ConditionTrue, stalledCondition.Status)
 				require.Equal(t, "ExistingNamespaceMissingLabel", stalledCondition.Reason)
+
+				reconcilingCondition := conditions.Get(&status, kargoapi.ConditionTypeReconciling)
+				require.NotNil(t, reconcilingCondition)
+				require.Equal(t, metav1.ConditionTrue, reconcilingCondition.Status)
+				require.Equal(t, "Syncing", reconcilingCondition.Reason)
 			},
 		},
 		{
-			name: "error ensuring project admin permissions",
+			name: "error ensuring api server permissions",
 			reconciler: &reconciler{
-				ensureNamespaceFn: func(
-					_ context.Context,
-					project *kargoapi.Project,
-				) (kargoapi.ProjectStatus, error) {
-					return *project.Status.DeepCopy(), nil
+				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
+					return nil
 				},
 				ensureAPIAdminPermissionsFn: func(
 					context.Context,
@@ -257,21 +534,22 @@ func TestSyncProject(t *testing.T) {
 					return errors.New("something went wrong")
 				},
 			},
+			project: &kargoapi.Project{},
 			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
 				require.ErrorContains(t, err, "something went wrong")
 
-				// Still initializing because retry could succeed
+				// Still syncing because retry could succeed
 				require.Len(t, status.Conditions, 2)
 
 				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
 				require.NotNil(t, readyCondition)
 				require.Equal(t, metav1.ConditionFalse, readyCondition.Status)
-				require.Equal(t, "PermissionsInitializationFailed", readyCondition.Reason)
+				require.Equal(t, "EnsuringAPIServerPermissionsFailed", readyCondition.Reason)
 
 				reconcilingCondition := conditions.Get(&status, kargoapi.ConditionTypeReconciling)
 				require.NotNil(t, reconcilingCondition)
 				require.Equal(t, metav1.ConditionTrue, reconcilingCondition.Status)
-				require.Equal(t, "Initializing", reconcilingCondition.Reason)
+				require.Equal(t, "Syncing", reconcilingCondition.Reason)
 			},
 		},
 		{
@@ -280,11 +558,8 @@ func TestSyncProject(t *testing.T) {
 				cfg: ReconcilerConfig{
 					ManageControllerRoleBindings: true,
 				},
-				ensureNamespaceFn: func(
-					_ context.Context,
-					project *kargoapi.Project,
-				) (kargoapi.ProjectStatus, error) {
-					return *project.Status.DeepCopy(), nil
+				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
+					return nil
 				},
 				ensureAPIAdminPermissionsFn: func(
 					context.Context,
@@ -299,18 +574,29 @@ func TestSyncProject(t *testing.T) {
 					return errors.New("something went wrong")
 				},
 			},
-			assertions: func(t *testing.T, _ kargoapi.ProjectStatus, err error) {
+			project: &kargoapi.Project{},
+			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
 				require.ErrorContains(t, err, "something went wrong")
+
+				// Still syncing because retry could succeed
+				require.Len(t, status.Conditions, 2)
+
+				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
+				require.NotNil(t, readyCondition)
+				require.Equal(t, metav1.ConditionFalse, readyCondition.Status)
+				require.Equal(t, "EnsuringControllerPermissionsFailed", readyCondition.Reason)
+
+				reconcilingCondition := conditions.Get(&status, kargoapi.ConditionTypeReconciling)
+				require.NotNil(t, reconcilingCondition)
+				require.Equal(t, metav1.ConditionTrue, reconcilingCondition.Status)
+				require.Equal(t, "Syncing", reconcilingCondition.Reason)
 			},
 		},
 		{
-			name: "error ensuring default project roles",
+			name: "error ensuring default user roles",
 			reconciler: &reconciler{
-				ensureNamespaceFn: func(
-					_ context.Context,
-					project *kargoapi.Project,
-				) (kargoapi.ProjectStatus, error) {
-					return *project.Status.DeepCopy(), nil
+				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
+					return nil
 				},
 				ensureAPIAdminPermissionsFn: func(
 					context.Context,
@@ -318,38 +604,36 @@ func TestSyncProject(t *testing.T) {
 				) error {
 					return nil
 				},
-				ensureDefaultProjectRolesFn: func(
+				ensureDefaultUserRolesFn: func(
 					context.Context,
 					*kargoapi.Project,
 				) error {
 					return errors.New("something went wrong")
 				},
 			},
+			project: &kargoapi.Project{},
 			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
 				require.ErrorContains(t, err, "something went wrong")
 
-				// Still initializing because retry could succeed
+				// Still syncing because retry could succeed
 				require.Len(t, status.Conditions, 2)
 
 				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
 				require.NotNil(t, readyCondition)
 				require.Equal(t, metav1.ConditionFalse, readyCondition.Status)
-				require.Equal(t, "RolesInitializationFailed", readyCondition.Reason)
+				require.Equal(t, "EnsuringDefaultUserRoles", readyCondition.Reason)
 
 				reconcilingCondition := conditions.Get(&status, kargoapi.ConditionTypeReconciling)
 				require.NotNil(t, reconcilingCondition)
 				require.Equal(t, metav1.ConditionTrue, reconcilingCondition.Status)
-				require.Equal(t, "Initializing", reconcilingCondition.Reason)
+				require.Equal(t, "Syncing", reconcilingCondition.Reason)
 			},
 		},
 		{
 			name: "success",
 			reconciler: &reconciler{
-				ensureNamespaceFn: func(
-					_ context.Context,
-					project *kargoapi.Project,
-				) (kargoapi.ProjectStatus, error) {
-					return *project.Status.DeepCopy(), nil
+				ensureNamespaceFn: func(context.Context, *kargoapi.Project) error {
+					return nil
 				},
 				ensureAPIAdminPermissionsFn: func(
 					context.Context,
@@ -357,13 +641,14 @@ func TestSyncProject(t *testing.T) {
 				) error {
 					return nil
 				},
-				ensureDefaultProjectRolesFn: func(
+				ensureDefaultUserRolesFn: func(
 					context.Context,
 					*kargoapi.Project,
 				) error {
 					return nil
 				},
 			},
+			project: &kargoapi.Project{},
 			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
 				require.NoError(t, err)
 
@@ -372,7 +657,7 @@ func TestSyncProject(t *testing.T) {
 				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
 				require.NotNil(t, readyCondition)
 				require.Equal(t, metav1.ConditionTrue, readyCondition.Status)
-				require.Equal(t, "Initialized", readyCondition.Reason)
+				require.Equal(t, "Synced", readyCondition.Reason)
 			},
 		},
 	}
@@ -380,21 +665,19 @@ func TestSyncProject(t *testing.T) {
 		t.Run(testCase.name, func(t *testing.T) {
 			status, err := testCase.reconciler.syncProject(
 				context.Background(),
-				&kargoapi.Project{
-					Status: kargoapi.ProjectStatus{},
-				},
+				testCase.project,
 			)
 			testCase.assertions(t, status, err)
 		})
 	}
 }
 
-func TestEnsureNamespace(t *testing.T) {
+func TestReconciler_ensureNamespace(t *testing.T) {
 	testCases := []struct {
 		name       string
 		project    *kargoapi.Project
 		reconciler *reconciler
-		assertions func(*testing.T, kargoapi.ProjectStatus, error)
+		assertions func(*testing.T, error)
 	}{
 		{
 			name:    "error getting namespace",
@@ -409,7 +692,7 @@ func TestEnsureNamespace(t *testing.T) {
 					return errors.New("something went wrong")
 				},
 			},
-			assertions: func(t *testing.T, _ kargoapi.ProjectStatus, err error) {
+			assertions: func(t *testing.T, err error) {
 				require.ErrorContains(t, err, "something went wrong")
 				require.ErrorContains(t, err, "error getting namespace")
 			},
@@ -427,7 +710,7 @@ func TestEnsureNamespace(t *testing.T) {
 					return nil
 				},
 			},
-			assertions: func(t *testing.T, _ kargoapi.ProjectStatus, err error) {
+			assertions: func(t *testing.T, err error) {
 				require.True(t, errors.Is(err, errProjectNamespaceExists))
 			},
 		},
@@ -457,7 +740,7 @@ func TestEnsureNamespace(t *testing.T) {
 					return nil
 				},
 			},
-			assertions: func(t *testing.T, _ kargoapi.ProjectStatus, err error) {
+			assertions: func(t *testing.T, err error) {
 				require.NoError(t, err)
 			},
 		},
@@ -487,7 +770,7 @@ func TestEnsureNamespace(t *testing.T) {
 					return false, errors.New("something went wrong")
 				},
 			},
-			assertions: func(t *testing.T, _ kargoapi.ProjectStatus, err error) {
+			assertions: func(t *testing.T, err error) {
 				require.ErrorContains(t, err, "error ensuring finalizer on namespace")
 				require.ErrorContains(t, err, "something went wrong")
 			},
@@ -525,7 +808,7 @@ func TestEnsureNamespace(t *testing.T) {
 					return errors.New("something went wrong")
 				},
 			},
-			assertions: func(t *testing.T, _ kargoapi.ProjectStatus, err error) {
+			assertions: func(t *testing.T, err error) {
 				require.ErrorContains(t, err, "error patching namespace")
 				require.ErrorContains(t, err, "something went wrong")
 			},
@@ -563,7 +846,7 @@ func TestEnsureNamespace(t *testing.T) {
 					return nil
 				},
 			},
-			assertions: func(t *testing.T, _ kargoapi.ProjectStatus, err error) {
+			assertions: func(t *testing.T, err error) {
 				require.NoError(t, err)
 			},
 		},
@@ -587,7 +870,7 @@ func TestEnsureNamespace(t *testing.T) {
 					return errors.New("something went wrong")
 				},
 			},
-			assertions: func(t *testing.T, _ kargoapi.ProjectStatus, err error) {
+			assertions: func(t *testing.T, err error) {
 				require.ErrorContains(t, err, "something went wrong")
 				require.ErrorContains(t, err, "error creating namespace")
 			},
@@ -616,23 +899,22 @@ func TestEnsureNamespace(t *testing.T) {
 					return nil
 				},
 			},
-			assertions: func(t *testing.T, _ kargoapi.ProjectStatus, err error) {
+			assertions: func(t *testing.T, err error) {
 				require.NoError(t, err)
 			},
 		},
 	}
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
-			res, err := testCase.reconciler.ensureNamespace(
-				context.Background(),
-				testCase.project,
+			testCase.assertions(
+				t,
+				testCase.reconciler.ensureNamespace(context.Background(), testCase.project),
 			)
-			testCase.assertions(t, res, err)
 		})
 	}
 }
 
-func TestEnsureAPIAdminPermissions(t *testing.T) {
+func TestReconciler_ensureAPIAdminPermissions(t *testing.T) {
 	testCases := []struct {
 		name       string
 		reconciler *reconciler
@@ -698,7 +980,7 @@ func TestEnsureAPIAdminPermissions(t *testing.T) {
 	}
 }
 
-func TestEnsureControllerPermissions(t *testing.T) {
+func TestReconciler_ensureControllerPermissions(t *testing.T) {
 	cfg := ReconcilerConfigFromEnv()
 
 	testControllerSA := &corev1.ServiceAccount{
@@ -952,7 +1234,7 @@ func TestEnsureControllerPermissions(t *testing.T) {
 	}
 }
 
-func TestEnsureDefaultProjectRoles(t *testing.T) {
+func TestReconciler_ensureDefaultProjectRoles(t *testing.T) {
 	testCases := []struct {
 		name       string
 		reconciler *reconciler
@@ -1061,7 +1343,7 @@ func TestEnsureDefaultProjectRoles(t *testing.T) {
 		t.Run(testCase.name, func(t *testing.T) {
 			testCase.assertions(
 				t,
-				testCase.reconciler.ensureDefaultProjectRoles(
+				testCase.reconciler.ensureDefaultUserRoles(
 					context.Background(),
 					&kargoapi.Project{},
 				),
@@ -1070,7 +1352,7 @@ func TestEnsureDefaultProjectRoles(t *testing.T) {
 	}
 }
 
-func TestMigratePhaseToConditions(t *testing.T) {
+func TestReconciler_migratePhaseToConditions(t *testing.T) {
 	tests := []struct {
 		name       string
 		project    *kargoapi.Project
@@ -1110,10 +1392,10 @@ func TestMigratePhaseToConditions(t *testing.T) {
 				reconcilingCondition := conditions.Get(&status, kargoapi.ConditionTypeReconciling)
 				require.NotNil(t, reconcilingCondition)
 				require.Equal(t, metav1.ConditionTrue, reconcilingCondition.Status)
-				require.Equal(t, "Initializing", reconcilingCondition.Reason)
+				require.Equal(t, "Syncing", reconcilingCondition.Reason)
 				require.Equal(
 					t,
-					"Creating project namespace and initializing permissions",
+					"Ensuring project namespace and permissions",
 					reconcilingCondition.Message,
 				)
 				require.Equal(t, int64(1), reconcilingCondition.ObservedGeneration)
@@ -1121,10 +1403,10 @@ func TestMigratePhaseToConditions(t *testing.T) {
 				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
 				require.NotNil(t, readyCondition)
 				require.Equal(t, metav1.ConditionFalse, readyCondition.Status)
-				require.Equal(t, "Initializing", readyCondition.Reason)
+				require.Equal(t, "Syncing", readyCondition.Reason)
 				require.Equal(
 					t,
-					"Creating project namespace and initializing permissions",
+					"Ensuring project namespace and permissions",
 					readyCondition.Message,
 				)
 				require.Equal(t, int64(1), readyCondition.ObservedGeneration)
@@ -1161,12 +1443,7 @@ func TestMigratePhaseToConditions(t *testing.T) {
 				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
 				require.NotNil(t, readyCondition)
 				require.Equal(t, metav1.ConditionFalse, readyCondition.Status)
-				require.Equal(t, "NamespaceInitializationFailed", readyCondition.Reason)
-				require.Contains(
-					t,
-					readyCondition.Message,
-					"namespace already exists and is not labeled as a Project namespace",
-				)
+				require.Equal(t, "ExistingNamespaceMissingLabel", readyCondition.Reason)
 				require.Equal(t, int64(2), readyCondition.ObservedGeneration)
 			},
 		},
@@ -1189,8 +1466,8 @@ func TestMigratePhaseToConditions(t *testing.T) {
 				readyCondition := conditions.Get(&status, kargoapi.ConditionTypeReady)
 				require.NotNil(t, readyCondition)
 				require.Equal(t, metav1.ConditionTrue, readyCondition.Status)
-				require.Equal(t, "Initialized", readyCondition.Reason)
-				require.Equal(t, "Project is initialized and ready for use", readyCondition.Message)
+				require.Equal(t, "Synced", readyCondition.Reason)
+				require.Equal(t, "Project is synced and ready for use", readyCondition.Message)
 				require.Equal(t, int64(3), readyCondition.ObservedGeneration)
 			},
 		},
@@ -1222,203 +1499,126 @@ func TestMigratePhaseToConditions(t *testing.T) {
 }
 
 func TestMigrateSpecToProjectConfig(t *testing.T) {
+	const testProject = "fake-project"
+	testScheme := runtime.NewScheme()
+	err := kargoapi.AddToScheme(testScheme)
+	require.NoError(t, err)
+
 	tests := []struct {
-		name       string
-		project    *kargoapi.Project
-		assertions func(t *testing.T, projectConfig *kargoapi.ProjectConfig, ok bool)
+		name        string
+		project     *kargoapi.Project
+		interceptor interceptor.Funcs
+		assertions  func(t *testing.T, migrated bool, cl client.Client, err error)
 	}{
 		{
 			name: "nil spec",
 			project: &kargoapi.Project{
-				Spec: nil,
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
+				Spec:       nil,
 			},
-			assertions: func(t *testing.T, projectConfig *kargoapi.ProjectConfig, ok bool) {
-				require.Nil(t, projectConfig)
-				require.False(t, ok)
+			assertions: func(t *testing.T, migrated bool, cl client.Client, err error) {
+				require.NoError(t, err)
+				require.False(t, migrated)
+				projCfg := &kargoapi.ProjectConfig{}
+				err = cl.Get(
+					context.Background(),
+					types.NamespacedName{
+						Name:      testProject,
+						Namespace: testProject,
+					},
+					projCfg,
+				)
+				require.True(t, kubeerr.IsNotFound(err))
 			},
 		},
 		{
 			name: "empty promotion policies",
 			project: &kargoapi.Project{
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
 				Spec: &kargoapi.ProjectSpec{ // nolint:staticcheck
 					PromotionPolicies: []kargoapi.PromotionPolicy{},
 				},
 			},
-			assertions: func(t *testing.T, projectConfig *kargoapi.ProjectConfig, ok bool) {
-				require.Nil(t, projectConfig)
-				require.True(t, ok)
-				// Verify that the spec was cleared
-				require.Nil(t, projectConfig)
+			assertions: func(t *testing.T, migrated bool, cl client.Client, err error) {
+				require.NoError(t, err)
+				require.True(t, migrated)
+				project := &kargoapi.Project{}
+				err = cl.Get(context.Background(), types.NamespacedName{Name: testProject}, project)
+				require.NoError(t, err)
+				require.Nil(t, project.Spec) // nolint:staticcheck
+				projCfg := &kargoapi.ProjectConfig{}
+				err = cl.Get(
+					context.Background(),
+					types.NamespacedName{
+						Name:      testProject,
+						Namespace: testProject,
+					},
+					projCfg,
+				)
+				require.True(t, kubeerr.IsNotFound(err))
 			},
 		},
 		{
-			name: "with promotion policies",
+			name: "error creating ProjectConfig",
 			project: &kargoapi.Project{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "test-project",
-				},
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
 				Spec: &kargoapi.ProjectSpec{ // nolint:staticcheck
 					PromotionPolicies: []kargoapi.PromotionPolicy{
 						{Stage: "policy-1"},
 					},
 				},
 			},
-			assertions: func(t *testing.T, projectConfig *kargoapi.ProjectConfig, ok bool) {
-				require.NotNil(t, projectConfig)
-				require.True(t, ok)
-				require.Equal(t, "test-project", projectConfig.Name)
-				require.Equal(t, "test-project", projectConfig.Namespace)
-				require.Len(t, projectConfig.Spec.PromotionPolicies, 1)
-				require.Equal(t, "policy-1", projectConfig.Spec.PromotionPolicies[0].Stage)
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			projectConfig, ok := migrateSpecToProjectConfig(tt.project)
-			tt.assertions(t, projectConfig, ok)
-		})
-	}
-}
-
-func TestMustReconcileProject(t *testing.T) {
-	tests := []struct {
-		name       string
-		project    *kargoapi.Project
-		assertions func(t *testing.T, reason string, ok bool)
-	}{
-		{
-			name: "Stalled condition is true",
-			project: &kargoapi.Project{
-				Status: kargoapi.ProjectStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:   kargoapi.ConditionTypeStalled,
-							Status: metav1.ConditionTrue,
-							Reason: "StalledReason",
-						},
-					},
-				},
-			},
-			assertions: func(t *testing.T, reason string, ok bool) {
-				require.Equal(t, "StalledReason", reason)
-				require.False(t, ok)
-			},
-		},
-		{
-			name: "Stalled condition is false",
-			project: &kargoapi.Project{
-				Status: kargoapi.ProjectStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:   kargoapi.ConditionTypeStalled,
-							Status: metav1.ConditionFalse,
-							Reason: "NotStalledReason",
-						},
-					},
-				},
-			},
-			assertions: func(t *testing.T, reason string, ok bool) {
-				require.Empty(t, reason)
-				require.True(t, ok)
-			},
-		},
-		{
-			name: "Ready condition is true",
-			project: &kargoapi.Project{
-				Status: kargoapi.ProjectStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:   kargoapi.ConditionTypeReady,
-							Status: metav1.ConditionTrue,
-							Reason: "ReadyReason",
-						},
-					},
-				},
-			},
-			assertions: func(t *testing.T, reason string, ok bool) {
-				require.Equal(t, "ReadyReason", reason)
-				require.False(t, ok)
-			},
-		},
-		{
-			name: "Ready condition is false",
-			project: &kargoapi.Project{
-				Status: kargoapi.ProjectStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:   kargoapi.ConditionTypeReady,
-							Status: metav1.ConditionFalse,
-							Reason: "NotReadyReason",
-						},
-					},
+			interceptor: interceptor.Funcs{
+				Create: func(context.Context, client.WithWatch, client.Object, ...client.CreateOption) error {
+					return errors.New("something went wrong")
 				},
 			},
-			assertions: func(t *testing.T, reason string, ok bool) {
-				require.Empty(t, reason)
-				require.True(t, ok)
+			assertions: func(t *testing.T, migrated bool, _ client.Client, err error) {
+				require.ErrorContains(t, err, "something went wrong")
+				require.False(t, migrated)
 			},
 		},
 		{
-			name: "Stalled true takes precedence over Ready true",
+			name: "success with promotion policies",
 			project: &kargoapi.Project{
-				Status: kargoapi.ProjectStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:   kargoapi.ConditionTypeStalled,
-							Status: metav1.ConditionTrue,
-							Reason: "StalledReason",
-						},
-						{
-							Type:   kargoapi.ConditionTypeReady,
-							Status: metav1.ConditionTrue,
-							Reason: "ReadyReason",
-						},
+				ObjectMeta: metav1.ObjectMeta{Name: testProject},
+				Spec: &kargoapi.ProjectSpec{ // nolint:staticcheck
+					PromotionPolicies: []kargoapi.PromotionPolicy{
+						{Stage: "policy-1"},
 					},
 				},
 			},
-			assertions: func(t *testing.T, reason string, ok bool) {
-				require.Equal(t, "StalledReason", reason)
-				require.False(t, ok)
-			},
-		},
-		{
-			name: "No relevant conditions",
-			project: &kargoapi.Project{
-				Status: kargoapi.ProjectStatus{
-					Conditions: []metav1.Condition{
-						{
-							Type:   "SomeOtherCondition",
-							Status: metav1.ConditionTrue,
-							Reason: "SomeOtherReason",
-						},
+			assertions: func(t *testing.T, migrated bool, cl client.Client, err error) {
+				require.NoError(t, err)
+				require.True(t, migrated)
+				project := &kargoapi.Project{}
+				err = cl.Get(context.Background(), types.NamespacedName{Name: testProject}, project)
+				require.NoError(t, err)
+				require.Nil(t, project.Spec) // nolint:staticcheck
+				projCfg := &kargoapi.ProjectConfig{}
+				err = cl.Get(
+					context.Background(),
+					types.NamespacedName{
+						Name:      testProject,
+						Namespace: testProject,
 					},
-				},
-			},
-			assertions: func(t *testing.T, reason string, ok bool) {
-				require.Empty(t, reason)
-				require.True(t, ok)
-			},
-		},
-		{
-			name: "Empty conditions",
-			project: &kargoapi.Project{
-				Status: kargoapi.ProjectStatus{
-					Conditions: []metav1.Condition{},
-				},
-			},
-			assertions: func(t *testing.T, reason string, ok bool) {
-				require.Empty(t, reason)
-				require.True(t, ok)
+					projCfg,
+				)
+				require.NoError(t, err)
 			},
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			reason, ok := mustReconcileProject(tt.project)
-			tt.assertions(t, reason, ok)
+			cl := fake.NewClientBuilder().
+				WithScheme(testScheme).
+				WithObjects(tt.project).
+				WithInterceptorFuncs(tt.interceptor).
+				Build()
+			r := &reconciler{client: cl}
+			migrated, err := r.migrateSpecToProjectConfig(context.Background(), tt.project)
+			tt.assertions(t, migrated, cl, err)
 		})
 	}
 }
diff --git a/internal/controller/management/projects/stats_test.go b/internal/controller/management/projects/stats_test.go
new file mode 100644
index 00000000..d32d1c56
--- /dev/null
+++ b/internal/controller/management/projects/stats_test.go
@@ -0,0 +1,244 @@
+package projects
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
+
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+	"github.com/akuity/kargo/internal/conditions"
+)
+
+func Test_reconciler_collectStats(t *testing.T) {
+	const testProject = "fake-project"
+
+	scheme := runtime.NewScheme()
+	err := kargoapi.AddToScheme(scheme)
+	require.NoError(t, err)
+
+	testCases := []struct {
+		name       string
+		project    *kargoapi.Project
+		client     client.Client
+		assertions func(*testing.T, kargoapi.ProjectStatus, error)
+	}{
+		{
+			name:    "Project not ready",
+			project: &kargoapi.Project{},
+			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
+				require.NoError(t, err)
+				require.Nil(t, status.Stats)
+			},
+		},
+		{
+			name: "error listing Warehouses",
+			project: &kargoapi.Project{
+				Status: kargoapi.ProjectStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeReady,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+			client: fake.NewClientBuilder().WithScheme(scheme).
+				WithInterceptorFuncs(interceptor.Funcs{
+					List: func(
+						context.Context,
+						client.WithWatch,
+						client.ObjectList,
+						...client.ListOption,
+					) error {
+						return fmt.Errorf("something went wrong")
+					},
+				}).Build(),
+			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
+				require.Error(t, err)
+				cond := conditions.Get(&status, kargoapi.ConditionTypeHealthy)
+				require.NotNil(t, cond)
+				require.Equal(t, metav1.ConditionFalse, cond.Status)
+				require.Equal(t, kargoapi.ConditionTypeHealthy, cond.Type)
+				require.Equal(t, "CollectingWarehouseStatsFailed", cond.Reason)
+			},
+		},
+		{
+			name: "error listing Stages",
+			project: &kargoapi.Project{
+				Status: kargoapi.ProjectStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeReady,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+			client: fake.NewClientBuilder().WithScheme(scheme).
+				WithInterceptorFuncs(interceptor.Funcs{
+					List: func(
+						_ context.Context,
+						_ client.WithWatch,
+						list client.ObjectList,
+						_ ...client.ListOption,
+					) error {
+						if _, ok := list.(*kargoapi.StageList); ok {
+							return fmt.Errorf("something went wrong")
+						}
+						return nil
+					},
+				}).Build(),
+			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
+				require.Error(t, err)
+				cond := conditions.Get(&status, kargoapi.ConditionTypeHealthy)
+				require.NotNil(t, cond)
+				require.Equal(t, metav1.ConditionFalse, cond.Status)
+				require.Equal(t, kargoapi.ConditionTypeHealthy, cond.Type)
+				require.Equal(t, "CollectingStageStatsFailed", cond.Reason)
+			},
+		},
+		{
+			name: "successful stats collection",
+			project: &kargoapi.Project{
+				Status: kargoapi.ProjectStatus{
+					Conditions: []metav1.Condition{{
+						Type:   kargoapi.ConditionTypeReady,
+						Status: metav1.ConditionTrue,
+					}},
+				},
+			},
+			client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
+				&kargoapi.Warehouse{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "warehouse1",
+						Namespace: testProject,
+					},
+					Status: kargoapi.WarehouseStatus{
+						Conditions: []metav1.Condition{{
+							Type:   kargoapi.ConditionTypeHealthy,
+							Status: metav1.ConditionTrue,
+						}},
+					},
+				},
+				&kargoapi.Warehouse{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "warehouse2",
+						Namespace: testProject,
+					},
+					Status: kargoapi.WarehouseStatus{
+						Conditions: []metav1.Condition{{
+							Type:   kargoapi.ConditionTypeHealthy,
+							Status: metav1.ConditionFalse,
+						}},
+					},
+				},
+				&kargoapi.Warehouse{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "warehouse3",
+						Namespace: testProject,
+					},
+					// No health condition == unknown
+				},
+				&kargoapi.Warehouse{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "warehouse4",
+						Namespace: testProject,
+					},
+					Status: kargoapi.WarehouseStatus{
+						Conditions: []metav1.Condition{{
+							Type:   kargoapi.ConditionTypeHealthy,
+							Status: metav1.ConditionStatus("bogus"),
+						}},
+					},
+				},
+				&kargoapi.Warehouse{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "warehouse5",
+						Namespace: testProject,
+					},
+					Status: kargoapi.WarehouseStatus{
+						Conditions: []metav1.Condition{{
+							Type:   kargoapi.ConditionTypeHealthy,
+							Status: metav1.ConditionUnknown,
+						}},
+					},
+				},
+				&kargoapi.Stage{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "stage1",
+						Namespace: testProject,
+					},
+					Status: kargoapi.StageStatus{
+						Conditions: []metav1.Condition{{
+							Type:   kargoapi.ConditionTypeHealthy,
+							Status: metav1.ConditionTrue,
+						}},
+					},
+				},
+				&kargoapi.Stage{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "stage2",
+						Namespace: testProject,
+					},
+					Status: kargoapi.StageStatus{
+						Conditions: []metav1.Condition{{
+							Type:   kargoapi.ConditionTypeHealthy,
+							Status: metav1.ConditionFalse,
+						}},
+					},
+				},
+				&kargoapi.Stage{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "stage3",
+						Namespace: testProject,
+					},
+					// No health condition == unknown
+				},
+				&kargoapi.Stage{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "stage4",
+						Namespace: testProject,
+					},
+					Status: kargoapi.StageStatus{
+						Conditions: []metav1.Condition{{
+							Type:   kargoapi.ConditionTypeHealthy,
+							Status: metav1.ConditionStatus("bogus"), // Unknown
+						}},
+					},
+				},
+				&kargoapi.Stage{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "stage5",
+						Namespace: testProject,
+					},
+					Status: kargoapi.StageStatus{
+						Conditions: []metav1.Condition{{
+							Type:   kargoapi.ConditionTypeHealthy,
+							Status: metav1.ConditionUnknown,
+						}},
+					},
+				},
+			).Build(),
+			assertions: func(t *testing.T, status kargoapi.ProjectStatus, err error) {
+				require.NoError(t, err)
+				require.Nil(t, conditions.Get(&status, kargoapi.ConditionTypeHealthy))
+				stats := status.Stats
+				require.Equal(t, int64(5), stats.Warehouses.Count)
+				require.Equal(t, int64(1), stats.Warehouses.Health.Healthy)
+				require.Equal(t, int64(5), stats.Stages.Count)
+				require.Equal(t, int64(1), stats.Stages.Health.Healthy)
+			},
+		},
+	}
+
+	for _, tt := range testCases {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &reconciler{client: tt.client}
+			status, err := r.collectStats(context.Background(), tt.project)
+			tt.assertions(t, status, err)
+		})
+	}
+}
diff --git a/internal/gitprovider/bitbucket/bitbucket_test.go b/internal/gitprovider/bitbucket/bitbucket_test.go
new file mode 100644
index 00000000..3d5b97bf
--- /dev/null
+++ b/internal/gitprovider/bitbucket/bitbucket_test.go
@@ -0,0 +1,1055 @@
+package bitbucket
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"github.com/ktrysmt/go-bitbucket"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/akuity/kargo/internal/gitprovider"
+)
+
+type mockPullRequestClient struct {
+	createPullRequestFunc func(opt *bitbucket.PullRequestsOptions) (any, error)
+	listPullRequestsFunc  func(opt *bitbucket.PullRequestsOptions) (any, error)
+	getPullRequestFunc    func(opt *bitbucket.PullRequestsOptions) (any, error)
+	getCommitFunc         func(opt *bitbucket.CommitsOptions) (any, error)
+}
+
+func (m *mockPullRequestClient) CreatePullRequest(opt *bitbucket.PullRequestsOptions) (any, error) {
+	return m.createPullRequestFunc(opt)
+}
+
+func (m *mockPullRequestClient) ListPullRequests(opt *bitbucket.PullRequestsOptions) (any, error) {
+	return m.listPullRequestsFunc(opt)
+}
+
+func (m *mockPullRequestClient) GetPullRequest(opt *bitbucket.PullRequestsOptions) (any, error) {
+	return m.getPullRequestFunc(opt)
+}
+
+func (m *mockPullRequestClient) GetCommit(opt *bitbucket.CommitsOptions) (any, error) {
+	return m.getCommitFunc(opt)
+}
+
+func TestNewProvider(t *testing.T) {
+	t.Run("successful creation", func(t *testing.T) {
+		provider, err := NewProvider("https://bitbucket.org/owner/repo", &gitprovider.Options{Token: "token"})
+		assert.NoError(t, err)
+		assert.NotNil(t, provider)
+	})
+
+	t.Run("successful creation with nil options", func(t *testing.T) {
+		provider, err := NewProvider("https://bitbucket.org/owner/repo", nil)
+		assert.NoError(t, err)
+		assert.NotNil(t, provider)
+	})
+
+	t.Run("error with invalid URL", func(t *testing.T) {
+		provider, err := NewProvider("://invalid-url", &gitprovider.Options{Token: "token"})
+		assert.Error(t, err)
+		assert.Nil(t, provider)
+	})
+
+	t.Run("error with unsupported host", func(t *testing.T) {
+		provider, err := NewProvider("https://not-bitbucket.org/owner/repo", &gitprovider.Options{Token: "token"})
+		assert.Error(t, err)
+		assert.Nil(t, provider)
+	})
+
+	t.Run("error with invalid path", func(t *testing.T) {
+		provider, err := NewProvider("https://bitbucket.org/invalid-path", &gitprovider.Options{Token: "token"})
+		assert.Error(t, err)
+		assert.Nil(t, provider)
+	})
+}
+
+func TestCreatePullRequest(t *testing.T) {
+	t.Run("successful creation", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			createPullRequestFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{
+					"id":    int64(1),
+					"state": prStateOpen,
+					"links": map[string]any{
+						"html": map[string]any{
+							"href": "https://bitbucket.org/owner/repo/pull-requests/1",
+						},
+					},
+					"source": map[string]any{
+						"branch": map[string]any{
+							"name": "feature-branch",
+						},
+						"commit": map[string]any{
+							"hash": "abcdef1234567890",
+						},
+					},
+					"destination": map[string]any{
+						"branch": map[string]any{
+							"name": "main",
+						},
+					},
+					"created_on": "2023-01-01T12:00:00Z",
+				}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		opts := &gitprovider.CreatePullRequestOpts{
+			Title:       "Test PR",
+			Description: "PR description",
+			Head:        "feature-branch",
+			Base:        "main",
+		}
+		pr, err := provider.CreatePullRequest(ctx, opts)
+		assert.NoError(t, err)
+		assert.NotNil(t, pr)
+		assert.Equal(t, int64(1), pr.Number)
+		assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL)
+		assert.Equal(t, "abcdef1234567890", pr.HeadSHA)
+		assert.True(t, pr.Open)
+		assert.False(t, pr.Merged)
+	})
+
+	t.Run("successful creation with nil options", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			createPullRequestFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{
+					"id":    int64(1),
+					"state": prStateOpen,
+				}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		pr, err := provider.CreatePullRequest(ctx, nil)
+		assert.NoError(t, err)
+		assert.NotNil(t, pr)
+		assert.Equal(t, int64(1), pr.Number)
+	})
+
+	t.Run("creation with merge commit", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			createPullRequestFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{
+					"id":    int64(1),
+					"state": prStateOpen,
+					"merge_commit": map[string]any{
+						"hash": "short123",
+					},
+				}, nil
+			},
+			getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) {
+				return map[string]any{
+					"hash": "full1234567890abcdef",
+				}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		pr, err := provider.CreatePullRequest(ctx, nil)
+		assert.NoError(t, err)
+		assert.NotNil(t, pr)
+		assert.Equal(t, "full1234567890abcdef", pr.MergeCommitSHA)
+	})
+
+	t.Run("error during creation", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			createPullRequestFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return nil, errors.New("creation failed")
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		pr, err := provider.CreatePullRequest(ctx, nil)
+		assert.Error(t, err)
+		assert.Nil(t, pr)
+	})
+
+	t.Run("error converting PR response", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			createPullRequestFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				// Return something that can't be properly unmarshaled
+				return make(chan int), nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		pr, err := provider.CreatePullRequest(ctx, nil)
+		assert.Error(t, err)
+		assert.Nil(t, pr)
+	})
+
+	t.Run("error getting full commit SHA", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			createPullRequestFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{
+					"id":    int64(1),
+					"state": prStateOpen,
+					"merge_commit": map[string]any{
+						"hash": "short123",
+					},
+				}, nil
+			},
+			getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) {
+				return nil, errors.New("commit fetch failed")
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		pr, err := provider.CreatePullRequest(ctx, nil)
+		assert.Error(t, err)
+		assert.Nil(t, pr)
+	})
+}
+
+func TestGetPullRequest(t *testing.T) {
+	t.Run("successful retrieval", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			getPullRequestFunc: func(opt *bitbucket.PullRequestsOptions) (any, error) {
+				assert.Equal(t, "1", opt.ID)
+				return map[string]any{
+					"id":    int64(1),
+					"state": prStateOpen,
+					"links": map[string]any{
+						"html": map[string]any{
+							"href": "https://bitbucket.org/owner/repo/pull-requests/1",
+						},
+					},
+					"source": map[string]any{
+						"branch": map[string]any{
+							"name": "feature-branch",
+						},
+						"commit": map[string]any{
+							"hash": "abcdef1234567890",
+						},
+					},
+					"destination": map[string]any{
+						"branch": map[string]any{
+							"name": "main",
+						},
+					},
+					"created_on": "2023-01-01T12:00:00Z",
+				}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		pr, err := provider.GetPullRequest(ctx, 1)
+		assert.NoError(t, err)
+		assert.NotNil(t, pr)
+		assert.Equal(t, int64(1), pr.Number)
+		assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL)
+		assert.Equal(t, "abcdef1234567890", pr.HeadSHA)
+		assert.True(t, pr.Open)
+		assert.False(t, pr.Merged)
+		assert.NotNil(t, pr.CreatedAt)
+	})
+
+	t.Run("retrieval of merged PR", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			getPullRequestFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{
+					"id":    int64(1),
+					"state": prStateMerged,
+					"merge_commit": map[string]any{
+						"hash": "short123",
+					},
+				}, nil
+			},
+			getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) {
+				return map[string]any{
+					"hash": "full1234567890abcdef",
+				}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		pr, err := provider.GetPullRequest(ctx, 1)
+		assert.NoError(t, err)
+		assert.NotNil(t, pr)
+		assert.False(t, pr.Open)
+		assert.True(t, pr.Merged)
+		assert.Equal(t, "full1234567890abcdef", pr.MergeCommitSHA)
+	})
+
+	t.Run("error during retrieval", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			getPullRequestFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return nil, errors.New("retrieval failed")
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		pr, err := provider.GetPullRequest(ctx, 1)
+		assert.Error(t, err)
+		assert.Nil(t, pr)
+	})
+
+	t.Run("error converting PR response", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			getPullRequestFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				// Return something that can't be properly unmarshaled
+				return make(chan int), nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		pr, err := provider.GetPullRequest(ctx, 1)
+		assert.Error(t, err)
+		assert.Nil(t, pr)
+	})
+}
+
+func TestListPullRequests(t *testing.T) {
+	t.Run("list open PRs by default", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(opt *bitbucket.PullRequestsOptions) (any, error) {
+				assert.Equal(t, []string{prStateOpen}, opt.States)
+				return map[string]any{"values": []any{
+					map[string]any{"id": int64(1), "state": prStateOpen},
+					map[string]any{"id": int64(2), "state": prStateOpen},
+				}}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, nil)
+		assert.NoError(t, err)
+		assert.Len(t, prs, 2)
+	})
+
+	t.Run("list all PRs", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(opt *bitbucket.PullRequestsOptions) (any, error) {
+				assert.Contains(t, opt.States, prStateOpen)
+				assert.Contains(t, opt.States, prStateMerged)
+				assert.Contains(t, opt.States, prStateDeclined)
+				assert.Contains(t, opt.States, prStateSuperseded)
+				return map[string]any{"values": []any{
+					map[string]any{"id": int64(1), "state": prStateOpen},
+					map[string]any{"id": int64(2), "state": prStateMerged},
+					map[string]any{"id": int64(3), "state": prStateDeclined},
+					map[string]any{"id": int64(4), "state": prStateSuperseded},
+				}}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{
+			State: gitprovider.PullRequestStateAny,
+		})
+		assert.NoError(t, err)
+		assert.Len(t, prs, 4)
+	})
+
+	t.Run("list closed PRs", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(opt *bitbucket.PullRequestsOptions) (any, error) {
+				assert.Contains(t, opt.States, prStateMerged)
+				assert.Contains(t, opt.States, prStateDeclined)
+				assert.Contains(t, opt.States, prStateSuperseded)
+				assert.NotContains(t, opt.States, prStateOpen)
+				return map[string]any{"values": []any{
+					map[string]any{"id": int64(2), "state": prStateMerged},
+					map[string]any{"id": int64(3), "state": prStateDeclined},
+					map[string]any{"id": int64(4), "state": prStateSuperseded},
+				}}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{
+			State: gitprovider.PullRequestStateClosed,
+		})
+		assert.NoError(t, err)
+		assert.Len(t, prs, 3)
+	})
+
+	t.Run("filter by head branch", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{"values": []any{
+					map[string]any{
+						"id":    int64(1),
+						"state": prStateOpen,
+						"source": map[string]any{
+							"branch": map[string]any{
+								"name": "feature-1",
+							},
+							"commit": map[string]any{
+								"hash": "hash1",
+							},
+						},
+						"destination": map[string]any{
+							"branch": map[string]any{
+								"name": "main",
+							},
+						},
+					},
+					map[string]any{
+						"id":    int64(2),
+						"state": prStateOpen,
+						"source": map[string]any{
+							"branch": map[string]any{
+								"name": "feature-2",
+							},
+							"commit": map[string]any{
+								"hash": "hash2",
+							},
+						},
+						"destination": map[string]any{
+							"branch": map[string]any{
+								"name": "main",
+							},
+						},
+					},
+				}}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{
+			HeadBranch: "feature-1",
+		})
+		assert.NoError(t, err)
+		assert.Len(t, prs, 1)
+		assert.Equal(t, int64(1), prs[0].Number)
+	})
+
+	t.Run("filter by base branch", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{"values": []any{
+					map[string]any{
+						"id":    int64(1),
+						"state": prStateOpen,
+						"source": map[string]any{
+							"branch": map[string]any{
+								"name": "feature-1",
+							},
+							"commit": map[string]any{
+								"hash": "hash1",
+							},
+						},
+						"destination": map[string]any{
+							"branch": map[string]any{
+								"name": "main",
+							},
+						},
+					},
+					map[string]any{
+						"id":    int64(2),
+						"state": prStateOpen,
+						"source": map[string]any{
+							"branch": map[string]any{
+								"name": "feature-2",
+							},
+							"commit": map[string]any{
+								"hash": "hash2",
+							},
+						},
+						"destination": map[string]any{
+							"branch": map[string]any{
+								"name": "dev",
+							},
+						},
+					},
+				}}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{
+			BaseBranch: "dev",
+		})
+		assert.NoError(t, err)
+		assert.Len(t, prs, 1)
+		assert.Equal(t, int64(2), prs[0].Number)
+	})
+
+	t.Run("filter by head commit", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{"values": []any{
+					map[string]any{
+						"id":    int64(1),
+						"state": prStateOpen,
+						"source": map[string]any{
+							"branch": map[string]any{
+								"name": "feature-1",
+							},
+							"commit": map[string]any{
+								"hash": "specific-hash",
+							},
+						},
+					},
+					map[string]any{
+						"id":    int64(2),
+						"state": prStateOpen,
+						"source": map[string]any{
+							"branch": map[string]any{
+								"name": "feature-2",
+							},
+							"commit": map[string]any{
+								"hash": "other-hash",
+							},
+						},
+					},
+				}}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{
+			HeadCommit: "specific-hash",
+		})
+		assert.NoError(t, err)
+		assert.Len(t, prs, 1)
+		assert.Equal(t, int64(1), prs[0].Number)
+	})
+
+	t.Run("PR with merge commit", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{"values": []any{
+					map[string]any{
+						"id":    int64(1),
+						"state": prStateMerged,
+						"merge_commit": map[string]any{
+							"hash": "short123",
+						},
+					},
+				}}, nil
+			},
+			getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) {
+				return map[string]any{
+					"hash": "full1234567890abcdef",
+				}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, nil)
+		assert.NoError(t, err)
+		assert.Len(t, prs, 1)
+		assert.Equal(t, "full1234567890abcdef", prs[0].MergeCommitSHA)
+	})
+
+	t.Run("error during list", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return nil, errors.New("list failed")
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, nil)
+		assert.Error(t, err)
+		assert.Nil(t, prs)
+	})
+
+	t.Run("invalid response format", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return "not a map", nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, nil)
+		assert.Error(t, err)
+		assert.Nil(t, prs)
+	})
+
+	t.Run("missing values field", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, nil)
+		assert.Error(t, err)
+		assert.Nil(t, prs)
+	})
+
+	t.Run("invalid values type", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			listPullRequestsFunc: func(*bitbucket.PullRequestsOptions) (any, error) {
+				return map[string]any{"values": "not an array"}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, nil)
+		assert.Error(t, err)
+		assert.Nil(t, prs)
+	})
+
+	t.Run("invalid state", func(t *testing.T) {
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+		}
+
+		ctx := context.Background()
+		prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{
+			State: "invalid-state",
+		})
+		assert.Error(t, err)
+		assert.Nil(t, prs)
+	})
+}
+
+func TestGetFullCommitSHA(t *testing.T) {
+	t.Run("successful retrieval", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			getCommitFunc: func(opt *bitbucket.CommitsOptions) (any, error) {
+				assert.Equal(t, "short123", opt.Revision)
+				return map[string]any{
+					"hash": "full1234567890abcdef",
+				}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		sha, err := provider.getFullCommitSHA(ctx, "short123")
+		assert.NoError(t, err)
+		assert.Equal(t, "full1234567890abcdef", sha)
+	})
+
+	t.Run("empty SHA input", func(t *testing.T) {
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+		}
+
+		ctx := context.Background()
+		sha, err := provider.getFullCommitSHA(ctx, "")
+		assert.NoError(t, err)
+		assert.Equal(t, "", sha)
+	})
+
+	t.Run("error during retrieval", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) {
+				return nil, errors.New("retrieval failed")
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		sha, err := provider.getFullCommitSHA(ctx, "short123")
+		assert.Error(t, err)
+		assert.Equal(t, "", sha)
+	})
+
+	t.Run("invalid response format", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) {
+				return "not a map", nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		sha, err := provider.getFullCommitSHA(ctx, "short123")
+		assert.Error(t, err)
+		assert.Equal(t, "", sha)
+	})
+
+	t.Run("missing hash field", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) {
+				return map[string]any{}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		sha, err := provider.getFullCommitSHA(ctx, "short123")
+		assert.Error(t, err)
+		assert.Equal(t, "", sha)
+	})
+
+	t.Run("invalid hash type", func(t *testing.T) {
+		mockClient := &mockPullRequestClient{
+			getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) {
+				return map[string]any{
+					"hash": 12345, // Not a string
+				}, nil
+			},
+		}
+		provider := &provider{
+			owner:    "owner",
+			repoSlug: "repo",
+			client:   mockClient,
+		}
+
+		ctx := context.Background()
+		sha, err := provider.getFullCommitSHA(ctx, "short123")
+		assert.Error(t, err)
+		assert.Equal(t, "", sha)
+	})
+}
+
+func TestParseRepoURL(t *testing.T) {
+	tests := []struct {
+		name      string
+		url       string
+		wantHost  string
+		wantOwner string
+		wantSlug  string
+		wantErr   bool
+	}{
+		{
+			name:      "valid URL",
+			url:       "https://bitbucket.org/owner/repo",
+			wantHost:  "bitbucket.org",
+			wantOwner: "owner",
+			wantSlug:  "repo",
+			wantErr:   false,
+		},
+		{
+			name:      "valid URL with trailing slash",
+			url:       "https://bitbucket.org/owner/repo/",
+			wantHost:  "bitbucket.org",
+			wantOwner: "owner",
+			wantSlug:  "repo",
+			wantErr:   false,
+		},
+		{
+			name:      "valid URL with .git suffix",
+			url:       "https://bitbucket.org/owner/repo.git",
+			wantHost:  "bitbucket.org",
+			wantOwner: "owner",
+			wantSlug:  "repo",
+			wantErr:   false,
+		},
+		{
+			name:      "valid SSH URL",
+			url:       "git@bitbucket.org:owner/repo.git",
+			wantHost:  "bitbucket.org",
+			wantOwner: "owner",
+			wantSlug:  "repo",
+			wantErr:   false,
+		},
+		{
+			name:    "invalid URL format",
+			url:     "://invalid-url",
+			wantErr: true,
+		},
+		{
+			name:    "missing repository name",
+			url:     "https://bitbucket.org/owner",
+			wantErr: true,
+		},
+		{
+			name:    "too many path segments",
+			url:     "https://bitbucket.org/owner/repo/extra",
+			wantErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			host, owner, slug, err := parseRepoURL(tt.url)
+			if tt.wantErr {
+				assert.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			assert.Equal(t, tt.wantHost, host)
+			assert.Equal(t, tt.wantOwner, owner)
+			assert.Equal(t, tt.wantSlug, slug)
+		})
+	}
+}
+
+func Test_toBitbucketPR(t *testing.T) {
+	t.Run("valid conversion", func(t *testing.T) {
+		resp := map[string]any{
+			"id":    int64(1),
+			"state": prStateOpen,
+			"links": map[string]any{
+				"html": map[string]any{
+					"href": "https://bitbucket.org/owner/repo/pull-requests/1",
+				},
+			},
+			"source": map[string]any{
+				"branch": map[string]any{
+					"name": "feature-branch",
+				},
+				"commit": map[string]any{
+					"hash": "abcdef1234567890",
+				},
+			},
+			"destination": map[string]any{
+				"branch": map[string]any{
+					"name": "main",
+				},
+				"commit": map[string]any{
+					"hash": "1234567890abcdef",
+				},
+			},
+			"merge_commit": map[string]any{
+				"hash": "merged1234567890",
+			},
+			"created_on": "2023-01-01T12:00:00Z",
+		}
+
+		pr, err := toBitbucketPR(resp)
+		require.NoError(t, err)
+		assert.Equal(t, int64(1), pr.ID)
+		assert.Equal(t, prStateOpen, pr.State)
+		assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.Links.HTML.Href)
+		assert.Equal(t, "feature-branch", pr.Source.Branch.Name)
+		assert.Equal(t, "abcdef1234567890", pr.Source.Commit.Hash)
+		assert.Equal(t, "main", pr.Destination.Branch.Name)
+		assert.Equal(t, "1234567890abcdef", pr.Destination.Commit.Hash)
+		assert.Equal(t, "merged1234567890", pr.MergeCommit.Hash)
+		assert.Equal(t, "2023-01-01T12:00:00Z", pr.CreatedOn)
+	})
+
+	t.Run("invalid input type", func(t *testing.T) {
+		pr, err := toBitbucketPR(make(chan int))
+		assert.Error(t, err)
+		assert.Nil(t, pr)
+	})
+}
+
+func Test_toProviderPR(t *testing.T) {
+	t.Run("valid conversion: open PR", func(t *testing.T) {
+		bbPR := &bitbucketPR{
+			ID:    1,
+			State: prStateOpen,
+			Links: struct {
+				HTML struct {
+					Href string `json:"href"`
+				} `json:"html"`
+			}{
+				HTML: struct {
+					Href string `json:"href"`
+				}{
+					Href: "https://bitbucket.org/owner/repo/pull-requests/1",
+				},
+			},
+			Source: struct {
+				Branch struct {
+					Name string `json:"name"`
+				} `json:"branch"`
+				Commit struct {
+					Hash string `json:"hash"`
+				} `json:"commit"`
+			}{
+				Branch: struct {
+					Name string `json:"name"`
+				}{
+					Name: "feature-branch",
+				},
+				Commit: struct {
+					Hash string `json:"hash"`
+				}{
+					Hash: "abcdef1234567890",
+				},
+			},
+			CreatedOn: "2023-01-01T12:00:00Z",
+		}
+
+		raw := map[string]any{"id": 1}
+		pr := toProviderPR(bbPR, raw)
+		require.NotNil(t, pr)
+		assert.Equal(t, int64(1), pr.Number)
+		assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL)
+		assert.Equal(t, "abcdef1234567890", pr.HeadSHA)
+		assert.True(t, pr.Open)
+		assert.False(t, pr.Merged)
+		assert.NotNil(t, pr.CreatedAt)
+		assert.Equal(t, raw, pr.Object)
+	})
+
+	t.Run("valid conversion: merged PR", func(t *testing.T) {
+		bbPR := &bitbucketPR{
+			ID:    1,
+			State: prStateMerged,
+			MergeCommit: struct {
+				Hash string `json:"hash"`
+			}{
+				Hash: "merged1234567890",
+			},
+			CreatedOn: "2023-01-01T12:00:00Z",
+		}
+
+		pr := toProviderPR(bbPR, nil)
+		require.NotNil(t, pr)
+		assert.False(t, pr.Open)
+		assert.True(t, pr.Merged)
+		assert.Equal(t, "merged1234567890", pr.MergeCommitSHA)
+	})
+
+	t.Run("nil input", func(t *testing.T) {
+		pr := toProviderPR(nil, nil)
+		assert.Nil(t, pr)
+	})
+
+	t.Run("invalid date", func(t *testing.T) {
+		bbPR := &bitbucketPR{
+			ID:        1,
+			CreatedOn: "not-a-date",
+		}
+
+		pr := toProviderPR(bbPR, nil)
+		require.NotNil(t, pr)
+		assert.Nil(t, pr.CreatedAt)
+	})
+}
+
+func Test_registration(t *testing.T) {
+	t.Run("predicate matches bitbucket.org URL", func(t *testing.T) {
+		result := registration.Predicate("https://bitbucket.org/owner/repo")
+		assert.True(t, result)
+	})
+
+	t.Run("predicate doesn't match other URLs", func(t *testing.T) {
+		result := registration.Predicate("https://github.com/owner/repo")
+		assert.False(t, result)
+	})
+
+	t.Run("predicate handles invalid URLs", func(t *testing.T) {
+		result := registration.Predicate("://invalid-url")
+		assert.False(t, result)
+	})
+
+	t.Run("NewProvider factory works", func(t *testing.T) {
+		provider, err := registration.NewProvider("https://bitbucket.org/owner/repo", nil)
+		assert.NoError(t, err)
+		assert.NotNil(t, provider)
+	})
+}
diff --git a/internal/promotion/runner/builtin/git_cloner_test.go b/internal/promotion/runner/builtin/git_cloner_test.go
index e73493e7..eb0c7c3e 100644
--- a/internal/promotion/runner/builtin/git_cloner_test.go
+++ b/internal/promotion/runner/builtin/git_cloner_test.go
@@ -215,7 +215,7 @@ func Test_gitCloner_run(t *testing.T) {
 	defer repo.Close()
 	err = os.WriteFile(filepath.Join(repo.Dir(), "test.txt"), []byte("foo"), 0600)
 	require.NoError(t, err)
-	err = repo.AddAllAndCommit("Initial commit")
+	err = repo.AddAllAndCommit("Initial commit", nil)
 	require.NoError(t, err)
 	err = repo.Push(nil)
 	require.NoError(t, err)
diff --git a/internal/promotion/runner/builtin/git_commiter_test.go b/internal/promotion/runner/builtin/git_commiter_test.go
index 20ed2938..4cc3e44a 100644
--- a/internal/promotion/runner/builtin/git_commiter_test.go
+++ b/internal/promotion/runner/builtin/git_commiter_test.go
@@ -24,7 +24,7 @@ func Test_gitCommitter_validate(t *testing.T) {
 		expectedProblems []string
 	}{
 		{
-			name:   "path not specified",
+			name: "path not specified",
 			config: promotion.Config{
 				"message": "fake commit message",
 			},
@@ -42,7 +42,7 @@ func Test_gitCommitter_validate(t *testing.T) {
 			},
 		},
 		{
-			name:   "message is not specified",
+			name: "message is not specified",
 			config: promotion.Config{
 				"path": "/tmp/foo",
 			},
diff --git a/internal/promotion/runner/builtin/git_pusher_test.go b/internal/promotion/runner/builtin/git_pusher_test.go
index cdd95331..e9649896 100644
--- a/internal/promotion/runner/builtin/git_pusher_test.go
+++ b/internal/promotion/runner/builtin/git_pusher_test.go
@@ -197,7 +197,7 @@ func Test_gitPusher_run(t *testing.T) {
 
 	// Commit the changes similarly to how gitCommitter would
 	// have. It will be gitPushStepRunner's job to push this commit.
-	err = workTree.AddAllAndCommit("Initial commit")
+	err = workTree.AddAllAndCommit("Initial commit", nil)
 	require.NoError(t, err)
 
 	// Now we can proceed to test gitPusher...