diff --git a/tekton/v1/tasks/ci/coderabbit-review-label.md b/tekton/v1/tasks/ci/coderabbit-review-label.md new file mode 100644 index 0000000000..04352a9902 --- /dev/null +++ b/tekton/v1/tasks/ci/coderabbit-review-label.md @@ -0,0 +1,74 @@ +# CodeRabbit review label + +The `coderabbit-review-label` Task reconciles `do-not-merge/request-change`. +It supplements the existing OWNERS, approved and lgtm requirements; it does not +require every commit to receive a CodeRabbit approval. + +## Policy + +- Reconcile only on CodeRabbit `pull_request_review` events with action + `submitted` or `dismissed`. Opening, reopening or pushing to a PR does not + trigger this Task; existing labels persist until a later review is processed. +- Read PR state and reviews with `gh pr view --json headRefOid,state,labels,reviews`; + gh automatically paginates reviews. Select GraphQL author login `coderabbitai`. + The webhook filter still uses REST login `coderabbitai[bot]` and type `Bot`; + `gh pr view` does not expose the review author's type. +- Order submitted decisions by submission time, using API array order to break + ties (GraphQL review IDs are opaque), ignoring COMMENTED + and PENDING reviews. Include DISMISSED as a conservative barrier: dismissal + must not revive an older approval. +- The latest CHANGES_REQUESTED adds the blocker, including when the PR has since + received new commits. +- The latest APPROVED can remove an existing blocker only when its `commit_id` + equals the current PR head SHA. Recheck the snapshot before and after removal. +- All other states preserve the existing label. Resolving a conversation alone + does not clear it. An approval followed by a push does not create a new blocker. +- The label stores the outstanding block. There is no historical-approval search + that can resurrect a resolved objection. Manual bypass is not implemented. + +Label changes use the same `gh pr edit --add-label/--remove-label` interface as +the other label Tasks in this repository. API errors fail the TaskRun. If +verification fails after deletion, the Task attempts to restore the label; +failed runs need operator attention and retry. +No PR code is checked out or executed. Event values enter the shell through +environment variables, and repository/PR identifiers are validated. + +## Rollout (deployment and event routing pending) + +1. The pilot is `ti-community-infra/configs`, restricted to PRs whose base + branch is exactly `test_ai_review`, in + `tekton/v1/triggers/triggers/env-gcp/_/github-pr-coderabbit-review-label.yaml`. + Create the branch separately if it does not exist. CodeRabbit must review PRs + targeting this non-default branch; creating or retargeting a PR alone is not + sufficient evidence that the review event chain works. +2. Deploy the registered Task, TriggerTemplate and Trigger to the same namespace. + The existing `github` secret needs PR read and issue label write permissions. + The selected release image must provide bash, gh (with `gh pr view --json` + support for the fields above and automatic review pagination), and jq. +3. Verify the authenticated webhook/EventListener route and its trigger selector. + This Trigger uses the existing `type: github-pr` label but also needs delivery + of `pull_request_review` events. EventListener definitions are not in this + change; do not assume the pull_request selector accepts review events. +4. In configs/prow/config/plugins.yaml, add `pull_request_review` to exactly the + external-plugin endpoint serving this deployment for the pilot repository + `ti-community-infra/configs`. This Task only requires `pull_request_review`; + preserve existing plugin entries and event subscriptions for other tasks. + Do not enable both tekton2-ee-cd and prow-tekton without verifying routing. +5. Sync the new label definition from configs. Every Tide query admitting a pilot + PR must exclude the exact label; bare `do-not-merge` is not a wildcard. +6. Reconcile existing open pilot PRs using the Task before relying on the gate. + Historical dismissed reviews cannot reconstruct a missing blocker; inspect + these PRs explicitly during rollout. + +## Limits + +This is an asynchronous Tide label gate, not a GitHub required check or a lock +on manual merging. Snapshot rechecks and restoration reduce stale writes but do +not serialize concurrent TaskRuns or atomically coordinate with Tide. A failed +delivery, process termination, or concurrent merge can leave a window before +the label is applied/restored. This implementation must not be described as a +zero-race merge guarantee. Retries re-read GitHub state and are safe to repeat. + +No automatic backfill, periodic repair, protected manual override, or additional +label permission policy is installed by this change. These are rollout or future +extensions, not existing functionality. diff --git a/tekton/v1/tasks/ci/coderabbit-review-label.yaml b/tekton/v1/tasks/ci/coderabbit-review-label.yaml new file mode 100644 index 0000000000..0d67c153d6 --- /dev/null +++ b/tekton/v1/tasks/ci/coderabbit-review-label.yaml @@ -0,0 +1,131 @@ +# yaml-language-server: $schema=https://github.com/redhat-developer/vscode-tekton/raw/refs/heads/main/scheme/tekton.dev/v1_Task.json +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: coderabbit-review-label +spec: + description: Block on CodeRabbit objections; only a current-head approval clears the label. + params: + - name: owner + type: string + - name: repo + type: string + - name: number + type: string + workspaces: + - name: github + description: GitHub token with pull request read and issue label write permissions. + steps: + - name: reconcile + image: ghcr.io/pingcap-qe/cd/utils/release:v2026.7.12-4-g372cd24 + env: + - name: PR_OWNER + value: $(params.owner) + - name: PR_REPO + value: $(params.repo) + - name: PR_NUMBER + value: $(params.number) + - name: TOKEN_FILE + value: $(workspaces.github.path)/token + script: | + #!/usr/bin/env bash + set -euo pipefail + export GH_TOKEN="$(cat "$TOKEN_FILE")" + [[ "$PR_OWNER" =~ ^[A-Za-z0-9-]+$ ]] + [[ "$PR_REPO" =~ ^[A-Za-z0-9_.-]+$ ]] + [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] + pr_url="https://github.com/$PR_OWNER/$PR_REPO/pull/$PR_NUMBER" + label='do-not-merge/request-change' + restore=false + + add_label() { + gh pr edit --add-label "$label" "$pr_url" >/dev/null + } + + # A failed read after deletion must not leave an unverified PR unblocked. + cleanup() { + result=$? + if [ "$restore" = true ]; then + add_label || result=1 + fi + exit "$result" + } + trap cleanup EXIT + + snapshot() { + local pr + pr=$(gh pr view "$PR_NUMBER" --repo "$PR_OWNER/$PR_REPO" \ + --json headRefOid,state,labels,reviews) || return + # GraphQL uses coderabbitai, without the REST/webhook [bot] suffix. + # Review IDs are opaque; use API order to break submission-time ties. + jq -c --arg label "$label" ' + (.reviews | to_entries | map(select( + .value.author.login == "coderabbitai" and + (.value.state == "APPROVED" or .value.state == "CHANGES_REQUESTED" or + .value.state == "DISMISSED") + )) | sort_by(.value.submittedAt, .key) | last | .value) as $review | + {head: .headRefOid, open: (.state == "OPEN"), + blocked: any(.labels[]; .name == $label), + review: ($review | if . == null then null else + {id, state, commit_id: .commit.oid, submitted_at: .submittedAt} end)}' <<< "$pr" + } + + can_clear() { + jq -e '.open and .review.state == "APPROVED" and + .review.commit_id == .head' >/dev/null <<< "$1" + } + + # Webhooks are hints. gh pr view paginates reviews to read current state. + # COMMENTED does not replace a decision. DISMISSED never revives an old + # approval. A new head alone neither creates nor clears a blocker. + for attempt in 1 2 3; do + current=$(snapshot) + if ! jq -e '.open' >/dev/null <<< "$current"; then + exit 0 + fi + state=$(jq -r '.review.state // "NONE"' <<< "$current") + if [ "$state" = CHANGES_REQUESTED ]; then + add_label + after=$(snapshot) + if [ "$(jq -c '{head, review}' <<< "$current")" != \ + "$(jq -c '{head, review}' <<< "$after")" ]; then + continue + fi + echo 'CodeRabbit requested changes; blocking Tide.' + exit 0 + fi + if ! can_clear "$current"; then + echo 'No current-head approval; preserving the existing label state.' + exit 0 + fi + if ! jq -e '.blocked' >/dev/null <<< "$current"; then + exit 0 + fi + before=$(snapshot) + if [ "$current" != "$before" ]; then + continue + fi + restore=true + delete_failed=false + if ! gh pr edit --remove-label "$label" "$pr_url" >/dev/null; then + # A concurrent TaskRun may already have removed the label. Verify + # the resulting state below instead of trusting this command alone. + delete_failed=true + fi + after=$(snapshot) + if can_clear "$after" && jq -e '.blocked | not' >/dev/null <<< "$after" && \ + [ "$(jq -c '{head, review}' <<< "$before")" = \ + "$(jq -c '{head, review}' <<< "$after")" ]; then + restore=false + echo 'Current-head CodeRabbit approval cleared the blocker.' + exit 0 + fi + add_label + restore=false + if [ "$delete_failed" = true ]; then + echo 'Failed to remove the blocker label; retained it.' >&2 + exit 1 + fi + done + echo 'PR changed during reconciliation; retained the blocker. Retry this TaskRun.' >&2 + exit 1 diff --git a/tekton/v1/tasks/kustomization.yaml b/tekton/v1/tasks/kustomization.yaml index 3dc028d09b..9d9c30f21e 100644 --- a/tekton/v1/tasks/kustomization.yaml +++ b/tekton/v1/tasks/kustomization.yaml @@ -3,6 +3,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - ci/ci-helper-for-pr.yaml + - ci/coderabbit-review-label.yaml - ci/resolve-cherrypick-conflicts.yaml - delivery/pingcap-deliver-binaries.yaml - delivery/pingcap-deliver-images.yaml diff --git a/tekton/v1/triggers/templates/_/coderabbit-review-label.yaml b/tekton/v1/triggers/templates/_/coderabbit-review-label.yaml new file mode 100644 index 0000000000..d651829925 --- /dev/null +++ b/tekton/v1/triggers/templates/_/coderabbit-review-label.yaml @@ -0,0 +1,28 @@ +apiVersion: triggers.tekton.dev/v1beta1 +kind: TriggerTemplate +metadata: + name: coderabbit-review-label +spec: + params: + - name: pr-owner + - name: pr-repo + - name: pr-number + resourcetemplates: + - apiVersion: tekton.dev/v1 + kind: TaskRun + metadata: + generateName: coderabbit-review-label- + spec: + params: + - name: owner + value: $(tt.params.pr-owner) + - name: repo + value: $(tt.params.pr-repo) + - name: number + value: $(tt.params.pr-number) + taskRef: + name: coderabbit-review-label + workspaces: + - name: github + secret: + secretName: github diff --git a/tekton/v1/triggers/templates/kustomization.yaml b/tekton/v1/triggers/templates/kustomization.yaml index be9c03afad..84c96fe226 100644 --- a/tekton/v1/triggers/templates/kustomization.yaml +++ b/tekton/v1/triggers/templates/kustomization.yaml @@ -6,6 +6,7 @@ resources: - _/build-component-linux.yaml - _/build-component-single-platform.yaml - _/ci-helper-for-pr.yaml + - _/coderabbit-review-label.yaml - _/collect-multi-arch-image.yaml # - _/push-oci-artifact-to-fileserver.yaml - pingcap/bump-placeholder-version-in-readme.yaml diff --git a/tekton/v1/triggers/triggers/env-gcp/_/github-pr-coderabbit-review-label.yaml b/tekton/v1/triggers/triggers/env-gcp/_/github-pr-coderabbit-review-label.yaml new file mode 100644 index 0000000000..618a1cd070 --- /dev/null +++ b/tekton/v1/triggers/triggers/env-gcp/_/github-pr-coderabbit-review-label.yaml @@ -0,0 +1,24 @@ +apiVersion: triggers.tekton.dev/v1beta1 +kind: Trigger +metadata: + name: github-pr-coderabbit-review-label + labels: + type: github-pr +spec: + interceptors: + - name: filter rollout repositories and review events + ref: { name: cel } + params: + - name: filter + # Pilot scope: only PRs targeting this repository and base branch. + value: >- + body.repository.full_name in ['ti-community-infra/configs'] + && body.pull_request.base.ref == 'test_ai_review' + && header.match('X-GitHub-Event', 'pull_request_review') + && body.action in ['submitted', 'dismissed'] + && body.review.user.login == 'coderabbitai[bot]' + && body.review.user.type == 'Bot' + bindings: + - ref: github-pr + template: + ref: coderabbit-review-label diff --git a/tekton/v1/triggers/triggers/env-gcp/kustomization.yaml b/tekton/v1/triggers/triggers/env-gcp/kustomization.yaml index 2f97064f43..6e4079e4c5 100644 --- a/tekton/v1/triggers/triggers/env-gcp/kustomization.yaml +++ b/tekton/v1/triggers/triggers/env-gcp/kustomization.yaml @@ -17,6 +17,7 @@ resources: - _/git-push-branch-build-failpoint.yaml - _/git-push-branch-build-ng.yaml - _/git-push-on-fips-branches.yaml + - _/github-pr-coderabbit-review-label.yaml - _/github-pr-labeled-with-lgtm.yaml - _/github-pr-labeled-with-needs-ok-to-test.yaml - _/github-pr-synchronized-remove-hold.yaml