diff --git a/.github/workflows/pr-codex-review.yml b/.github/workflows/pr-codex-review.yml index 99d3790..8a8c65e 100644 --- a/.github/workflows/pr-codex-review.yml +++ b/.github/workflows/pr-codex-review.yml @@ -4,6 +4,7 @@ on: pull_request: types: [opened, reopened, ready_for_review, synchronize] + permissions: contents: read pull-requests: write @@ -19,6 +20,7 @@ jobs: | All files | No actionable feedback generated | None | info | AZURE_OPENAI_BASE_URL: ${{ secrets.AZURE_OPENAI_BASE_URL }} AZURE_OPENAI_MODEL: ${{ secrets.AZURE_OPENAI_MODEL || 'gpt-5-codex' }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -43,13 +45,11 @@ jobs: - name: Prepare fallback review if: steps.diff.outputs.no_changes == 'true' run: | - printf '%s' "$REVIEW_TABLE_FALLBACK" > codex_review.md - cp codex_review.md review_result.md + printf '%s' "$REVIEW_TABLE_FALLBACK" > raw_review.md + cp raw_review.md review_result.md - name: Install Codex CLI if: steps.diff.outputs.no_changes == 'false' - env: - AZURE_OPENAI_BASE_URL: ${{ secrets.AZURE_OPENAI_BASE_URL }} run: | sudo apt-get update sudo apt-get install -y --no-install-recommends curl ca-certificates git @@ -75,8 +75,6 @@ jobs: - name: Run Codex code review if: steps.diff.outputs.no_changes == 'false' - env: - AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} run: | if [ -z "$AZURE_OPENAI_API_KEY" ]; then echo "AZURE_OPENAI_API_KEY secret is not configured." >&2 @@ -97,16 +95,14 @@ jobs: printf '\n\nRaw Codex Output:\n%s\n' "$(cat codex_raw.txt)" - sed -E 's/\x1B\[[0-9;]*[A-Za-z]//g' codex_raw.txt | tr -d '\r' > codex_review.md + sed -E 's/\x1B\[[0-9;]*[A-Za-z]//g' codex_raw.txt | tr -d '\r' > raw_review.md - if ! grep -q '|' codex_review.md; then - printf '%s\n' "$REVIEW_TABLE_FALLBACK" > codex_review.md + if ! grep -q '|' raw_review.md; then + printf '%s\n' "$REVIEW_TABLE_FALLBACK" > raw_review.md fi - name: Normalize review table with Azure OpenAI - if: steps.diff.outputs.no_changes == 'false' - env: - AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} + if: steps.diff.outputs.no_changes == 'false' run: | python3 -m pip install --quiet --upgrade pip 'openai>=1.45.0' python3 scripts/normalize_review_result.py @@ -117,13 +113,16 @@ jobs: run: | if [ -f review_result.md ]; then BODY_FILE=review_result.md - elif [ -f codex_review.md ]; then - BODY_FILE=codex_review.md + elif [ -f raw_review.md ]; then + BODY_FILE=raw_review.md else printf '%s' "$REVIEW_TABLE_FALLBACK" > review_result.md BODY_FILE=review_result.md fi + # Append attribution footer to the comment body + printf '\n\nReviewed by Codex\n' >> "$BODY_FILE" + gh pr comment ${{ github.event.pull_request.number }} --body-file "$BODY_FILE" - name: Upload Codex raw output @@ -133,7 +132,7 @@ jobs: name: codex-review-logs path: | codex_raw.txt - codex_review.md + raw_review.md review_result.md diff.patch changed_files.txt diff --git a/.github/workflows/pr-copilot-review.yml b/.github/workflows/pr-copilot-review.yml new file mode 100644 index 0000000..a57161f --- /dev/null +++ b/.github/workflows/pr-copilot-review.yml @@ -0,0 +1,128 @@ +name: PR Copilot Review + +on: + pull_request: + types: [opened, reopened, ready_for_review, synchronize] + + +permissions: + contents: read + pull-requests: write + +jobs: + copilot_pr_review: + name: Copilot PR Review + runs-on: ubuntu-latest + env: + REVIEW_TABLE_FALLBACK: | + | File | Concern | Recommendation | Severity | + | --- | --- | --- | --- | + | All files | No actionable feedback generated | None | info | + AZURE_OPENAI_BASE_URL: ${{ secrets.AZURE_OPENAI_BASE_URL }} + AZURE_OPENAI_MODEL: ${{ secrets.AZURE_OPENAI_MODEL || 'gpt-5-codex' }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Compute diff and changed files + id: diff + run: | + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + + git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed_files.txt + git diff --unified=3 "$BASE_SHA" "$HEAD_SHA" > diff.patch + + if [ ! -s changed_files.txt ]; then + echo "no_changes=true" >> "$GITHUB_OUTPUT" + else + echo "no_changes=false" >> "$GITHUB_OUTPUT" + fi + + - name: Prepare fallback review + if: steps.diff.outputs.no_changes == 'true' + run: | + printf '%s' "$REVIEW_TABLE_FALLBACK" > raw_review.md + cp raw_review.md review_result.md + + - name: Install Copilot CLI + env: + GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_PAT }} + if: steps.diff.outputs.no_changes == 'false' + run: | + npm install -g @github/copilot + copilot --version + + - name: Run Copilot code review + env: + GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_PAT }} + if: steps.diff.outputs.no_changes == 'false' + run: | + if [ -z "$AZURE_OPENAI_API_KEY" ]; then + echo "AZURE_OPENAI_API_KEY secret is not configured." >&2 + exit 1 + fi + + HEADER="You are an expert software engineer performing a strict code review for the provided pull request diff." + RULES="Rules:\n- Output ONLY a GitHub-flavored Markdown table with exactly these columns: File | Concern | Recommendation | Severity.\n- Every row must reference a real file path from the Changed files list.\n- Severity must be one of: info, minor, major, critical.\n- If no issues are found, return a single table row with 'All files' in the File column and 'No issues found' in the Concern column.\n- Cite line numbers from the diff using the format L.\n- Do not wrap the table in backticks or add any prose before or after the table.\n- Focus on actionable feedback specific to the diff." + + CHANGED_FILES_SECTION="Changed files:\n$(cat changed_files.txt)" + DIFF_SECTION="Unified diff:\n$(cat diff.patch)" + + export COPILOT_PROMPT="$HEADER\n\n$RULES\n\n$CHANGED_FILES_SECTION\n\n$DIFF_SECTION" + + printf '%s\n' "$COPILOT_PROMPT" + + copilot -p "$COPILOT_PROMPT" | tee copilot_raw.txt >/dev/null + + printf '\n\nRaw Copilot Output:\n%s\n' "$(cat copilot_raw.txt)" + + sed -E 's/\x1B\[[0-9;]*[A-Za-z]//g' copilot_raw.txt | tr -d '\r' > raw_review.md + + if ! grep -q '|' raw_review.md; then + printf '%s\n' "$REVIEW_TABLE_FALLBACK" > raw_review.md + fi + + - name: Normalize review table with Azure OpenAI + if: steps.diff.outputs.no_changes == 'false' + run: | + python3 -m pip install --quiet --upgrade pip 'openai>=1.45.0' + python3 scripts/normalize_review_result.py + + - name: Post review as PR comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ -f review_result.md ]; then + BODY_FILE=review_result.md + elif [ -f raw_review.md ]; then + BODY_FILE=raw_review.md + else + printf '%s' "$REVIEW_TABLE_FALLBACK" > review_result.md + BODY_FILE=review_result.md + fi + + # Append attribution footer to the comment body + printf '\n\nReviewed by GitHub Copilot CLI\n' >> "$BODY_FILE" + + gh pr comment ${{ github.event.pull_request.number }} --body-file "$BODY_FILE" + + - name: Upload Copilot raw output + if: steps.diff.outputs.no_changes == 'false' + uses: actions/upload-artifact@v4 + with: + name: copilot-review-logs + path: | + copilot_raw.txt + raw_review.md + review_result.md + diff.patch + changed_files.txt diff --git a/scripts/normalize_review_result.py b/scripts/normalize_review_result.py index a5299ec..b6a5aa6 100644 --- a/scripts/normalize_review_result.py +++ b/scripts/normalize_review_result.py @@ -39,9 +39,9 @@ def require_env(name: str) -> str: def main() -> None: - source_path = Path("codex_review.md") + source_path = Path("raw_review.md") if not source_path.exists(): - print("codex_review.md not found; cannot repair review output.", file=sys.stderr) + print("raw_review.md not found; cannot repair review output.", file=sys.stderr) raise SystemExit(1) source_text = source_path.read_text(encoding="utf-8")