Manual Approval Gate for Tekton Pipelines

TOC

Feature Overview

Manual Approval Gate lets pipeline authors pause a PipelineRun until designated approvers review and approve the operation. Behind the scenes, the Operator-provided controller creates an ApprovalTask resource for every approval step, tracks approver responses, and writes the result back to the originating CustomRun.

Use Cases

  • Enforcing human checkpoints before deploying to production or performing destructive maintenance.
  • Implementing multi-person approval policies by combining individuals and groups with numberOfApprovalsRequired.
  • Auditing who approved or rejected a change by querying ApprovalTask status fields or CLI output.

Prerequisites

  • A cluster administrator has deployed Manual Approval Gate. If not, refer to the deployment guide.
  • You can create or edit Pipeline/PipelineRun objects in the target namespace.
  • Approvers can patch approvaltasks.openshift-pipelines.org resources (typically via kubectl) in the target namespace. Platforms such as Alauda Container Platform grant this capability by default; only customize RBAC if you have explicitly removed the built-in permissions.

Steps

1. Declare the approval step in a pipeline

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: deploy-with-approval
spec:
  tasks:
    - name: build
      taskRef:
        name: build-bundle     # Placeholder Task; replace with your build logic
    - name: wait-for-approval
      runAfter:
        - build
      taskRef:
        apiVersion: openshift-pipelines.org/v1alpha1
        kind: ApprovalTask
      timeout: "24h"
      params:
        - name: approvers
          value:
            - alice
            - group:release-managers
        - name: numberOfApprovalsRequired
          value: "1"
        - name: description
          value: "Approve promotion into production"
    - name: deploy
      runAfter:
        - wait-for-approval
      taskRef:
        name: deploy-prod     # Placeholder Task; replace with your deployment logic

When the pipeline reaches wait-for-approval, Tekton emits a CustomRun. The approval controller creates an ApprovalTask with your parameters and keeps the PipelineRun pending until quorum is met or a rejection occurs.

2. Monitor approval status

$ kubectl get approvaltasks.openshift-pipelines.org

NAME                                         AGE
deploy-with-approval-run-wait-for-approval   4m16s
$ kubectl describe approvaltask deploy-with-approval-run-wait-for-approval
Name:         deploy-with-approval-run-wait-for-approval
Status:
  Approvals Received:  1
  Approvals Required:  1
  Approvers:
    alice
    release-managers
  Approvers Response:
    Name:      alice
    Response:  approved
    Type:      User
  Start Time:  2025-11-17T10:37:01Z
  State:       approved
Events:        <none>

Important status fields:

  • status.state: overall gate state such as pending, approved, or rejected.
  • status.approvalsRequired / status.approvalsReceived: quorum tracking (the received count appears only after at least one approver responds).
  • status.approversResponse: per-user/group outcome plus messages, useful for auditing.

3. Approve or reject

User approvals

First, inspect the approvers list to determine the correct index:

$ kubectl get approvaltask deploy-with-approval-run-wait-for-approval -o json | jq '.spec.approvers'
[
  {
    "name": "alice",
    "type": "User",
    "input": "pending",
  },
  {
    "name": "release-managers",
    "type": "Group",
    "input": "pending",
    "users": []
  }
]

To approve as a user:

$ kubectl patch approvaltask deploy-with-approval-run-wait-for-approval \
    --type='json' \
    --as alice \
    -p='[
      {"op":"replace","path":"/spec/approvers/0/input","value":"approve"},
      {"op":"replace","path":"/spec/approvers/0/message","value":"QA complete"}
    ]'

To reject as a user:

$ kubectl patch approvaltask deploy-with-approval-run-wait-for-approval \
    --type='json' \
    --as alice \
    -p='[
      {"op":"replace","path":"/spec/approvers/0/input","value":"reject"},
      {"op":"replace","path":"/spec/approvers/0/message","value":"Found regression"}
    ]'

Group approvals

When a group is configured as an approver, individual members of that group can submit their approval by adding their response to the users array under the group entry. Multiple group members can approve independently, and each approval counts toward the approvalsReceived total.

Initially, the group approver entry does not have a users field:

spec:
  approvers:
  - name: alice
    type: User
    input: pending
    message: ""
  - name: release-managers
    type: Group
    input: pending
    message: ""
  numberOfApprovalsRequired: 2

The first group member creates the users array and sets the group's input to approve:

# First member (bob) from release-managers group approves
$ kubectl patch approvaltask deploy-with-approval-run-wait-for-approval \
    --type='json' \
    --as bob \
    --as-group release-managers \
    -p='[
      {"op":"add","path":"/spec/approvers/1/users","value":[{"name":"bob","input":"approve"}]},
      {"op":"replace","path":"/spec/approvers/1/input","value":"approve"},
      {"op":"replace","path":"/spec/approvers/1/message","value":"Approved by bob"}
    ]'

Subsequent group members append to the existing users array and update the group's input:

# Second member (carol) from release-managers group also approves
$ kubectl patch approvaltask deploy-with-approval-run-wait-for-approval \
    --type='json' \
    --as carol \
    --as-group release-managers \
    -p='[
      {"op":"add","path":"/spec/approvers/1/users/-","value":{"name":"carol","input":"approve"}},
      {"op":"replace","path":"/spec/approvers/1/input","value":"approve"},
      {"op":"replace","path":"/spec/approvers/1/message","value":"Approved by carol"}
    ]'

To reject as a group member:

# A member (david) from release-managers group rejects
$ kubectl patch approvaltask deploy-with-approval-run-wait-for-approval \
    --type='json' \
    --as david \
    --as-group release-managers \
    -p='[
      {"op":"add","path":"/spec/approvers/1/users/-","value":{"name":"david","input":"reject"}},
      {"op":"replace","path":"/spec/approvers/1/input","value":"reject"},
      {"op":"replace","path":"/spec/approvers/1/message","value":"Security concerns found"}
    ]'

After these patches, check the group entry and status to see all members' responses:

$ kubectl get approvaltask deploy-with-approval-run-wait-for-approval -o json | jq '{spec: .spec.approvers[1], status: .status}'
{
  "spec": {
    "input": "approve",
    "message": "Approved by carol",
    "name": "release-managers",
    "type": "Group",
    "users": [
      {
        "input": "approve",
        "name": "bob"
      },
      {
        "input": "approve",
        "name": "carol"
      }
    ]
  },
  "status": {
    "approvalsReceived": 2,
    "approvalsRequired": 2,
    "approvers": [
      "alice",
      "release-managers"
    ],
    "approversResponse": [
      {
        "groupMembers": [
          {
            "message": "Approved by carol",
            "name": "bob",
            "response": "approved"
          },
          {
            "message": "Approved by carol",
            "name": "carol",
            "response": "approved"
          }
        ],
        "message": "Approved by carol",
        "name": "release-managers",
        "response": "approved",
        "type": "Group"
      }
    ],
    "startTime": "2025-11-18T14:07:48Z",
    "state": "approved"
  }
}

In this example, both bob and carol from the release-managers group have approved. Each approval from a group member increments approvalsReceived separately, so two group member approvals count as two approvals toward the required total. The status.approversResponse shows detailed approval information including individual group members' responses.

Key points for group approvals:

  • Each group member must perform two required operations: add their entry to the users array AND set the group's input (either approve or reject). Optionally, they can also set the group's message
  • The first group member creates the users array using path /spec/approvers/<index>/users with an array value
  • Subsequent members append to the array using path /spec/approvers/<index>/users/- where - appends to the array end
  • Each user entry in the users array contains only name and input fields (no message field within the user entry)
  • The group-level message field is optional and shared; it will be overwritten by subsequent responses if they provide a new message
  • Each group member approval increments approvalsReceived independently
  • Multiple members from the same group can approve, and each counts toward the required total
  • The status.approversResponse field tracks detailed approval information including individual group members
  • Use --as <username> --as-group <groupname> to identify as a group member when patching

The controller sets the corresponding CustomRun and PipelineRun to Succeeded or Failed accordingly: approvals accumulate until numberOfApprovalsRequired is satisfied, while any rejection immediately fails that section of the pipeline.

Tip: Use --as <username> (required) and --as-group <group> when you need to approve as a specific identity. The validation webhook allows you to modify only the entry that matches that impersonated user and group. RBAC must grant you impersonation rights. For example, kubectl patch ... --as bob --as-group release-managers identifies you as user bob acting within the release-managers group.

4. Extend PipelineRun timeouts for long approvals

If an approval could take hours or days, configure both PipelineRun.spec.timeouts.pipeline and PipelineRun.spec.timeouts.tasks to exceed the approval window so the run does not terminate before approvers respond. A simple PipelineRun to exercise the approval gate looks like the following:

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: deploy-with-approval-run
spec:
  pipelineRef:
    name: deploy-with-approval
  timeouts:
    pipeline: 72h
    tasks: 72h

Ensure the approval task's timeout parameter is shorter than the pipeline timeout. Otherwise, the PipelineRun might expire first, leaving the approval unresolved.

Operation Results

  • kubectl get approvaltasks -o yaml shows each approval gate with state and quorum-related fields (the approvalsReceived column appears after someone responds).
  • PipelineRun status reflects the approval outcome: when approved, downstream tasks resume; when rejected, the run fails with the reason propagated from the ApprovalTask.
  • Dispatch logs or kubectl get approvaltask -o yaml output provide the approval history for auditing.

Troubleshooting

  • Approval task never appears: Confirm the administrator-installed ManualApprovalGate CR is READY. Without the controller, CustomRun objects remain pending.
  • Approvers lack permissions: Grant them get, list, update, and patch access to approvaltasks.openshift-pipelines.org in the relevant namespace.
  • Pipeline ended before approval finished: Set both PipelineRun.spec.timeouts.pipeline and PipelineRun.spec.timeouts.tasks to cover the expected approval window, and ensure the approval timeout is realistic. Otherwise the run may time out even if approvers have not responded.
  • Stuck in pending even after approvals: Check status.approversResponse for users who changed their vote or rejected. You may need to update the approver list and rerun the pipeline.

User and group identifiers

Manual Approval Gate relies on your platform's identity provider to match approver names. Always use the canonical identifiers exposed by the provider rather than UI display names. For example, on Alauda Container Platform:

$ kubectl get users.auth.alauda.io
NAME                               TYPE    USERNAME   AGE
21232f297a57a5a743894a0e4a801fc3   local   admin      19d

Use the USERNAME column (such as admin) when adding user approvers.

$ kubectl get groups.auth.alauda.io
NAME      DISPLAYNAME   CONNTYPE   CONNID   AGE
g-v9mfs   test-group    local      local    19d

Use the NAME column (such as g-v9mfs) when referencing group approvers (for example, group:g-v9mfs). Other platforms expose similar resources—consult the identity service documentation for the exact field names.

Learn More