# Codex Code Review Integration (Blocking)
#
# This workflow fetches Codex review comments and BLOCKS merge if P0/P1 issues found.
#
# How it works:
# 1. Fetches PR review threads using GraphQL (filters out resolved threads)
# 2. Fetches issue comments from users containing "codex" in login
# 3. Counts P0 (blocker) and P1 (major) severity issues from UNRESOLVED threads only
# 4. Generates summary table in GitHub Actions check summary
# 5. FAILS the check if P0 or P1 issues found (blocking merge)
#
# To request a review: Comment "@codex review" on the PR
#
# Codex severity levels:
# - P0 (Blocker): Must address before merge - BLOCKS
# - P1 (Major): Should address before merge - BLOCKS
# - P2+ (Minor): Address in follow-up if complex - Does NOT block
#
# Note: Resolved review threads are ignored - if you've addressed the issue and
# resolved the thread, it won't block the PR.
name: Codex Code Review
on:
pull_request:
types: [opened, synchronize, reopened]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
# Allow manual trigger for re-checking after fixes
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to check'
required: true
type: number
# Security: Least-privilege permissions
permissions:
contents: read
pull-requests: read
# Prevent duplicate runs but don't cancel - we want latest status
concurrency:
group: codex-review-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
cancel-in-progress: false
jobs:
codex-review:
name: Codex Review Check
runs-on: ubuntu-latest
# Only run on PRs (not issues) and for relevant comment events
if: |
github.event_name == 'pull_request' ||
github.event_name == 'pull_request_review' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request) ||
github.event_name == 'pull_request_review_comment'
outputs:
p0_count: ${{ steps.codex.outputs.p0_count }}
p1_count: ${{ steps.codex.outputs.p1_count }}
total_count: ${{ steps.codex.outputs.codex_comment_count }}
steps:
- name: Determine PR Number
id: pr
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "number=${{ github.event.inputs.pr_number }}" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" == "pull_request" ]; then
echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" == "pull_request_review" ]; then
echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
else
echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
fi
- name: Wait for Codex to process
# Give Codex time to analyze the PR (especially on synchronize events)
if: github.event_name == 'pull_request' && github.event.action == 'synchronize'
run: |
echo "Waiting 30 seconds for Codex to process new commits..."
sleep 30
- name: Fetch Codex comments
id: codex
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
PR_NUM: ${{ steps.pr.outputs.number }}
run: |
echo "Fetching Codex comments for PR #$PR_NUM..."
# Extract owner and repo name
OWNER="${REPO%/*}"
REPO_NAME="${REPO#*/}"
# Use GraphQL to fetch review threads with resolution status
# Only comments from UNRESOLVED threads are considered
echo "Fetching review threads via GraphQL (filtering out resolved threads)..."
# shellcheck disable=SC2016
# Single quotes intentional - GraphQL variables passed via -f/-F flags
GRAPHQL_RESPONSE=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 50) {
nodes {
author { login }
body
}
}
}
}
}
}
}
' -f owner="$OWNER" -f repo="$REPO_NAME" -F pr="$PR_NUM" 2>/dev/null || echo '{"data":{"repository":{"pullRequest":{"reviewThreads":{"nodes":[]}}}}}')
# Extract comments from UNRESOLVED threads only, from Codex users
UNRESOLVED_THREAD_COMMENTS=$(echo "$GRAPHQL_RESPONSE" | jq '
[.data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved == false)
| .comments.nodes[]
| select(.author.login | test("codex"; "i"))
| {body: .body}]
' 2>/dev/null || echo "[]")
RESOLVED_COUNT=$(echo "$GRAPHQL_RESPONSE" | jq '
[.data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved == true)
| .comments.nodes[]
| select(.author.login | test("codex"; "i"))]
| length
' 2>/dev/null || echo "0")
echo "Resolved Codex threads (ignored): $RESOLVED_COUNT"
# Fetch issue comments from Codex users (these don't have thread resolution)
ISSUE_COMMENTS=$(gh api \
"repos/$REPO/issues/$PR_NUM/comments" \
--jq '[.[] | select(.user.login | test("codex"; "i")) | {body: .body}]' 2>/dev/null || echo "[]")
# Fetch PR reviews from Codex users (overall review comments, not line-specific)
REVIEWS=$(gh api \
"repos/$REPO/pulls/$PR_NUM/reviews" \
--jq '[.[] | select(.user.login | test("codex"; "i")) | {body: .body}]' 2>/dev/null || echo "[]")
# Combine all comments (unresolved thread comments + issue comments + reviews)
ALL_COMMENTS=$(echo "$UNRESOLVED_THREAD_COMMENTS" "$ISSUE_COMMENTS" "$REVIEWS" | jq -s 'add | map(select(.body != null and .body != ""))')
TOTAL=$(echo "$ALL_COMMENTS" | jq 'length')
echo "codex_comment_count=$TOTAL" >> "$GITHUB_OUTPUT"
# Count P0 issues (Codex badge format or explicit P0 markers)
# Matches: ![P0 Badge], badge/P0, [P0], **P0**, explicit "P0" word
# Avoids false positives from generic words like "critical"
P0=$(echo "$ALL_COMMENTS" | jq '[.[] | select(.body | test("badge/P0|P0-red|\\[P0\\]|\\*\\*P0\\*\\*|\\bP0\\b"; "i"))] | length')
echo "p0_count=$P0" >> "$GITHUB_OUTPUT"
# Count P1 issues (Codex badge format or explicit P1 markers)
# Matches: ![P1 Badge], badge/P1, [P1], **P1**, explicit "P1" word
# Avoids false positives from phrases like "no major issues"
P1=$(echo "$ALL_COMMENTS" | jq '[.[] | select(.body | test("badge/P1|P1-orange|\\[P1\\]|\\*\\*P1\\*\\*|\\bP1\\b"; "i"))] | length')
echo "p1_count=$P1" >> "$GITHUB_OUTPUT"
# Debug output
echo "Total active Codex comments: $TOTAL"
echo "P0 (blocker) issues: $P0"
echo "P1 (major) issues: $P1"
if [ "$RESOLVED_COUNT" != "0" ]; then
echo "Note: $RESOLVED_COUNT resolved thread comment(s) were ignored"
fi
- name: Generate summary
env:
TOTAL: ${{ steps.codex.outputs.codex_comment_count }}
P0: ${{ steps.codex.outputs.p0_count }}
P1: ${{ steps.codex.outputs.p1_count }}
PR_NUM: ${{ steps.pr.outputs.number }}
run: |
{
echo "## Codex Code Review Summary"
echo ""
if [ "$TOTAL" = "0" ]; then
echo "> No Codex review comments found on this PR."
echo ""
echo "_Tip: Tag \`@codex review\` in a PR comment to request a code review._"
else
echo "| Severity | Count | Status |"
echo "|----------|-------|--------|"
# P0 status
if [ "$P0" != "0" ]; then
echo "| **P0 (Blocker)** | $P0 | :x: **BLOCKING** |"
else
echo "| P0 (Blocker) | $P0 | :white_check_mark: None |"
fi
# P1 status
if [ "$P1" != "0" ]; then
echo "| **P1 (Major)** | $P1 | :x: **BLOCKING** |"
else
echo "| P1 (Major) | $P1 | :white_check_mark: None |"
fi
echo "| Total Comments | $TOTAL | |"
echo ""
if [ "$P0" != "0" ] || [ "$P1" != "0" ]; then
echo "### :warning: How to resolve"
echo ""
echo "1. Review Codex comments on the **Conversation** and **Files changed** tabs"
echo "2. Address all P0 and P1 issues"
echo "3. Push fixes OR resolve the review thread if the issue is a false positive"
echo "4. Re-run this check via Actions tab or push new commits"
echo ""
echo "_Note: Resolved review threads are automatically ignored._"
else
echo ":white_check_mark: **No blocking issues found!** PR is ready for merge."
fi
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Fail on blocking issues
if: steps.codex.outputs.p0_count != '0' || steps.codex.outputs.p1_count != '0'
env:
P0: ${{ steps.codex.outputs.p0_count }}
P1: ${{ steps.codex.outputs.p1_count }}
run: |
echo "::error::Codex found blocking issues: $P0 P0 (blocker) and $P1 P1 (major) issues."
echo ""
echo "Please address these issues before merging."
echo "See the Codex comments on the PR for details."
exit 1