mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
- Replace 3 duplicate review jobs with 1 matrix job (-200 lines) Matrix entries: Security, Quality, CCS Compliance (run in parallel) - Restore review-prompt.md as orchestrator merge prompt (dedup, severity rank, unified assessment, security+CCS tables) - Aggregate job now runs claude-code-action with orchestrator prompt to produce polished unified review (not just stapled sections) - Fallback to simple concat if orchestrator fails - Dynamic prompt access via outputs[matrix.prompt_output] syntax
569 lines
23 KiB
YAML
569 lines
23 KiB
YAML
# AI Code Review Workflow
|
|
# Uses anthropics/claude-code-action with GLM routing via GitHub App tokens
|
|
# Same-repo PRs stay on the self-hosted cliproxy runner; external PRs use ubuntu-latest
|
|
#
|
|
# Triggers:
|
|
# - Automatically when PR is opened, receives new commits, or is reopened
|
|
# - Manually via /review comment on PR
|
|
# - Manually via workflow_dispatch
|
|
#
|
|
# Pipeline:
|
|
# prepare → load-prompts → review (matrix: security, quality, ccs) → aggregate (orchestrator merge + publish)
|
|
#
|
|
# Note: Concurrency group cancels in-progress reviews when new commits arrive.
|
|
# This prevents wasting resources on outdated code reviews.
|
|
|
|
name: AI Code Review
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [opened, synchronize, reopened]
|
|
issue_comment:
|
|
types: [created]
|
|
workflow_dispatch:
|
|
inputs:
|
|
pr_number:
|
|
description: 'PR number to review'
|
|
required: true
|
|
type: string
|
|
|
|
# Smart concurrency: Prevents self-cancellation when non-command comments arrive
|
|
#
|
|
# Problem: When a bot or maintainer posts a normal PR note, GitHub fires an
|
|
# issue_comment event. A loose substring match on "/review" can accidentally
|
|
# treat text like "CI/review" as a manual command, which joins the PR
|
|
# concurrency group and cancels the in-progress review.
|
|
#
|
|
# Solution: Non-actionable triggers (bot comments, comments without /review) get a
|
|
# unique per-run group, while legitimate triggers share the PR-based group for proper
|
|
# cancellation of outdated reviews.
|
|
concurrency:
|
|
group: >-
|
|
ai-review-${{
|
|
github.event_name == 'issue_comment' && (
|
|
github.event.comment.user.type == 'Bot' ||
|
|
!startsWith(github.event.comment.body, '/review')
|
|
) && format('skip-{0}', github.run_id) ||
|
|
github.event.pull_request.number ||
|
|
github.event.issue.number ||
|
|
github.event.inputs.pr_number
|
|
}}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
prepare:
|
|
name: Resolve review target
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
issues: read
|
|
outputs:
|
|
pr_number: ${{ steps.context.outputs.pr_number }}
|
|
head_ref: ${{ steps.context.outputs.head_ref }}
|
|
head_sha: ${{ steps.context.outputs.head_sha }}
|
|
head_repo: ${{ steps.context.outputs.head_repo }}
|
|
author_login: ${{ steps.context.outputs.author_login }}
|
|
author_association: ${{ steps.context.outputs.author_association }}
|
|
contributor_source: ${{ steps.context.outputs.contributor_source }}
|
|
runs_on: ${{ steps.context.outputs.runs_on }}
|
|
|
|
# Conditions:
|
|
# - PR event: on opened, synchronize (new commits), or reopened
|
|
# - Comment event: only if it's a PR, starts with /review, and NOT from a bot
|
|
if: >
|
|
github.event_name == 'pull_request_target' ||
|
|
github.event_name == 'workflow_dispatch' ||
|
|
(github.event_name == 'issue_comment' &&
|
|
github.event.issue.pull_request &&
|
|
startsWith(github.event.comment.body, '/review') &&
|
|
github.event.comment.user.type != 'Bot' &&
|
|
contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.comment.author_association))
|
|
|
|
steps:
|
|
- name: Resolve PR metadata and runner
|
|
id: context
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPOSITORY: ${{ github.repository }}
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }}
|
|
run: |
|
|
case "$EVENT_NAME" in
|
|
pull_request_target) PR_NUM="$EVENT_PR_NUMBER" ;;
|
|
issue_comment) PR_NUM="$EVENT_ISSUE_NUMBER" ;;
|
|
workflow_dispatch) PR_NUM="$INPUT_PR_NUMBER" ;;
|
|
*)
|
|
echo "Unsupported event: $EVENT_NAME" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
PR_JSON="$(gh api "repos/$REPOSITORY/pulls/$PR_NUM")"
|
|
HEAD_REPO="$(jq -r '.head.repo.full_name' <<<"$PR_JSON")"
|
|
HEAD_REF="$(jq -r '.head.ref' <<<"$PR_JSON")"
|
|
HEAD_SHA="$(jq -r '.head.sha' <<<"$PR_JSON")"
|
|
AUTHOR_LOGIN="$(jq -r '.user.login' <<<"$PR_JSON")"
|
|
AUTHOR_ASSOCIATION="$(jq -r '.author_association' <<<"$PR_JSON")"
|
|
|
|
if [ "$HEAD_REPO" = "$REPOSITORY" ]; then
|
|
CONTRIBUTOR_SOURCE="internal"
|
|
RUNS_ON='["self-hosted","cliproxy"]'
|
|
else
|
|
CONTRIBUTOR_SOURCE="external"
|
|
RUNS_ON='["ubuntu-latest"]'
|
|
fi
|
|
|
|
{
|
|
echo "pr_number=$PR_NUM"
|
|
echo "head_ref=$HEAD_REF"
|
|
echo "head_sha=$HEAD_SHA"
|
|
echo "head_repo=$HEAD_REPO"
|
|
echo "author_login=$AUTHOR_LOGIN"
|
|
echo "author_association=$AUTHOR_ASSOCIATION"
|
|
echo "contributor_source=$CONTRIBUTOR_SOURCE"
|
|
echo "runs_on=$RUNS_ON"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
load-prompts:
|
|
name: Load review prompts
|
|
needs: prepare
|
|
if: needs.prepare.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
security_prompt: ${{ steps.prompts.outputs.security_prompt }}
|
|
quality_prompt: ${{ steps.prompts.outputs.quality_prompt }}
|
|
ccs_prompt: ${{ steps.prompts.outputs.ccs_prompt }}
|
|
base_ref: ${{ steps.prompts.outputs.base_ref }}
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Load prompts from base branch
|
|
id: prompts
|
|
env:
|
|
BASE_REF: ${{ github.base_ref || 'dev' }}
|
|
run: |
|
|
# Always load prompts from base branch to prevent PR-controlled prompt injection.
|
|
# External PRs could modify review prompts to suppress security findings.
|
|
git fetch origin "$BASE_REF" --depth=1 2>/dev/null || true
|
|
|
|
SECURITY_PROMPT=$(git show "origin/${BASE_REF}:.github/review-prompts/security.md" 2>/dev/null || echo "")
|
|
if [ -z "$SECURITY_PROMPT" ]; then
|
|
echo "::warning::security.md not found on base branch ${BASE_REF} — using inline fallback"
|
|
SECURITY_PROMPT="You are a security reviewer. Check the diff for injection vulnerabilities, auth bypasses, race conditions, secrets exposure, and supply chain risks. Report findings as: #### [HIGH|MEDIUM|LOW] [SECURITY] file:line"
|
|
fi
|
|
|
|
QUALITY_PROMPT=$(git show "origin/${BASE_REF}:.github/review-prompts/quality.md" 2>/dev/null || echo "")
|
|
if [ -z "$QUALITY_PROMPT" ]; then
|
|
echo "::warning::quality.md not found on base branch ${BASE_REF} — using inline fallback"
|
|
QUALITY_PROMPT="You are a code quality reviewer. Check for error handling gaps, false assumptions, performance issues, dead code, and test gaps. Report findings as: #### [HIGH|MEDIUM|LOW] [QUALITY] file:line"
|
|
fi
|
|
|
|
CCS_PROMPT=$(git show "origin/${BASE_REF}:.github/review-prompts/ccs-compliance.md" 2>/dev/null || echo "")
|
|
if [ -z "$CCS_PROMPT" ]; then
|
|
echo "::warning::ccs-compliance.md not found on base branch ${BASE_REF} — using inline fallback"
|
|
CCS_PROMPT="You are a CCS compliance reviewer. Check for: no emojis in CLI output, getCcsDir() usage, cross-platform parity, --help updates, string-only settings, conventional commits. Report findings as: #### [HIGH|MEDIUM|LOW] [CCS] file:line"
|
|
fi
|
|
|
|
SECURITY_DELIM="SECURITY_$(openssl rand -hex 16)"
|
|
echo "security_prompt<<${SECURITY_DELIM}" >> "$GITHUB_OUTPUT"
|
|
printf '%s\n' "$SECURITY_PROMPT" >> "$GITHUB_OUTPUT"
|
|
echo "${SECURITY_DELIM}" >> "$GITHUB_OUTPUT"
|
|
|
|
QUALITY_DELIM="QUALITY_$(openssl rand -hex 16)"
|
|
echo "quality_prompt<<${QUALITY_DELIM}" >> "$GITHUB_OUTPUT"
|
|
printf '%s\n' "$QUALITY_PROMPT" >> "$GITHUB_OUTPUT"
|
|
echo "${QUALITY_DELIM}" >> "$GITHUB_OUTPUT"
|
|
|
|
CCS_DELIM="CCS_$(openssl rand -hex 16)"
|
|
echo "ccs_prompt<<${CCS_DELIM}" >> "$GITHUB_OUTPUT"
|
|
printf '%s\n' "$CCS_PROMPT" >> "$GITHUB_OUTPUT"
|
|
echo "${CCS_DELIM}" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "base_ref=$BASE_REF" >> "$GITHUB_OUTPUT"
|
|
|
|
review:
|
|
name: "${{ matrix.name }} Review"
|
|
needs: [prepare, load-prompts]
|
|
if: needs.prepare.result == 'success'
|
|
timeout-minutes: 10
|
|
runs-on: ${{ fromJSON(needs.prepare.outputs.runs_on) }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- name: Security
|
|
prompt_output: security_prompt
|
|
output_file: security_review.md
|
|
artifact_prefix: security
|
|
- name: Quality
|
|
prompt_output: quality_prompt
|
|
output_file: quality_review.md
|
|
artifact_prefix: quality
|
|
- name: CCS Compliance
|
|
prompt_output: ccs_prompt
|
|
output_file: ccs_review.md
|
|
artifact_prefix: ccs
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
issues: read
|
|
env:
|
|
ANTHROPIC_BASE_URL: https://api.z.ai/api/anthropic
|
|
REVIEW_MODEL: glm-5.1
|
|
ANTHROPIC_AUTH_TOKEN: ${{ secrets.GLM_API_KEY }}
|
|
ANTHROPIC_MODEL: glm-5.1
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: glm-5.1
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: glm-5.1
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: GLM-4.7-FlashX
|
|
DISABLE_BUG_COMMAND: '1'
|
|
DISABLE_ERROR_REPORTING: '1'
|
|
DISABLE_TELEMETRY: '1'
|
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS: '32000'
|
|
MAX_THINKING_TOKENS: '8000'
|
|
REVIEW_OUTPUT_FILE: ${{ matrix.output_file }}
|
|
|
|
steps:
|
|
- name: Prepare isolated Claude runtime
|
|
run: |
|
|
REVIEW_HOME="$RUNNER_TEMP/claude-home"
|
|
mkdir -p "$REVIEW_HOME" "$RUNNER_TEMP/xdg-config" "$RUNNER_TEMP/xdg-cache" "$RUNNER_TEMP/xdg-state"
|
|
{
|
|
echo "HOME=$REVIEW_HOME"
|
|
echo "XDG_CONFIG_HOME=$RUNNER_TEMP/xdg-config"
|
|
echo "XDG_CACHE_HOME=$RUNNER_TEMP/xdg-cache"
|
|
echo "XDG_STATE_HOME=$RUNNER_TEMP/xdg-state"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Checkout PR code
|
|
run: |
|
|
git fetch origin "refs/pull/${{ needs.prepare.outputs.pr_number }}/head"
|
|
git checkout --force FETCH_HEAD
|
|
|
|
- name: "Run ${{ matrix.name }} Review"
|
|
id: claude-review
|
|
uses: anthropics/claude-code-action@v1
|
|
with:
|
|
anthropic_api_key: ${{ secrets.GLM_API_KEY }}
|
|
github_token: ${{ github.token }}
|
|
show_full_output: true
|
|
track_progress: false
|
|
prompt: |
|
|
think
|
|
|
|
You are a ${{ matrix.name }}-focused code reviewer for PR #${{ needs.prepare.outputs.pr_number }} in ${{ github.repository }}.
|
|
PR HEAD SHA: ${{ needs.prepare.outputs.head_sha }}
|
|
${{ needs.prepare.outputs.contributor_source == 'external' && 'EXTERNAL PR: Apply maximum scrutiny.' || '' }}
|
|
|
|
Follow the repository CLAUDE.md for project-specific guidelines.
|
|
|
|
${{ needs.load-prompts.outputs[matrix.prompt_output] }}
|
|
|
|
## IMPORTANT: Writing the Review
|
|
After completing your analysis, use the `Write` tool to write your findings to `${{ env.REVIEW_OUTPUT_FILE }}`.
|
|
Use `Write` tool directly — do NOT use `Edit`.
|
|
Do NOT post GitHub comments. Do NOT modify source code.
|
|
IMPORTANT: Do NOT use shell operators (|| &&) or heredoc (<<) in bash commands.
|
|
|
|
claude_args: |
|
|
--bare
|
|
--model ${{ env.REVIEW_MODEL }}
|
|
--permission-mode bypassPermissions
|
|
--max-turns 25
|
|
|
|
- name: Fallback extraction
|
|
if: always() && steps.claude-review.outcome != 'cancelled'
|
|
run: |
|
|
if [ -s "$REVIEW_OUTPUT_FILE" ]; then exit 0; fi
|
|
EXEC_LOG="$RUNNER_TEMP/claude-execution-output.json"
|
|
if [ ! -f "$EXEC_LOG" ]; then echo "${{ matrix.name }} review not available." > "$REVIEW_OUTPUT_FILE"; exit 0; fi
|
|
EXTRACTED=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // empty' "$EXEC_LOG" 2>/dev/null || true)
|
|
if [ -n "$EXTRACTED" ]; then
|
|
printf '%s\n' "$EXTRACTED" > "$REVIEW_OUTPUT_FILE"
|
|
else
|
|
echo "${{ matrix.name }} review produced no output." > "$REVIEW_OUTPUT_FILE"
|
|
fi
|
|
|
|
- name: Upload review output
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ matrix.artifact_prefix }}-review-${{ needs.prepare.outputs.pr_number }}-run${{ github.run_id }}
|
|
path: ${{ env.REVIEW_OUTPUT_FILE }}
|
|
retention-days: 3
|
|
if-no-files-found: warn
|
|
|
|
- name: Upload execution log
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ matrix.artifact_prefix }}-exec-log-pr${{ needs.prepare.outputs.pr_number }}-run${{ github.run_id }}
|
|
path: ${{ runner.temp }}/claude-execution-output.json
|
|
retention-days: 7
|
|
if-no-files-found: ignore
|
|
|
|
aggregate:
|
|
name: Merge & Publish Review
|
|
needs: [prepare, load-prompts, review]
|
|
if: always() && needs.prepare.result == 'success'
|
|
timeout-minutes: 10
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
issues: write
|
|
env:
|
|
ANTHROPIC_BASE_URL: https://api.z.ai/api/anthropic
|
|
REVIEW_MODEL: glm-5.1
|
|
ANTHROPIC_AUTH_TOKEN: ${{ secrets.GLM_API_KEY }}
|
|
ANTHROPIC_MODEL: glm-5.1
|
|
ANTHROPIC_DEFAULT_OPUS_MODEL: glm-5.1
|
|
ANTHROPIC_DEFAULT_SONNET_MODEL: glm-5.1
|
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: GLM-4.7-FlashX
|
|
DISABLE_BUG_COMMAND: '1'
|
|
DISABLE_ERROR_REPORTING: '1'
|
|
DISABLE_TELEMETRY: '1'
|
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS: '64000'
|
|
MAX_THINKING_TOKENS: '16000'
|
|
REVIEW_OUTPUT_FILE: merged_review.md
|
|
REVIEW_COMMENT_FILE: .ccs-ai-review-comment.md
|
|
|
|
steps:
|
|
- name: Generate App Token
|
|
id: app-token
|
|
uses: actions/create-github-app-token@v1
|
|
with:
|
|
app-id: ${{ secrets.CCS_REVIEWER_APP_ID }}
|
|
private-key: ${{ secrets.CCS_REVIEWER_PRIVATE_KEY }}
|
|
|
|
- name: Add eyes reaction to /review comment
|
|
if: github.event_name == 'issue_comment'
|
|
run: |
|
|
gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
|
|
--method POST -f content=eyes
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
|
|
- name: Download all review artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: "*-review-${{ needs.prepare.outputs.pr_number }}-run${{ github.run_id }}"
|
|
merge-multiple: true
|
|
continue-on-error: true
|
|
|
|
- name: Prepare isolated Claude runtime
|
|
run: |
|
|
REVIEW_HOME="$RUNNER_TEMP/claude-home"
|
|
mkdir -p "$REVIEW_HOME" "$RUNNER_TEMP/xdg-config" "$RUNNER_TEMP/xdg-cache" "$RUNNER_TEMP/xdg-state"
|
|
rm -f "$REVIEW_OUTPUT_FILE" "$REVIEW_COMMENT_FILE"
|
|
{
|
|
echo "HOME=$REVIEW_HOME"
|
|
echo "XDG_CONFIG_HOME=$RUNNER_TEMP/xdg-config"
|
|
echo "XDG_CACHE_HOME=$RUNNER_TEMP/xdg-cache"
|
|
echo "XDG_STATE_HOME=$RUNNER_TEMP/xdg-state"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Checkout PR code
|
|
run: |
|
|
git fetch origin "refs/pull/${{ needs.prepare.outputs.pr_number }}/head"
|
|
git checkout --force FETCH_HEAD
|
|
|
|
- name: Load orchestrator prompt
|
|
id: orchestrator
|
|
env:
|
|
BASE_REF: ${{ github.base_ref || 'dev' }}
|
|
run: |
|
|
git fetch origin "$BASE_REF" --depth=1 2>/dev/null || true
|
|
ORCH_PROMPT=$(git show "origin/${BASE_REF}:.github/review-prompt.md" 2>/dev/null || echo "")
|
|
if [ -z "$ORCH_PROMPT" ]; then
|
|
echo "::warning::review-prompt.md not found — using fallback merge"
|
|
ORCH_PROMPT="Merge the 3 review outputs below into a single unified review. Deduplicate findings by file:line (highest severity wins). Produce a final assessment: APPROVED, APPROVED WITH NOTES, or CHANGES REQUESTED."
|
|
fi
|
|
DELIM="ORCH_$(openssl rand -hex 16)"
|
|
echo "prompt<<${DELIM}" >> "$GITHUB_OUTPUT"
|
|
printf '%s\n' "$ORCH_PROMPT" >> "$GITHUB_OUTPUT"
|
|
echo "${DELIM}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Prepare review inputs
|
|
id: review-inputs
|
|
run: |
|
|
SECURITY=$(cat security_review.md 2>/dev/null || echo "Security review not available.")
|
|
QUALITY=$(cat quality_review.md 2>/dev/null || echo "Quality review not available.")
|
|
CCS=$(cat ccs_review.md 2>/dev/null || echo "CCS compliance review not available.")
|
|
|
|
# Write combined input file for orchestrator
|
|
{
|
|
echo "## Security Review Output"
|
|
echo ""
|
|
printf '%s\n' "$SECURITY"
|
|
echo ""
|
|
echo "---"
|
|
echo ""
|
|
echo "## Quality Review Output"
|
|
echo ""
|
|
printf '%s\n' "$QUALITY"
|
|
echo ""
|
|
echo "---"
|
|
echo ""
|
|
echo "## CCS Compliance Review Output"
|
|
echo ""
|
|
printf '%s\n' "$CCS"
|
|
} > review_inputs.md
|
|
|
|
- name: Run Orchestrator Merge
|
|
id: claude-merge
|
|
uses: anthropics/claude-code-action@v1
|
|
with:
|
|
anthropic_api_key: ${{ secrets.GLM_API_KEY }}
|
|
github_token: ${{ steps.app-token.outputs.token }}
|
|
show_full_output: true
|
|
track_progress: false
|
|
prompt: |
|
|
think
|
|
|
|
You are the review orchestrator for PR #${{ needs.prepare.outputs.pr_number }} in ${{ github.repository }}.
|
|
PR HEAD SHA: ${{ needs.prepare.outputs.head_sha }}
|
|
CONTRIBUTOR: @${{ needs.prepare.outputs.author_login }}
|
|
${{ needs.prepare.outputs.contributor_source == 'external' && 'EXTERNAL CONTRIBUTOR PR.' || 'INTERNAL PR.' }}
|
|
|
|
${{ steps.orchestrator.outputs.prompt }}
|
|
|
|
## Review Inputs from 3 Parallel Reviewers
|
|
|
|
Read the file `review_inputs.md` for the raw outputs from all 3 reviewers (security, quality, CCS compliance).
|
|
|
|
## IMPORTANT: Writing the Final Review
|
|
After merging and assessing, use the `Write` tool to write the final unified review to `${{ env.REVIEW_OUTPUT_FILE }}`.
|
|
Use `Write` tool directly — do NOT use `Edit`.
|
|
Do NOT post GitHub comments yourself. The workflow will publish the saved file.
|
|
Do NOT modify any source code files.
|
|
IMPORTANT: Do NOT use shell operators (|| &&) or heredoc (<<) in bash commands.
|
|
|
|
End your review with:
|
|
> Parallel review by `${{ env.REVIEW_MODEL }}` (3 focused reviewers + orchestrator merge)
|
|
|
|
claude_args: |
|
|
--bare
|
|
--model ${{ env.REVIEW_MODEL }}
|
|
--permission-mode bypassPermissions
|
|
--max-turns 15
|
|
|
|
- name: Fallback merge (if orchestrator fails)
|
|
if: always() && steps.claude-merge.outcome != 'cancelled'
|
|
run: |
|
|
if [ -s "$REVIEW_OUTPUT_FILE" ]; then exit 0; fi
|
|
echo "::warning::Orchestrator merge failed — using simple concatenation fallback"
|
|
SECURITY=$(cat security_review.md 2>/dev/null || echo "Security review not available.")
|
|
QUALITY=$(cat quality_review.md 2>/dev/null || echo "Quality review not available.")
|
|
CCS=$(cat ccs_review.md 2>/dev/null || echo "CCS compliance review not available.")
|
|
{
|
|
echo "# Parallel AI Code Review"
|
|
echo ""
|
|
echo "> [!] Orchestrator merge failed — raw reviewer outputs below."
|
|
echo ""
|
|
echo "---"
|
|
echo ""
|
|
echo "## Security Review"
|
|
echo ""
|
|
printf '%s\n' "$SECURITY"
|
|
echo ""
|
|
echo "---"
|
|
echo ""
|
|
echo "## Quality & Correctness Review"
|
|
echo ""
|
|
printf '%s\n' "$QUALITY"
|
|
echo ""
|
|
echo "---"
|
|
echo ""
|
|
echo "## CCS Compliance Review"
|
|
echo ""
|
|
printf '%s\n' "$CCS"
|
|
echo ""
|
|
printf '> Parallel review by `%s` (fallback — orchestrator unavailable)\n' "$REVIEW_MODEL"
|
|
} > "$REVIEW_OUTPUT_FILE"
|
|
|
|
- name: Publish review comment
|
|
if: always()
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
REVIEW_MARKER: >-
|
|
<!-- ccs-ai-review
|
|
run:${{ github.run_id }}
|
|
attempt:${{ github.run_attempt }}
|
|
pr:${{ needs.prepare.outputs.pr_number }}
|
|
sha:${{ needs.prepare.outputs.head_sha }} -->
|
|
run: |
|
|
if [ ! -s "$REVIEW_OUTPUT_FILE" ]; then
|
|
echo "::error::No merged review content available"
|
|
exit 1
|
|
fi
|
|
|
|
{
|
|
printf '%s\n\n' "$REVIEW_MARKER"
|
|
cat "$REVIEW_OUTPUT_FILE"
|
|
} > "$REVIEW_COMMENT_FILE"
|
|
|
|
COMMENTS_JSON="$(gh api "repos/${{ github.repository }}/issues/${{ needs.prepare.outputs.pr_number }}/comments?per_page=100")"
|
|
COMMENT_ID="$(
|
|
printf '%s' "$COMMENTS_JSON" |
|
|
jq -r --arg marker "$REVIEW_MARKER" '.[] | select(.body | contains($marker)) | .id' |
|
|
tail -n 1
|
|
)"
|
|
|
|
if [ -n "$COMMENT_ID" ]; then
|
|
jq -Rs '{body: .}' < "$REVIEW_COMMENT_FILE" |
|
|
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" --method PATCH --input -
|
|
echo "[i] Updated review comment ${COMMENT_ID} for PR #${{ needs.prepare.outputs.pr_number }}"
|
|
else
|
|
jq -Rs '{body: .}' < "$REVIEW_COMMENT_FILE" |
|
|
gh api "repos/${{ github.repository }}/issues/${{ needs.prepare.outputs.pr_number }}/comments" --method POST --input -
|
|
echo "[i] Posted review comment for PR #${{ needs.prepare.outputs.pr_number }}"
|
|
fi
|
|
|
|
- name: Add success reaction
|
|
if: success() && github.event_name == 'issue_comment'
|
|
run: |
|
|
gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
|
|
--method POST -f content=rocket
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
|
|
- name: Add failure reaction
|
|
if: failure() && github.event_name == 'issue_comment'
|
|
run: |
|
|
gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
|
|
--method POST -f content=confused
|
|
env:
|
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
|
|
- name: Upload merged review artifact
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: merged-review-pr${{ needs.prepare.outputs.pr_number }}-run${{ github.run_id }}
|
|
path: ${{ env.REVIEW_OUTPUT_FILE }}
|
|
retention-days: 7
|
|
if-no-files-found: warn
|
|
|
|
- name: Cleanup
|
|
if: always()
|
|
run: rm -f "$REVIEW_COMMENT_FILE" "$REVIEW_OUTPUT_FILE" security_review.md quality_review.md ccs_review.md review_inputs.md
|