Skip to content

CRD Specs

PromotionStrategy

The PromotionStrategy is the user's interface to controlling how changes are promoted through their environments. In this CR, the user configures the list of live hydrated environment branches in their order of promotion. They'll also configure the checks which must pass between promotion steps.

apiVersion: promoter.argoproj.io/v1alpha1
kind: PromotionStrategy
metadata:
  name: example-promotion-strategy
spec:
  gitRepositoryRef:
    name: example-git-repo
  activeCommitStatuses:
    - key: argocd-app-health
  proposedCommitStatuses:
    - key: security-scan
  environments:
    - branch: environment/dev
    - branch: environment/test
    - branch: environment/prod
      autoMerge: false
      activeCommitStatuses:
      - key: performance-test
      proposedCommitStatuses:
      - key: deployment-freeze
status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    # The Ready condition indicates that the resource has been successfully reconciled, when there is an error during
    # reconciliation, the condition will be False with a reason of ReconciliationError. When we successfully reconcile the resource,
    # the condition will be True with a reason of ReconciliationSuccess. The Ready condition is essentially a way to show reconciliation
    # errors to the user. This condition exists on all resources that have reconciliation logic.
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess, ReconciliationError, ChangeTransferPolicyNotReady, or PreviousEnvironmentCommitStatusNotReady
      status: "True" # "True," "False," or "Unknown"
      observedGeneration: 123
  environments:
  - branch: environment/dev
    # The proposed and active fields are pulled directly from the status of the environment's ChangeTransferPolicy resource.
    proposed:
      dry:
        author: "Author Name <author@example.com>"
        body: "Body of the commit message (i.e. excluding the subject line)"
        commitTime: 2023-10-01T00:00:00Z
        repoURL: "https://git.example.com/org/repo.git"
        sha: "abcdef1234567890abcdef1234567890abcdef12"
        subject: "chore: Example commit subject line"
        references:
          # An array of reference commits that where used to create the dry commit. This is used generally via CI systems to add tracing information to the commit.
          # For example, in a code/deployment repo setup this could contain the commit from the code repo that triggered the deployment commit in the deployment repo.
          - commit:
              author: '"Zach Aller" <code@example.com>'
              body: |-
                Commit message of the code commit

                Signed-off-by: Author Name <author@example.com>
              date: '2025-07-19T17:50:18Z'
              repoURL: https://github.com/org/repo
              sha: 9d5ccef278218dea4caa903bb6abb9ed974a1d90
              subject: This change fixes a bug in the code
      hydrated:
        # The hydrated field contains the same fields as proposed.dry, but the contents correspond to a hydrated commit
        # instead of a dry commit.
      note:
        # Dry SHA from the hydrator git note on the proposed hydrated commit (only drySha is populated today)
        drySha: "abcdef1234567890abcdef1234567890abcdef12"
      commitStatuses:
        - key: example-key
          phase: pending # pending, success, or failure
          url: https://example.com/checks/example-key
          description: Waiting for check
    active:
    # The active field contains the same fields as proposed.
    pullRequest:
      id: "848"
      state: open # open, closed, or merged; empty when externallyMergedOrClosed is true
      url: https://github.com/org/repo/pull/848
      prCreationTime: 2023-10-01T00:00:00Z
    history:
      # The history field contains a snapshot of each promotion that has occurred in the environment. The most recent promotion
      # is at the front of the list. The fields here are similar to those in proposed and active top level fields. They only differ in
      # that the proposed field does not contain a dry field, because in the context of history proposed becomes active when promoted.
      - active:
        # same as top level active
        proposed:
        # same as top level proposed but without dry field because in the context of history proposed.dry becomes active when promoted.
        pullRequest:
          # The pullRequest field contains information about the pull request that was merged to create this history item.
          id: '848'
          prCreationTime: '2025-08-04T19:50:15Z'
          url: https://github.com/org/repo/pull/848
    lastHealthyDryShas:
    - sha: "abcdef1234567890abcdef1234567890abcdef12"
      time: 2023-10-01T00:00:00Z
  - branch: environment/test
    # same fields as dev
  - branch: environment/prod
    # same fields as dev

ChangeTransferPolicy

A ChangeTransferPolicy represents a pair hydrated environment branch pair: the proposed environment branch and the live environment branch. When a new commit appears in the proposed branch, the ChangeTransferPolicy will open a PR against the live branch. When all the configured checks pass, the ChangeTransferPolicy will merge the PR.

A PromotionStrategy will create a ChangeTransferPolicy for each configured environment. For each environment besides the first one, the PromotionStrategy controller will inject a proposedCommitStatus to represent the active status of the previous environment. This is how the PromotionStrategy ensures that the environment PRs are merged in order, respecting the previous environments' active commit statuses.

The Events page documents the Kubernetes events produced by ChangeTransferPolicies. PromotionStrategy and ChangeTransferPolicy controllers set standard labels on related resources; see Labels.

apiVersion: promoter.argoproj.io/v1alpha1
kind: ChangeTransferPolicy
metadata:
  name: environment
spec:
  gitRepositoryRef:
    name: example-git-repository
  proposedBranch: environment/dev-next
  activeBranch: environment/dev
  # Defaults to true when omitted
  autoMerge: true
  activeCommitStatuses:
  - key: argocd-app-health
  proposedCommitStatuses:
  - key: security-scan
  - key: promoter-previous-environment
status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    # The Ready condition indicates that the resource has been successfully reconciled, when there is an error during
    # reconciliation, the condition will be False with a reason of ReconciliationError. When we successfully reconcile the resource,
    # the condition will be True with a reason of ReconciliationSuccess. The Ready condition is essentially a way to show reconciliation
    # errors to the user. This condition exists on all resources that have reconciliation logic.
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess ReconciliationError, or PullRequestNotReady
      status: "True" # "True," "False," or "Unknown"
      observedGeneration: 123

  proposed:
    dry:
      author: "Author Name <author@example.com>"
      body: "Body of the commit message (i.e. excluding the subject line)"
      commitTime: 2023-10-01T00:00:00Z
      repoURL: "https://git.example.com/org/repo.git"
      sha: "abcdef1234567890abcdef1234567890abcdef12"
      subject: "chore: Example commit subject line"
    hydrated:
    # The hydrated field contains the same fields as proposed.dry.
    note:
      # Only drySha is populated from the hydrator git note
      drySha: "abcdef1234567890abcdef1234567890abcdef12"
    commitStatuses:
      - key: example-key
        phase: pending # pending, success, or failure
        url: https://example.com/checks/example-key
        description: Waiting for check
  active:
    dry:
      sha: "fedcba0987654321fedcba0987654321fedcba09"
      commitTime: 2023-09-15T00:00:00Z
      repoURL: "https://git.example.com/org/repo.git"
      subject: "chore: Currently deployed commit"
    hydrated:
    # same structure as proposed.dry
    commitStatuses:
      - key: argocd-app-health
        phase: success
  pullRequest:
    id: "848"
    state: open # open, closed, or merged; empty when externallyMergedOrClosed is true
    url: https://github.com/org/repo/pull/848
    prCreationTime: 2023-10-01T00:00:00Z
    # prMergeTime: set when merged
    # externallyMergedOrClosed: true  # PR closed/merged outside the controller
  history:
    # Recent promotions merged by the controller (newest first, at most 5 entries).
    - active:
        dry:
          sha: "fedcba0987654321fedcba0987654321fedcba09"
      proposed:
        hydrated:
          sha: "abcdef1234567890abcdef1234567890abcdef12"
        commitStatuses:
          - key: example-key
            phase: success
      pullRequest:
        id: "847"
        state: merged
        prCreationTime: 2023-09-01T00:00:00Z
        prMergeTime: 2023-09-01T01:00:00Z
        url: https://github.com/org/repo/pull/847

PullRequest

A PullRequest is a thin wrapper around the SCM's pull request API. ChangeTransferPolicies use PullRequests to manage promotions. PullRequests carry promotion-strategy, change-transfer-policy, and environment labels for correlation; see Labels.

apiVersion: promoter.argoproj.io/v1alpha1
kind: PullRequest
metadata:
  name: example-proposed-commit
spec:
  gitRepositoryRef:
    name: example-git-repository
  targetBranch:
  sourceBranch:
  title:
  description:
  commit:
    # The commit message that will be written for the commit that's made when merging the PR
    message: "example message"
  # The commit SHA that must be at the head of the source branch for the merge to succeed.
  # This prevents race conditions where a different commit gets merged than intended.
  mergeSha: abc123def456789012345678901234567890abcd

  # Must be closed, merged, or open. Default is open.
  # Must be set to "open" when initially created, and cannot be set to "closed" or "merged" unless status.id is set
  # (which the controller should do automatically as long as there are no errors).
  state:
status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    # The Ready condition indicates that the resource has been successfully reconciled, when there is an error during
    # reconciliation, the condition will be False with a reason of ReconciliationError. When we successfully reconcile the resource,
    # the condition will be True with a reason of ReconciliationSuccess. The Ready condition is essentially a way to show reconciliation
    # errors to the user. This condition exists on all resources that have reconciliation logic.
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess or ReconciliationError
      status: "True" # "True," "False," or "Unknown"
      observedGeneration: 123
  id: "848"
  state: open # open, closed, or merged; empty when externallyMergedOrClosed is true
  url: https://github.com/org/repo/pull/848
  prCreationTime: 2023-10-01T00:00:00Z
  # externallyMergedOrClosed: true

CommitStatus

A CommitStatus is a thin wrapper for the SCM's commit status API. CommitStatuses are the primary source of truth for promotion gates. In the ideal case, the CommitStatus will write its state to the SCM's API so that the appropriate checkmarks/failures appear in the SCM's UI. But even if the SCM API calls fail, the ChangeTransferPolicy controller will use the contents of the CommitStatuses spec fields.

Controllers label CommitStatuses with three standard labels (gate key, environment branch, and parent gate). See Labels for label keys, derived parent-gate labels, and troubleshooting queries.

apiVersion: promoter.argoproj.io/v1alpha1
kind: CommitStatus
metadata:
  name: example-commit-status
  labels:
    # This label is used to define the "key" of the commit status. The value is referenced via the `key` field in a
    # PromotionStrategy's (or ChangeTransferPolicy's) `activeCommitStatuses` or `proposedCommitStatuses` field.
    #
    # CommitStatuses should be unique per key/sha combination. For example, there may be one CommitStatus with the key
    # `example` for shas abc123 and def456, but there should not be two CommitStatuses with the key `example` for sha
    # abc123.
    promoter.argoproj.io/commit-status: example
spec:
  gitRepositoryRef:
    name: example-git-repo
  sha: abcdef1234567890abcdef1234567890abcdef12
  # SCM status name; PromotionStrategy/ChangeTransferPolicy selectors reference this via key
  name: argocd-app-health
  description: Argo CD application `example-app` is healthy

  # Can be pending, success, failure. Default is pending.
  phase: success

  # Optional URL to link to more information about the commit status.
  url: https://argocd.example.com/applications/example-app
status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    # The Ready condition indicates that the resource has been successfully reconciled, when there is an error during
    # reconciliation, the condition will be False with a reason of ReconciliationError. When we successfully reconcile the resource,
    # the condition will be True with a reason of ReconciliationSuccess. The Ready condition is essentially a way to show reconciliation
    # errors to the user. This condition exists on all resources that have reconciliation logic.
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess or ReconciliationError
      status: "True" # "True," "False," or "Unknown"
      observedGeneration: 123
  id: example-commit-status-id
  phase: success # pending, success, or failure
  sha: abcdef1234567890abcdef1234567890abcdef12

GitRepository

A GitRepository represents a single git repository. It references an ScmProvider to enable access via some configured auth mechanism.

apiVersion: promoter.argoproj.io/v1alpha1
kind: GitRepository
metadata:
  name: example-git-repository
spec:
  # Only one of the git providers should be specified.
  github:
    name:
    owner:

  gitlab:
    name:
    namespace:
    projectId:

  forgejo:
    name:
    owner:

  gitea:
    name:
    owner:

  bitbucketCloud:
    owner:
    name:

  azureDevOps:
    name:
    project:

  # fake:
  #   name: example
  #   owner: example

  scmProviderRef:
    kind: ScmProvider # or ClusterScmProvider
    name: example-scm-provider
status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess or ReconciliationError
      status: "True"
      observedGeneration: 123

ScmProvider

An ScmProvider represents a scm instance (such as github). It references a Secret to enable access via some configured auth mechanism.

apiVersion: promoter.argoproj.io/v1alpha1
kind: ScmProvider
metadata:
  name: example-scm-provider
spec:
  secretRef:
    name: example-scm-provider-secret

  # You must specify exactly one provider. Multiple are shown here as examples.
  # If you do not need to specify any sub-fields, just set the field to {}.

  github:
    domain: github.example.com # Optional, leave empty for default github.com
    appID: 1234
    installationID: 1234 # Optional, will query ListInstallations if not provided

  gitlab:
    domain: gitlab.com # Optional

  forgejo:
    domain: forgejo.example.com

  gitea:
    domain: gitea.example.com

  bitbucketCloud: {}

  azureDevOps:
    organization: example-organization
    domain: dev.azure.com # Optional

  # fake: {}  # testing only
status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess or ReconciliationError
      status: "True"
      observedGeneration: 123

ClusterScmProvider

A ClusterScmProvider represents a SCM instance (such as GitHub). ClusterScmProvider is the cluster-scoped alternative to the ScmProvider. It references a Secret in the same namespace where the promoter is running to enable access via some configured auth mechanism. A ClusterScmProvider can be referenced by any GitRepository in the cluster, regardless of namespace.

apiVersion: promoter.argoproj.io/v1alpha1
kind: ClusterScmProvider
metadata:
  name: example-cluster-scm-provider
spec:
  secretRef:
    # Secret must be in the same namespace where the promoter is running
    name: example-cluster-scm-provider-secret 

  # You must specify either github, gitlab, forgejo, or bitbucketCloud. Multiple are provided here as examples.
  # If you do not need to specify any sub-fields, just set the field to {}.

  github:
    domain: github.example.com # Optional, leave empty for default github.com
    appID: 1234
    installationID: 1234 # Optional, will query ListInstallations if not provided

  gitlab:
    domain: gitlab.com # Optional

  forgejo:
    domain: forgejo.example.com

  bitbucketCloud: {}

  azureDevOps:
    organization: example-org
    domain: dev.azure.com # Optional

  gitea:
    domain: gitea.example.com
status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess or ReconciliationError
      status: "True"
      observedGeneration: 123

ArgoCDCommitStatus

An ArgoCDCommitStatus is used as a way to aggregate all the Argo CD Applications that are being used in the promotion strategy. It is used to check the status of the Argo CD Applications that are being used in the promotion strategy.

apiVersion: promoter.argoproj.io/v1alpha1
kind: ArgoCDCommitStatus
metadata:
  name: argocdcommitstatus-sample
spec:
  # Gate key for PromotionStrategy; default argocd-health
  key: argocd-health
  applicationSelector:
    matchLabels:
      app: demo
  promotionStrategyRef:
    name: argocon-demo
  url:
    template: |
      {{- $baseURL := "https://dev.argocd.local" -}}
      {{- if eq .Environment "environment/development" -}}
      {{- $baseURL = "https://dev.argocd.local" -}}
      {{- else if eq .Environment "environment/staging" -}}
      {{- $baseURL = "https://staging.argocd.local" -}}
      {{- else if eq .Environment "environment/production" -}}
      {{- $baseURL = "https://prod.argocd.local" -}}
      {{- end -}}
      {{- $labels := "" -}}
      {{- range $key, $value := .ArgoCDCommitStatus.Spec.ApplicationSelector.MatchLabels -}}
      {{- $labels = printf "%s%s=%s," $labels $key $value -}}
      {{- end -}}
      {{- printf "%s/applications?labels=%s" $baseURL (urlQueryEscape $labels) -}}
    options:
      - missingkey=zero
status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    # The Ready condition indicates that the resource has been successfully reconciled, when there is an error during
    # reconciliation, the condition will be False with a reason of ReconciliationError. When we successfully reconcile the resource,
    # the condition will be True with a reason of ReconciliationSuccess. The Ready condition is essentially a way to show reconciliation
    # errors to the user. This condition exists on all resources that have reconciliation logic.
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess, ReconciliationError, or CommitStatusesNotReady
      status: "True" # "True," "False," or "Unknown"
      observedGeneration: 123
  # applicationsSelected is a list of applications that match the applicationSelector.
  applicationsSelected:
    - environment: environments/dev
      name: example-app-name
      namespace: example-app-namespace
      phase: success # pending, success, or failure
      sha: abcdef1234567890abcdef1234567890abcdef12
      lastTransitionTime: 2023-10-01T00:00:00Z
      clusterName: "" # an empty cluster name means "local cluster"
    - environment: environments/prod
      name: example-app-name
      namespace: example-app-namespace
      phase: success # pending, success, or failure
      sha: abcdef1234567890abcdef1234567890abcdef12
      lastTransitionTime: 2023-10-01T00:00:00Z
      clusterName: "prod"

TimedCommitStatus

A TimedCommitStatus provides time-based gating for environment promotions. It monitors how long commits have been running in specified environments and creates CommitStatus resources (as active commit statuses) based on configured duration requirements.

This enables "soak time" or "bake time" policies where changes must run successfully in environments for a minimum duration before being promoted.

apiVersion: promoter.argoproj.io/v1alpha1
kind: TimedCommitStatus
metadata:
  name: webservice-tier-1
  namespace: default
spec:
  # Gate key for PromotionStrategy; default timer
  key: timer
  # Reference to the PromotionStrategy this TimedCommitStatus monitors
  promotionStrategyRef:
    name: webservice-tier-1

  # List of environments to monitor for time-based gating
  # For each environment, the controller will:
  # 1. Check how long the active commit has been running
  # 2. Create a CommitStatus for the CURRENT environment's active SHA
  # 3. Set the CommitStatus phase based on whether duration is met
  environments:
    # Monitor development environment
    # Creates an active CommitStatus that gates promotions from development
    - branch: environment/development
      duration: 1h

    # Monitor staging environment
    # Creates an active CommitStatus that gates promotions from staging
    - branch: environment/staging
      duration: 4h

    # Monitor production environment
    # Creates an active CommitStatus for production (useful for audit/compliance)
    - branch: environment/production
      duration: 24h

status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess or ReconciliationError
      status: "True"
      observedGeneration: 123
  # Status is populated by the controller
  environments:
    - branch: environment/development
      # The active commit SHA being monitored
      sha: abcdef1234567890abcdef1234567890abcdef12
      # When this commit was deployed to the environment
      commitTime: "2024-01-15T10:00:00Z"
      # The configured required duration
      requiredDuration: 1h
      # Maximum time remaining until the gate is satisfied (0 when phase is success)
      atMostDurationRemaining: 14m30s
      # Current gate status: "pending" or "success"
      phase: pending
    - branch: environment/staging
      sha: abcdef1234567890abcdef1234567890abcdef99
      commitTime: "2024-01-14T06:00:00Z"
      requiredDuration: 4h
      atMostDurationRemaining: 0s
      phase: success

GitCommitStatus

A GitCommitStatus evaluates commit data with a custom expression and creates CommitStatus resources for promotion gating. See Git Commit Status for configuration, expression variables, and examples.

apiVersion: promoter.argoproj.io/v1alpha1
kind: GitCommitStatus
metadata:
  name: no-revert-in-active
spec:
  promotionStrategyRef:
    name: example-promotion-strategy
  # Matched against proposedCommitStatuses / activeCommitStatuses key in PromotionStrategy
  key: revert-check
  description: Block promotions if active commit is a revert
  # active (default) validates the deployed commit; proposed validates the incoming commit
  target: active
  expression: '!(Commit.Subject startsWith "Revert" || Commit.Body startsWith "Revert")'
status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess # ReconciliationSuccess or ReconciliationError
      status: "True"
      observedGeneration: 123
  environments:
    - branch: environment/dev
      proposedHydratedSha: abcdef1234567890abcdef1234567890abcdef12
      activeHydratedSha: fedcba0987654321fedcba0987654321fedcba09
      targetedSha: fedcba0987654321fedcba0987654321fedcba09
      phase: success # pending, success, or failure
      expressionResult: true

WebRequestCommitStatus

A WebRequestCommitStatus gates promotions on external HTTP/HTTPS API validation. It makes HTTP requests to configurable endpoints, evaluates a validation expression against the response, and creates or updates CommitStatus resources. It supports polling mode (fixed interval) or trigger mode (expression-based triggering). See the Web Request Commit Status documentation for full configuration, examples, and template variables.

apiVersion: promoter.argoproj.io/v1alpha1
kind: WebRequestCommitStatus
metadata:
  name: external-approval
  namespace: default
spec:
  # Reference to the PromotionStrategy this WebRequestCommitStatus applies to
  promotionStrategyRef:
    name: my-app

  # Unique key for this validation; matched against proposedCommitStatuses or activeCommitStatuses
  key: external-approval

  # Human-readable description shown in the SCM (Go templates supported). Optional.
  descriptionTemplate: "Waiting for external approval"

  # URL for the commit status target link in the SCM (Go templates supported). Optional.
  urlTemplate: ""

  # Which SHA to report on: "proposed" (default) or "active". Optional.
  reportOn: proposed

  # HTTP request configuration; URL, headers, and body support Go templates
  httpRequest:
    urlTemplate: "https://approvals.example.com/api/check/{{ range .PromotionStrategy.Status.Environments }}{{ if eq .Branch $.Branch }}{{ .Proposed.Hydrated.Sha }}{{ end }}{{ end }}"
    methodTemplate: GET
    # Optional: header name -> template value
    headerTemplates: {}
    # Optional: request body template
    bodyTemplate: ""
    timeout: 30s
    # Optional: authentication (basic, bearer, oauth2, or tls with secretRef); omit if none

  # When the HTTP response is considered successful (phase success)
  success:
    when:
      expression: 'Response != nil ? (Response.StatusCode == 200 && Response.Body.approved == true) : Phase == "success"'
      # output:
      #   expression: ""      # optional; expr returning map, stored in status.successOutput (available as SuccessOutput)

  # Exactly one of polling or trigger must be set.
  mode:
    # context: environments (default) — one HTTP request per environment; status.environments[] below.
    # Use context: promotionstrategy for one shared request; then status.promotionStrategyContext is set instead.
    context: environments
    # Polling: single optional field. Interval (default 1m) controls how often the HTTP request runs.
    polling:
      interval: 2m
    # Trigger: use this instead of polling for expression-based triggering.
    # trigger:
    #   requeueDuration: 1m     # optional, default 1m
    #   when:
    #     variables:            # optional; expr returning a map, exposed as Variables to expression + output below
    #       expression: '{ "ready": true }'
    #     expression: "Variables.ready"   # required; boolean expr deciding whether to fire the request
    #     output:
    #       expression: '{ tracked: Variables.ready }'  # optional; map stored in status.triggerOutput
    #   response:               # optional
    #     output:
    #       expression: ""      # expr returning map, stored in status.responseOutput

status:
  observedGeneration: 123 # compare with metadata.generation to detect stale status
  conditions:
    # The Ready condition indicates that the resource has been successfully reconciled.
    - type: Ready
      lastTransitionTime: 2023-10-01T00:00:00Z
      message: Reconciliation succeeded
      reason: ReconciliationSuccess
      status: "True"
      observedGeneration: 123
  # Status per environment; populated by the controller
  environments:
    - branch: environment/development
      reportedSha: abc123def456789012345678901234567890abcd
      lastSuccessfulSha: abc123def456789012345678901234567890abcd
      phase: success
      lastRequestTime: "2024-01-15T10:30:00Z"
      lastResponseStatusCode: 200
      # triggerOutput (trigger mode): output from trigger.when.output.expression (available as TriggerOutput in expressions/templates)
      # responseOutput (trigger + response.output): output from response.output.expression (available as ResponseOutput in expressions/templates)
      # successOutput: output from success.when.output.expression (available as SuccessOutput in expressions/templates)

# --- promotionstrategy context (illustrative; use a separate WebRequestCommitStatus resource) ---
# spec:
#   promotionStrategyRef: { name: my-app }
#   key: pipeline-gate
#   descriptionTemplate: "Gate: {{ .Phase }} (use .PromotionStrategy for branch-specific text)"
#   reportOn: proposed
#   httpRequest:
#     urlTemplate: "https://deployments.example.com/apps/{{ .PromotionStrategy.Spec.RepositoryReference.Name }}/status"
#     method: GET
#   success:
#     when:
#       # Boolean (all envs same phase) or object { defaultPhase, environments } — see docs/commit-status-controllers/web-request.md
#       expression: "Response.StatusCode == 200"
#   mode:
#     context: promotionstrategy
#     polling:
#       interval: 2m
# status:
#   observedGeneration: 123
#   conditions:
#     - type: Ready
#       reason: ReconciliationSuccess
#       status: "True"
#       observedGeneration: 123
#   # status.environments is empty when context is promotionstrategy
#   promotionStrategyContext:
#     phasePerBranch:
#       - branch: environment/dev
#         phase: success
#       - branch: environment/staging
#         phase: pending
#     lastSuccessfulShas:
#       - branch: environment/dev
#         lastSuccessfulSha: abcdef1234567890abcdef1234567890abcdef12
#     lastRequestTime: "2024-01-15T10:30:00Z"
#     lastResponseStatusCode: 200
#     # triggerOutput / responseOutput / successOutput: JSON maps from expr output expressions

ControllerConfiguration

A ControllerConfiguration is used to configure the behavior of the promoter.

A global ControllerConfiguration is deployed alongside the controller and applies to all promotions.

All fields are required, but defaults are provided in the installation manifests.

# ControllerConfiguration defines the global settings for all promoter controllers.
# Each controller has its own configuration section with WorkQueue settings that control
# how frequently resources are reconciled, how many can run concurrently, and how
# rate limiting is applied to prevent overwhelming external systems.
apiVersion: promoter.argoproj.io/v1alpha1
kind: ControllerConfiguration
metadata:
  name: example-controller-configuration
spec:
  # PromotionStrategy controller manages promotion strategies and creates ChangeTransferPolicies
  promotionStrategy:
    workQueue:
      # How often to automatically requeue resources for reconciliation
      requeueDuration: "5m"
      # Maximum number of concurrent reconcile operations for this controller
      maxConcurrentReconciles: 3
      # Rate limiter controls retry behavior for failed reconciliations
      rateLimiter:
        # Exponential backoff: start at 1s, max out at 1 minute
        exponentialFailure:
          baseDelay: "1s"
          maxDelay: "1m"

  # ChangeTransferPolicy controller handles the actual promotion logic and creates PRs
  changeTransferPolicy:
    workQueue:
      requeueDuration: "5m"
      maxConcurrentReconciles: 5
      rateLimiter:
        # MaxOf combiner: use the maximum delay from multiple rate limiters
        # This combines per-item exponential backoff with a global rate limit
        maxOf:
          - exponentialFailure:
              baseDelay: "1s"
              maxDelay: "2m"
          - bucket:
              # Allow 10 operations per second with bursts up to 20
              qps: 10
              bucket: 20

  # PullRequest controller manages pull request lifecycle
  pullRequest:
    # Template configuration for generating PR titles and descriptions.
    # Template data has access to: .ChangeTransferPolicy, .PromotionStrategy
    template:
      title: "Promote {{ trunc 7 .ChangeTransferPolicy.Status.Proposed.Dry.Sha }} to `{{ .ChangeTransferPolicy.Spec.ActiveBranch }}`"
      description: |
        This PR is promoting the environment branch `{{ .ChangeTransferPolicy.Spec.ActiveBranch }}`.

        **Changes:**
        - Current SHA: {{ .ChangeTransferPolicy.Status.Active.Dry.Sha }}
        - Proposed SHA: {{ .ChangeTransferPolicy.Status.Proposed.Dry.Sha }}
    workQueue:
      requeueDuration: "5m"
      maxConcurrentReconciles: 3
      rateLimiter:
        # FastSlow: retry quickly for the first 3 attempts, then slow down
        # Good for handling transient SCM API errors
        fastSlow:
          fastDelay: "500ms"
          slowDelay: "30s"
          maxFastAttempts: 3

  # CommitStatus controller updates commit status based on policies
  commitStatus:
    workQueue:
      requeueDuration: "2m"
      maxConcurrentReconciles: 5
      rateLimiter:
        exponentialFailure:
          baseDelay: "500ms"
          maxDelay: "1m"

  # ArgoCDCommitStatus controller syncs ArgoCD application status to commit statuses
  argocdCommitStatus:
    watchLocalApplications: true
    workQueue:
      requeueDuration: "5m"
      maxConcurrentReconciles: 10
      rateLimiter:
        # Bucket rate limiter: control overall request rate to external APIs
        bucket:
          qps: 20
          bucket: 50

  # TimedCommitStatus controller enforces soak-time gates via CommitStatus resources
  timedCommitStatus:
    workQueue:
      requeueDuration: "1m"
      maxConcurrentReconciles: 5
      rateLimiter:
        exponentialFailure:
          baseDelay: "500ms"
          maxDelay: "1m"

  # GitCommitStatus controller evaluates commit expressions and creates CommitStatus resources
  gitCommitStatus:
    workQueue:
      requeueDuration: "2m"
      maxConcurrentReconciles: 5
      rateLimiter:
        exponentialFailure:
          baseDelay: "500ms"
          maxDelay: "1m"

  # WebRequestCommitStatus controller runs HTTP requests and reports commit status from response
  webRequestCommitStatus:
    workQueue:
      requeueDuration: "2m"
      maxConcurrentReconciles: 5
      rateLimiter:
        exponentialFailure:
          baseDelay: "500ms"
          maxDelay: "1m"

Status Conditions

Every CRD which is reconciled has a status.conditions field. Each CRD currently only populates a single Ready condition. If the Ready condition is True, then it means that 1) reconciliation of the resource has completed successfully, and 2) all child resources also had a Ready condition of True.

Observed generation

Reconciled CRDs (all resources in this document except ControllerConfiguration) set status.observedGeneration to the metadata.generation that produced the current status. When it equals metadata.generation, status is current; when it is lower, reconciliation has not caught up yet (or the last apply failed — see the Ready condition). Each Ready condition also has its own observedGeneration for the generation that condition reflects; on a failed apply, top-level status.observedGeneration may stay pinned to the last successful reconcile while the condition records the attempted generation.

Condition Reasons

All CRDs may have the following condition reasons:

  • ReconciliationSuccess
  • ReconciliationError

ArgoCDCommitStatus

The ArgoCDCommitStatus CRD may also have the following condition reasons:

  • CommitStatusesNotReady

ChangeTransferPolicy

The ChangeTransferPolicy CRD may also have the following condition reasons:

  • PullRequestNotReady

PromotionStrategy

The PromotionStrategy CRD may also have the following condition reasons:

  • PreviousEnvironmentCommitStatusNotReady
  • ChangeTransferPolicyNotReady

Finalizers

GitOps Promoter uses Kubernetes finalizers to ensure resources are deleted in the correct order, preventing orphaned resources and ensuring proper cleanup of external resources (like pull requests in the SCM).

All finalizers are managed automatically by the controllers. You do not need to set them manually via GitOps.

For a complete finalizer table (including ChangeTransferPolicy cross-resource finalizers), risks of manual removal, and how to report stuck deletes, see Finalizers. Contributors adding finalizers should read Using Finalizers.

PullRequest Finalizer

Finalizer: pullrequest.promoter.argoproj.io/finalizer

When a PullRequest is deleted, the finalizer ensures that the pull request is properly closed on the SCM before the Kubernetes resource is removed. This prevents orphaned pull requests in your SCM.

ChangeTransferPolicy-owned PullRequest finalizer: changetransferpolicy.promoter.argoproj.io/pullrequest-finalizer

Set on PullRequests managed by a ChangeTransferPolicy so the CTP controller can copy PR status before the PullRequest resource is removed.

ChangeTransferPolicy cleanup finalizer: changetransferpolicy.promoter.argoproj.io/finalizer

Set on ChangeTransferPolicy while owned PullRequests may still carry the pullrequest finalizer above; cleared after cleanup during deletion.

GitRepository Finalizer

Finalizer: gitrepository.promoter.argoproj.io/finalizer

The GitRepository finalizer prevents deletion of the GitRepository while any PullRequest resources still reference it. This ensures that PullRequests can authenticate to the SCM to close themselves properly before the GitRepository is removed.

ScmProvider and ClusterScmProvider Finalizers

ScmProvider Finalizer: scmprovider.promoter.argoproj.io/finalizer
ClusterScmProvider Finalizer: clusterscmprovider.promoter.argoproj.io/finalizer

These finalizers prevent deletion of the SCM provider while any GitRepository resources still reference it. Additionally, the ScmProvider and ClusterScmProvider controllers manage finalizers on the Secret resources they reference:

Secret Finalizer (ScmProvider): scmprovider.promoter.argoproj.io/secret-finalizer
Secret Finalizer (ClusterScmProvider): clusterscmprovider.promoter.argoproj.io/secret-finalizer

This ensures that the Secret containing authentication credentials is not deleted while it's still needed by the SCM provider.

Deletion Order

When you delete a PromotionStrategy and its associated resources, the finalizers ensure deletion happens in this order:

  1. PullRequest - Closes the PR on the SCM
  2. GitRepository - Can be deleted once all PullRequests referencing the GitRepository are gone
  3. ScmProvider/ClusterScmProvider - Can be deleted once all GitRepositories referencing the Provider are gone
  4. Secret - Can be deleted once all ScmProviders/ClusterScmProviders referencing the Secret are gone

If you attempt to delete resources out of order, Kubernetes will mark them for deletion but they will remain in a "Terminating" state until their dependent resources are removed. This is normal and expected behavior.

Labels

Built-in controllers set promoter labels on ChangeTransferPolicy, PullRequest, and CommitStatus objects so promotion checks and cleanup can list related resources. Each gate-created CommitStatus gets three standard labels: promoter.argoproj.io/commit-status, promoter.argoproj.io/environment, and a derived parent-gate key (for example promoter.argoproj.io/argo-cd-commit-status).

Label keys are defined in api/v1alpha1/constants.go; parent-gate label keys are derived from the gate Kind. Branch and name values are sanitized with KubeSafeLabel before they are stored.

See Labels for the full reference, useful kubectl queries, and troubleshooting when gating does not match expectations. For how commit-status labels relate to PromotionStrategy selectors, see Gating Promotions.

Validation Conventions

Commit SHA Format

All SHA fields in the CRDs support both SHA-1 (40 characters) and SHA-256 (64 characters) lowercase hexadecimal hashes. This ensures compatibility with both traditional and modern Git repositories while maintaining reliable comparisons between SHAs.

Background on Git Hash Functions

Git is transitioning from SHA-1 to SHA-256 as documented in the Git hash function transition plan. While most Git repositories currently use SHA-1 (40-character hashes), newer repositories can be created with SHA-256 (64-character hashes). This project supports both formats to ensure compatibility during and after the transition period.

The validation rules differ based on whether the field is required or optional:

Required SHA fields (in spec):

# +kubebuilder:validation:MinLength=40
# +kubebuilder:validation:MaxLength=64
# +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$`

Optional SHA fields (omitempty):

# +kubebuilder:validation:MaxLength=64
# +kubebuilder:validation:Pattern=`^([a-f0-9]{40}|[a-f0-9]{64})$`

Rationale: - MinLength=40: Makes the field truly required (enforces non-empty for required fields) - MaxLength=64: Accommodates both SHA-1 (40 chars) and SHA-256 (64 chars) - Pattern=^([a-f0-9]{40}|[a-f0-9]{64})$: Validates exact format - either 40 or 64 lowercase hex characters

Optional fields omit MinLength so they can be empty, but when provided, they must match one of the two valid SHA formats (40 or 64 characters).