Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
7a13ef6
feat: GitCommitStatus init
zachaller Nov 17, 2025
c90eb8e
save point
zachaller Nov 17, 2025
d4e83b8
save point
zachaller Nov 18, 2025
0764324
save point
zachaller Nov 18, 2025
692c817
Merge branch 'main' of github.com:argoproj-labs/gitops-promoter into …
zachaller Nov 18, 2025
fb6eb83
save point
zachaller Nov 18, 2025
05a469b
save point
zachaller Nov 18, 2025
20acbbe
save point
zachaller Nov 19, 2025
b193d6d
save point
zachaller Nov 19, 2025
8a1512f
save point
zachaller Nov 20, 2025
c629733
Merge branch 'main' of github.com:argoproj-labs/gitops-promoter into …
zachaller Nov 20, 2025
987a5c6
Merge branch 'main' of github.com:argoproj-labs/gitops-promoter into …
zachaller Nov 20, 2025
97c5a98
Merge branch 'main' of github.com:argoproj-labs/gitops-promoter into …
zachaller Nov 21, 2025
746dab9
Merge branch 'main' of github.com:argoproj-labs/gitops-promoter into …
zachaller Nov 22, 2025
f38ab73
chore: go mod tidy
zachaller Nov 22, 2025
6e702a8
chore: lint
zachaller Nov 23, 2025
710f657
compile cache
zachaller Nov 26, 2025
9dd9160
Merge branch 'main' of github.com:argoproj-labs/gitops-promoter into …
zachaller Nov 26, 2025
002c0e6
docs updates
zachaller Nov 26, 2025
1fdf2bb
docs updates
zachaller Nov 26, 2025
9e3ad1b
add suppport for proposed validation
zachaller Nov 26, 2025
cabafbf
add docs
zachaller Nov 26, 2025
3c65423
lint
zachaller Nov 26, 2025
2acab4a
more tests
zachaller Nov 26, 2025
767f259
more tests
zachaller Nov 27, 2025
e4926a4
more tests
zachaller Nov 27, 2025
8f94a98
remove old docs
zachaller Nov 27, 2025
9e4f653
Merge branch 'main' of github.com:argoproj-labs/gitops-promoter into …
zachaller Dec 1, 2025
6745970
docs cleanup
zachaller Dec 1, 2025
c511069
fix: make trailers support multipe same values
zachaller Dec 1, 2025
14bdc74
cleanup
zachaller Dec 1, 2025
32461fa
codegen
zachaller Dec 1, 2025
1bbc186
add test for actice failing
zachaller Dec 2, 2025
5bf2357
add test using git revert
zachaller Dec 2, 2025
ddbdd9b
lint
zachaller Dec 2, 2025
17730d7
Merge branch 'main' of github.com:argoproj-labs/gitops-promoter into …
zachaller Dec 2, 2025
fdd005b
cleanup
zachaller Dec 2, 2025
81c4c41
Merge branch 'main' of github.com:argoproj-labs/gitops-promoter into …
zachaller Dec 9, 2025
28d4ea5
move over to enqueue func
zachaller Dec 9, 2025
5c1993a
update docs
zachaller Dec 9, 2025
e382735
remove ReconciledAt annotation
zachaller Dec 9, 2025
3729aa0
small refactor
zachaller Dec 9, 2025
14a7f88
cleanup
zachaller Dec 9, 2025
e21a1e5
remove accidental add
zachaller Dec 9, 2025
bcb595b
change printer columns
zachaller Dec 9, 2025
3173d01
ExpressionMessage to Message
zachaller Dec 10, 2025
32e6c1b
no need to pass emtpy string
zachaller Dec 10, 2025
cfa8bef
use constants
zachaller Dec 10, 2025
a92f53a
return errors
zachaller Dec 11, 2025
21924c4
remove message
zachaller Dec 11, 2025
09c1ddf
remove message
zachaller Dec 11, 2025
5cd28a6
min 1 validation
zachaller Dec 11, 2025
dad9391
validatedcommit to Target
zachaller Dec 12, 2025
ddd7a5a
small docs change
zachaller Dec 12, 2025
b16a8f5
remove old fields
zachaller Dec 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,13 @@ resources:
kind: TimedCommitStatus
path: github.com/argoproj-labs/gitops-promoter/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: argoproj.io
group: promoter
kind: GitCommitStatus
path: github.com/argoproj-labs/gitops-promoter/api/v1alpha1
version: v1alpha1
version: "3"
2 changes: 1 addition & 1 deletion api/v1alpha1/argocdcommitstatus_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ type ApplicationsSelected struct {
// +kubebuilder:subresource:status

// ArgoCDCommitStatus is the Schema for the argocdcommitstatuses API.
// +kubebuilder:printcolumn:name="Strategy",type=string,JSONPath=`.spec.promotionStrategyRef.name`,priority=1
// +kubebuilder:printcolumn:name="PromotionStrategy",type=string,JSONPath=`.spec.promotionStrategyRef.name`,priority=1
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
type ArgoCDCommitStatus struct {
metav1.TypeMeta `json:",inline"`
Expand Down
3 changes: 0 additions & 3 deletions api/v1alpha1/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ const TimedCommitStatusLabel = "promoter.argoproj.io/timed-commit-status"
// PreviousEnvironmentCommitStatusKey the commit status key name used to indicate the previous environment health
const PreviousEnvironmentCommitStatusKey = "promoter-previous-environment"

// ReconcileAtAnnotation is the annotation used to indicate when the webhook triggered a reconcile
const ReconcileAtAnnotation = "promoter.argoproj.io/reconcile-at"

// CommitStatusPreviousEnvironmentStatusesAnnotation is the label used to identify commit statuses that make up the aggregated active commit status
const CommitStatusPreviousEnvironmentStatusesAnnotation = "promoter.argoproj.io/previous-environment-statuses"

Expand Down
16 changes: 16 additions & 0 deletions api/v1alpha1/controllerconfiguration_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ type ControllerConfigurationSpec struct {
// including WorkQueue settings that control reconciliation behavior.
// +required
TimedCommitStatus TimedCommitStatusConfiguration `json:"timedCommitStatus"`

// GitCommitStatus contains the configuration for the GitCommitStatus controller,
// including WorkQueue settings that control reconciliation behavior.
// +required
GitCommitStatus GitCommitStatusConfiguration `json:"gitCommitStatus"`
}

// PromotionStrategyConfiguration defines the configuration for the PromotionStrategy controller.
Expand Down Expand Up @@ -140,6 +145,17 @@ type TimedCommitStatusConfiguration struct {
WorkQueue WorkQueue `json:"workQueue"`
}

// GitCommitStatusConfiguration defines the configuration for the GitCommitStatus controller.
//
// This configuration controls how the GitCommitStatus controller processes reconciliation
// requests, including requeue intervals, concurrency limits, and rate limiting behavior.
type GitCommitStatusConfiguration struct {
// WorkQueue contains the work queue configuration for the GitCommitStatus controller.
// This includes requeue duration, maximum concurrent reconciles, and rate limiter settings.
// +required
WorkQueue WorkQueue `json:"workQueue"`
}

// WorkQueue defines the work queue configuration for a controller.
//
// This configuration directly correlates to parameters used with Kubernetes client-go work queues.
Expand Down
229 changes: 229 additions & 0 deletions api/v1alpha1/gitcommitstatus_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
Copyright 2024.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// GitCommitStatusSpec defines the desired state of GitCommitStatus
type GitCommitStatusSpec struct {
// PromotionStrategyRef is a reference to the promotion strategy that this commit status applies to.
// The controller will validate commits from ALL environments in the referenced PromotionStrategy
// where this GitCommitStatus.Spec.Key matches an entry in either:
// - PromotionStrategy.Spec.ProposedCommitStatuses (applies to all environments), OR
// - Environment.ProposedCommitStatuses (applies to specific environment)
// +required
PromotionStrategyRef ObjectReference `json:"promotionStrategyRef"`

// Key is the unique identifier for this validation rule.
// It is used as the commit status key and in status messages.
// This key is matched against PromotionStrategy's proposedCommitStatuses or activeCommitStatuses
// to determine which environments this validation applies to.
// +required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
Key string `json:"key"`

// Description is a human-readable description of this validation that will be shown in the SCM provider
// (GitHub, GitLab, etc.) as the commit status description.
// If not specified, defaults to empty string.
// +optional
Description string `json:"description,omitempty"`

// Target specifies which commit SHA to validate with the expression.
// - "active": Validates the currently active/deployed commit (default behavior)
// - "proposed": Validates the proposed commit that will be promoted
//
// The validation result is always reported on the PROPOSED commit (for gating), but this field
// controls which commit's data is used in the expression evaluation.
//
// Examples:
// target: "active" - "Don't promote if a revert commit is detected"
// target: "proposed" - "Don't promote unless new commit follows naming convention"
//
// +optional
// +kubebuilder:default="active"
// +kubebuilder:validation:Enum=active;proposed
Target string `json:"target,omitempty"`

// Expression is evaluated using the expr library (github.com/expr-lang/expr) against commit data
// for environments in the referenced PromotionStrategy.
// The expression must return a boolean value where true indicates the validation passed.
//
// The commit validated is determined by the Target field:
// - "active" (default): Validates the ACTIVE (currently deployed) commit
// - "proposed": Validates the PROPOSED commit (what will be promoted)
//
// The validation result is always reported on the PROPOSED commit to enable promotion gating.
//
// Use Cases by Mode:
// Active mode: State-based gating - validate current environment before allowing promotion
// - "Don't promote until active commit has required sign-offs"
// - "Verify current deployment meets compliance before next promotion"
// - "Ensure active commit is not a revert before promoting"
//
// Proposed mode: Change-based gating - validate the incoming change itself
// - "Don't promote unless new commit follows naming convention"
// - "Ensure proposed commit has proper JIRA ticket reference"
// - "Require specific author for proposed changes"
//
//
// Available variables in the expression context:
// - Commit.SHA (string): the commit SHA being validated (active or proposed based on Target)
// - Commit.Subject (string): the first line of the commit message
// - Commit.Body (string): the commit message body (everything after the subject line)
// - Commit.Author (string): commit author email address
// - Commit.Committer (string): committer email address
// - Commit.Trailers (map[string][]string): git trailers parsed from commit message
//
// +required
Expression string `json:"expression"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have it return a structure so that we can expand features in the future?

expression: |
  {
    "passed": 1 == 2,
    "message": Commit.SHA + " is busted"
  }

Or maybe that would just end up being a new field:

messageExpression: |-
  Commit.SHA + " is busted"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I cant think of a reason to change the return to anything other than bool but I do think in a follow up PR it would be good to have description templateable.

}

// GitCommitStatusStatus defines the observed state of GitCommitStatus.
type GitCommitStatusStatus struct {
// Environments holds the validation results for each environment where this validation applies.
// Each entry corresponds to an environment from the PromotionStrategy where the Key matches
// either global or environment-specific proposedCommitStatuses.
//
// The controller validates the commit specified by Target ("active" or "proposed")
// but the CommitStatus is always reported on the PROPOSED commit for promotion gating.
// Each environment entry tracks both the ProposedHydratedSha (where status is reported) and the
// ActiveHydratedSha, with TargetedSha indicating which one was actually evaluated.
// +listType=map
// +listMapKey=branch
// +optional
Environments []GitCommitStatusEnvironmentStatus `json:"environments,omitempty"`

// Conditions represent the latest available observations of the GitCommitStatus's state.
// Standard condition types include "Ready" which aggregates the status of all environments.
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// GitCommitStatusEnvironmentStatus defines the observed validation status for a specific environment.
type GitCommitStatusEnvironmentStatus struct {
// Branch is the environment branch name being validated.
// +required
Branch string `json:"branch"`

// ProposedHydratedSha is the proposed hydrated commit SHA where the validation result is reported.
// This comes from the PromotionStrategy's environment status.
// The CommitStatus resource is created with this SHA, allowing the PromotionStrategy to gate
// promotions based on the validation of the ACTIVE commit.
// May be empty if the PromotionStrategy hasn't reconciled yet.
// +required
ProposedHydratedSha string `json:"proposedHydratedSha"`

// ActiveHydratedSha is the currently active (deployed) hydrated commit SHA that was validated.
// This comes from the PromotionStrategy's environment status.
// The expression is evaluated against THIS commit's data, not the proposed commit.
// May be empty if the PromotionStrategy hasn't reconciled yet.
// +optional
ActiveHydratedSha string `json:"activeHydratedSha,omitempty"`

// TargetedSha is the commit SHA that was actually validated by the expression.
// This will match either ProposedHydratedSha or ActiveHydratedSha depending on
// the Target setting ("proposed" or "active").
// This field clarifies which commit's data was used in the expression evaluation.
// +optional
TargetedSha string `json:"targetedSha,omitempty"`

// Phase represents the current validation state of the commit.
// - "pending": validation has not completed, commit data is not yet available, or SHAs are empty
// - "success": expression evaluated to true, validation passed
// - "failure": expression evaluated to false, validation failed, or expression compilation failed
// +kubebuilder:validation:Enum=pending;success;failure
// +required
Phase string `json:"phase"`

// ExpressionResult contains the boolean result of the expression evaluation.
// Only set when the expression successfully evaluates to a boolean.
// nil indicates the expression has not yet been evaluated, failed to compile, or failed to evaluate.
// +optional
ExpressionResult *bool `json:"expressionResult,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Key",type=string,JSONPath=`.spec.key`
// +kubebuilder:printcolumn:name="PromotionStrategy",type=string,JSONPath=`.spec.promotionStrategyRef.name`
// +kubebuilder:printcolumn:name="Validates",type=string,JSONPath=`.spec.target`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`

// GitCommitStatus is the Schema for the gitcommitstatuses API.
//
// It validates commits from PromotionStrategy environments using configurable expressions
// and creates CommitStatus resources with the validation results.
//
// Use the Target field to control which commit is validated:
// - "active" (default): Validates the currently deployed commit
// - "proposed": Validates the incoming commit
//
// The validation result is always reported on the PROPOSED commit to enable promotion gating,
// regardless of which commit was validated.
//
// Workflow:
// 1. Controller reads PromotionStrategy to get ProposedHydratedSha and ActiveHydratedSha
// 2. Controller selects SHA to validate based on Target field
// 3. Controller fetches commit data (subject, body, author, trailers) for selected SHA
// 4. Controller evaluates expression against selected commit data
// 5. Controller creates/updates CommitStatus with result attached to PROPOSED SHA
// 6. PromotionStrategy checks CommitStatus on PROPOSED SHA before allowing promotion
//
// Common use cases:
// - "Ensure active commit is not a revert before promoting"
type GitCommitStatus struct {
metav1.TypeMeta `json:",inline"`

// metadata is a standard object metadata
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`

// spec defines the desired state of GitCommitStatus
// +required
Spec GitCommitStatusSpec `json:"spec"`

// status defines the observed state of GitCommitStatus
// +optional
Status GitCommitStatusStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// GitCommitStatusList contains a list of GitCommitStatus
type GitCommitStatusList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []GitCommitStatus `json:"items"`
}

// GetConditions returns the conditions of the GitCommitStatus.
func (g *GitCommitStatus) GetConditions() *[]metav1.Condition {
return &g.Status.Conditions
}

func init() {
SchemeBuilder.Register(&GitCommitStatus{}, &GitCommitStatusList{})
}
2 changes: 1 addition & 1 deletion api/v1alpha1/pullrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (ps *PullRequest) GetConditions() *[]metav1.Condition {

// PullRequest is the Schema for the pullrequests API
// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state`
// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.ID`
// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.status.id`
// +kubebuilder:printcolumn:name="Source",type=string,JSONPath=`.spec.sourceBranch`,priority=1
// +kubebuilder:printcolumn:name="Target",type=string,JSONPath=`.spec.targetBranch`,priority=1
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/timedcommitstatus_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ type TimedCommitStatusEnvironmentsStatus struct {
// +kubebuilder:subresource:status

// TimedCommitStatus is the Schema for the timedcommitstatuses API
// +kubebuilder:printcolumn:name="Strategy",type=string,JSONPath=`.spec.promotionStrategyRef.name`
// +kubebuilder:printcolumn:name="PromotionStrategy",type=string,JSONPath=`.spec.promotionStrategyRef.name`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
type TimedCommitStatus struct {
metav1.TypeMeta `json:",inline"`
Expand Down
Loading
Loading