diff --git a/scripts/ci/pr_governance_gate.sh b/scripts/ci/pr_governance_gate.sh index fa19678a9..a66142ca8 100644 --- a/scripts/ci/pr_governance_gate.sh +++ b/scripts/ci/pr_governance_gate.sh @@ -282,8 +282,26 @@ THREADS_JSON="$(gh api graphql \ -F owner="$OWNER" \ -F repo="$REPO" \ -F number="$PR_NUMBER" \ - -f query='query($owner:String!, $repo:String!, $number:Int!) { repository(owner:$owner, name:$repo) { pullRequest(number:$number) { headRefOid mergeStateStatus reviewThreads(first:100) { nodes { id isResolved isOutdated } } } } }')" + -f query='query($owner:String!, $repo:String!, $number:Int!) { repository(owner:$owner, name:$repo) { pullRequest(number:$number) { headRefOid mergeStateStatus reviewThreads(first:100) { pageInfo { hasNextPage } nodes { id isResolved isOutdated comments(first:100) { pageInfo { hasNextPage } nodes { databaseId } } } } } } }')" +THREAD_METADATA_TRUNCATED="$(printf '%s' "$THREADS_JSON" | jq ' + (.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage // false) + or any( + .data.repository.pullRequest.reviewThreads.nodes[]?; + (.comments.pageInfo.hasNextPage // false) + )' +)" +if [ "$THREAD_METADATA_TRUNCATED" = "true" ]; then + add_blocker 'Review thread metadata was truncated; current resolution state could not be proven.' +fi UNRESOLVED_THREADS="$(printf '%s' "$THREADS_JSON" | jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false and .isOutdated == false)] | length')" +UNRESOLVED_REVIEW_COMMENT_IDS_JSON="$(printf '%s' "$THREADS_JSON" | jq ' + [.data.repository.pullRequest.reviewThreads.nodes[] + | select(.isResolved == false and .isOutdated == false) + | .comments.nodes[]? + | .databaseId + | select(. != null)] + | unique' +)" if [ "$UNRESOLVED_THREADS" != "0" ]; then add_blocker "${UNRESOLVED_THREADS} unresolved current review thread(s) remain." fi @@ -442,10 +460,14 @@ if ! REVIEW_COMMENTS_JSON="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls printf '%s\n' "$(<"$REVIEW_COMMENTS_ERROR_FILE")" | sed 's/^/ /' add_blocker 'PR review comments could not be read; see the workflow run log.' else - CODERABBIT_REVIEW_BLOCKERS="$(printf '%s' "$REVIEW_COMMENTS_JSON" | jq -s --arg head_sha "$HEAD_SHA" --arg pattern "$CODERABBIT_BLOCKING_PATTERN" ' + CODERABBIT_REVIEW_BLOCKERS="$(printf '%s' "$REVIEW_COMMENTS_JSON" | jq -s \ + --arg head_sha "$HEAD_SHA" \ + --arg pattern "$CODERABBIT_BLOCKING_PATTERN" \ + --argjson unresolved_comment_ids "$UNRESOLVED_REVIEW_COMMENT_IDS_JSON" ' [.[][] | select((.user.login // "") | test("'"$REVIEW_BOT_LOGIN_PATTERN"'"; "i")) | select((.body // "") | test($pattern; "i")) + | select(.id as $comment_id | ($unresolved_comment_ids | index($comment_id)) != null) | select(((.commit_id // "") == $head_sha) or ((.original_commit_id // "") == $head_sha) or ((.body // "") | contains($head_sha)))] | length' )" diff --git a/scripts/ci/test_pr_governance_gate.sh b/scripts/ci/test_pr_governance_gate.sh index ce15243b3..8fb42aa9b 100644 --- a/scripts/ci/test_pr_governance_gate.sh +++ b/scripts/ci/test_pr_governance_gate.sh @@ -76,6 +76,22 @@ if [ "$1" = "api" ] && [ "$2" = "graphql" ]; then printf 'GraphQL request failed\n' >&2 exit 1 fi + if [ "${GH_SCENARIO:-pass}" = "coderabbit_resolved_current_review_comment" ]; then + printf '{"data":{"repository":{"pullRequest":{"headRefOid":"%s","mergeStateStatus":"CLEAN","reviewThreads":{"nodes":[{"id":"thread-888","isResolved":true,"isOutdated":false,"comments":{"nodes":[{"databaseId":888}]}}]}}}}}' "$head_sha" + exit 0 + fi + if [ "${GH_SCENARIO:-pass}" = "coderabbit_current_review_comment" ] || [ "${GH_SCENARIO:-pass}" = "github_code_quality_current_review_comment" ]; then + printf '{"data":{"repository":{"pullRequest":{"headRefOid":"%s","mergeStateStatus":"CLEAN","reviewThreads":{"nodes":[{"id":"thread-888","isResolved":false,"isOutdated":false,"comments":{"nodes":[{"databaseId":888}]}}]}}}}}' "$head_sha" + exit 0 + fi + if [ "${GH_SCENARIO:-pass}" = "review_threads_truncated" ]; then + printf '{"data":{"repository":{"pullRequest":{"headRefOid":"%s","mergeStateStatus":"CLEAN","reviewThreads":{"pageInfo":{"hasNextPage":true},"nodes":[]}}}}}' "$head_sha" + exit 0 + fi + if [ "${GH_SCENARIO:-pass}" = "review_thread_comments_truncated" ]; then + printf '{"data":{"repository":{"pullRequest":{"headRefOid":"%s","mergeStateStatus":"CLEAN","reviewThreads":{"pageInfo":{"hasNextPage":false},"nodes":[{"id":"thread-888","isResolved":false,"isOutdated":false,"comments":{"pageInfo":{"hasNextPage":true},"nodes":[{"databaseId":888}]}}]}}}}}' "$head_sha" + exit 0 + fi if [ "${GH_SCENARIO:-pass}" = "persistent_unknown" ]; then printf '{"data":{"repository":{"pullRequest":{"headRefOid":"%s","mergeStateStatus":"UNKNOWN","reviewThreads":{"nodes":[]}}}}}' "$head_sha" exit 0 @@ -248,7 +264,7 @@ fi if [ "$1" = "api" ] && [[ "$args" == *repos/*/pulls/42/comments* ]]; then case "${GH_SCENARIO:-pass}" in - coderabbit_current_review_comment) + coderabbit_current_review_comment|coderabbit_resolved_current_review_comment) printf '[{"id":888,"user":{"login":"coderabbitai[bot]"},"commit_id":"0123456789abcdef0123456789abcdef01234567","original_commit_id":"old","created_at":"2026-05-19T00:01:00Z","body":"Potential issue on current head"}]' ;; github_code_quality_current_review_comment) @@ -757,6 +773,40 @@ assert_coderabbit_current_review_comment_blocks() { assert_not_in_file '^pr merge' "$temp_dir/gh.log" } +assert_coderabbit_resolved_current_review_comment_does_not_block() { + local temp_dir + temp_dir="$(mktemp -d)" + run_gate coderabbit_resolved_current_review_comment "$temp_dir" + + assert_exit_code 0 "$temp_dir" + assert_in_file 'PR governance metadata gate is ready' "$temp_dir/output.txt" + assert_not_in_file 'Current-head CodeRabbit review comment' "$temp_dir/output.txt" + assert_not_in_file 'Current-head CodeRabbit review comment' "$temp_dir/gh.log" + assert_not_in_file '^pr merge' "$temp_dir/gh.log" +} + +assert_truncated_review_thread_metadata_blocks() { + local temp_dir + temp_dir="$(mktemp -d)" + run_gate review_threads_truncated "$temp_dir" + + assert_exit_code 0 "$temp_dir" + assert_in_file 'Review thread metadata was truncated; current resolution state could not be proven.' "$temp_dir/gh.log" + assert_in_file '' "$temp_dir/gh.log" + assert_not_in_file '^pr merge' "$temp_dir/gh.log" +} + +assert_truncated_review_thread_comments_metadata_blocks() { + local temp_dir + temp_dir="$(mktemp -d)" + run_gate review_thread_comments_truncated "$temp_dir" + + assert_exit_code 0 "$temp_dir" + assert_in_file 'Review thread metadata was truncated; current resolution state could not be proven.' "$temp_dir/gh.log" + assert_in_file '' "$temp_dir/gh.log" + assert_not_in_file '^pr merge' "$temp_dir/gh.log" +} + assert_github_code_quality_current_review_comment_blocks() { local temp_dir temp_dir="$(mktemp -d)" @@ -900,6 +950,9 @@ assert_coderabbit_review_limit_issue_comment_does_not_block assert_coderabbit_no_actionable_summary_does_not_block assert_coderabbit_no_actionable_summary_with_blocker_still_blocks assert_coderabbit_current_review_comment_blocks +assert_coderabbit_resolved_current_review_comment_does_not_block +assert_truncated_review_thread_metadata_blocks +assert_truncated_review_thread_comments_metadata_blocks assert_github_code_quality_current_review_comment_blocks assert_coderabbit_stale_review_comment_does_not_block assert_changes_requested_creates_marker_comment