Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

292 lines
15 KiB
YAML

name: AI Code Reviewer
# Reviews are opt-in per PR: add the ai_code_review label to request one
# (the label is auto-removed after the run so re-adding it re-triggers).
# The automatic opened/synchronize/ready_for_review triggers were removed —
# reviewing every push burned reviewer-API spend on PRs nobody asked to have
# reviewed, and a billing outage (HTTP 402) then showed a red check on every
# PR in the repo.
#
# Residual risk accepted: the diff text goes into the LLM prompt, so a
# hostile diff can try to steer the posted comment or labels (prompt
# injection). The maintainer-applied label gate plus the advisory-only
# decision (FAIL_ON_REQUESTED_CHANGES defaults to false) bound that
# blast radius, and it exists for internal PRs too.
on:
# zizmor: ignore[dangerous-triggers] — pull_request_target is required so
# the review also works on fork PRs: fork-triggered pull_request runs get
# no secrets (OPENROUTER_API_KEY) and a read-only token, so the reviewer
# could neither call the model nor post its comment. This is safe under
# two invariants — KEEP BOTH when editing this file:
# 1. Only the ai_code_review label triggers a run, and only users with
# triage permission can add labels: every run on fork code is an
# explicit maintainer action, never attacker-initiated.
# 2. Nothing from the PR head is ever executed. The checkout is the
# trusted base branch (pull_request_target default), so
# .github/scripts/combine-ai-reviews.sh runs from base; the PR head
# is fetched as *data only* to compute the diff.
pull_request_target:
types: [labeled]
# Per-PR concurrency with cancel-in-progress: re-adding the label while a
# review is still running cancels the now-stale run. Keyed on the PR number
# so runs for *different* PRs never cancel each other — that repo-wide
# cancellation was the problem in the earlier attempt (#3554, reverted
# #3599), not per-PR cancellation.
#
# The labeled trigger fires for *every* label added to the PR, but the job's
# `if` only proceeds when the label is ai_code_review. Without the run_id
# suffix below, a skip-only labeled event would still enter this group,
# cancel a real in-flight review, and then skip itself — leaving the PR with
# no review at all. Routing those into a unique group keyed on run_id means
# they cancel nothing; the `if` discards them quietly.
concurrency:
group: >-
ai-code-review-${{ github.event.pull_request.number }}${{
(github.event.action == 'labeled' && github.event.label.name != 'ai_code_review')
&& format('-skip-{0}', github.run_id) || ''
}}
cancel-in-progress: true
permissions: {} # Minimal top-level for OSSF Scorecard Token-Permissions
jobs:
comprehensive-review:
name: AI Code Review
runs-on: ubuntu-latest
timeout-minutes: 30
environment: ci
# Only the ai_code_review label triggers a review (works on drafts too);
# every other labeled event is discarded here.
if: github.event.label.name == 'ai_code_review'
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
# SECURITY: this checks out the trusted BASE branch (the
# pull_request_target default), which is what makes running
# .github/scripts/combine-ai-reviews.sh below safe on fork PRs.
# Never add `ref: ${{ github.event.pull_request.head.sha }}` here —
# that would execute fork-controlled scripts with secrets in env.
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq
- name: Get PR diff
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# The PR head — possibly fork code — is fetched as *data only*
# through the base repo's refs/pull/N/head mirror (present for
# fork and same-repo PRs alike); it is never checked out and
# nothing from it is executed. Env vars prevent template
# injection from malicious branch names.
git fetch origin "$BASE_REF" "+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr-head"
git diff "origin/$BASE_REF...origin/pr-head" --no-color > diff.txt
- name: Download AI reviewer script
run: |
curl -fsSL https://raw.githubusercontent.com/LearningCircuit/Friendly-AI-Reviewer/main/ai-reviewer.sh -o ai-reviewer.sh
chmod +x ai-reviewer.sh
- name: AI Code Review
id: ai-review
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
# Comma-separated list of models. Each model produces one independent
# review, presented as "Reviewer 1", "Reviewer 2", ... in a single
# combined comment. Falls back to the shared AI_MODEL (also used by
# release.yml, which must stay a single model) when unset, so a
# one-entry list reproduces the original single-review behavior.
AI_REVIEW_MODELS: ${{ vars.AI_REVIEW_MODELS || vars.AI_MODEL || 'moonshotai/kimi-k2-thinking' }}
AI_TEMPERATURE: ${{ vars.AI_TEMPERATURE || '0.1' }}
AI_MAX_TOKENS: ${{ vars.AI_MAX_TOKENS || '64000' }}
MAX_DIFF_SIZE: ${{ vars.MAX_DIFF_SIZE || '800000' }}
EXCLUDE_FILE_PATTERNS: ${{ vars.EXCLUDE_FILE_PATTERNS || '*.lock,*.min.js,*.min.css,package-lock.json,yarn.lock' }}
STRUCTURED_OUTPUT: ${{ vars.STRUCTURED_OUTPUT || 'true' }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_FULL_NAME: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
FAIL_ON_REQUESTED_CHANGES: ${{ vars.FAIL_ON_REQUESTED_CHANGES || 'false' }}
DEBUG_MODE: ${{ vars.DEBUG_MODE || 'false' }}
run: |
echo "Running AI code review..."
# Parse the comma-separated model list. Each entry becomes one
# independent reviewer ("Reviewer 1", "Reviewer 2", ...). A single
# entry reproduces the original one-review behavior.
# `read` returns non-zero at EOF; the here-string always appends a
# trailing newline so it returns 0 here, but `|| true` keeps a future
# edge case from tripping `set -e`. An empty/whitespace AI_REVIEW_MODELS
# yields only empty entries, which the filter below drops, correctly
# falling through to the "No models configured" error.
IFS=',' read -ra RAW_MODELS <<< "$AI_REVIEW_MODELS" || true
MODELS=()
for m in "${RAW_MODELS[@]}"; do
# Trim surrounding whitespace with parameter expansion. (xargs would
# choke on a model id containing a quote and word-collapse internal
# spaces — model ids shouldn't have either, but this is robust.)
m="${m#"${m%%[![:space:]]*}"}" # strip leading whitespace
m="${m%"${m##*[![:space:]]}"}" # strip trailing whitespace
[ -n "$m" ] && MODELS+=("$m")
done
if [ "${#MODELS[@]}" -eq 0 ]; then
echo "❌ No models configured in AI_REVIEW_MODELS"
exit 1
fi
echo "Configured ${#MODELS[@]} reviewer model(s)"
# Launch every model concurrently so the job's wall-clock is the
# slowest single review, not the sum of all of them. Each reviewer
# writes its JSON response and its diagnostics to its own index-keyed
# files, so the "Reviewer N" numbering stays stable and the parallel
# output never interleaves. AI_MODEL is overridden per call; all other
# settings (temperature, token limits, diff size, context fetching)
# are shared.
pids=()
for i in "${!MODELS[@]}"; do
AI_MODEL="${MODELS[i]}" bash ai-reviewer.sh < diff.txt \
> "resp_$i.json" 2> "err_$i.log" &
pids[i]=$!
done
# Record each reviewer's exit code to code_<i> (a failing model must
# not abort the others, so disable errexit around wait).
for i in "${!MODELS[@]}"; do
set +e
wait "${pids[i]}"
echo $? > "code_$i"
set -e
done
# Assemble the combined comment with the extracted, unit-tested helper
# (.github/scripts/combine-ai-reviews.sh). It reads resp_<i>.json /
# code_<i> from the working dir and writes the comment body, label set,
# decision, and success count back out. Keeping the assembly in a
# script is what lets tests/ci/test_combine_ai_reviews.py cover the
# parsing, footer-stripping, label union, and pass/fail logic without
# any network or GitHub API access.
bash .github/scripts/combine-ai-reviews.sh "$PWD" "${MODELS[@]}"
COMMENT_BODY="$(cat comment_body.md)"
LABELS="$(cat labels.txt)"
DECISION="$(cat decision.txt)"
SUCCESS_COUNT="$(cat success_count.txt)"
# Post (or update) the reviews as a single sticky PR comment. On each
# push we edit the same comment in place instead of stacking a new one
# per push. The hidden marker identifies our comment specifically, so
# we never touch comments from humans or other bots. (Review comments
# made before this change lack the marker and are left as-is.)
# A commit-sha line lets reviewers tell which push the (edited-in-place)
# comment reflects, since GitHub only shows a vague "edited" marker.
# This literal MUST match the marker the helper script prepends to the
# comment body.
STICKY_MARKER="<!-- ai-code-review:sticky -->"
# STICKY_MARKER is interpolated into the jq program below, so it must
# stay free of `"` and `\`. gh api's --jq takes no --arg, so we can't
# pass it as a jq variable; keeping the marker a literal constant is
# what keeps this safe. The filter yields a single id (we maintain
# exactly one sticky comment), so no post-filtering is needed.
EXISTING_COMMENT_ID=$(gh api "repos/$REPO_FULL_NAME/issues/$PR_NUMBER/comments" \
--paginate \
--jq "[.[] | select(.body | contains(\"$STICKY_MARKER\"))] | last | .id // empty")
if [ -n "$EXISTING_COMMENT_ID" ]; then
echo "Updating existing AI review comment ($EXISTING_COMMENT_ID)"
printf '%s' "$COMMENT_BODY" | gh api \
"repos/$REPO_FULL_NAME/issues/comments/$EXISTING_COMMENT_ID" \
--method PATCH -F body=@- >/dev/null
else
echo "Posting new AI review comment"
printf '%s' "$COMMENT_BODY" | gh pr comment "$PR_NUMBER" --repo "$REPO_FULL_NAME" -F -
fi
# Add labels to PR. Iterate line-by-line (labels.txt is one label per
# line) rather than word-splitting, so a multi-word label such as
# "good first issue" is treated as a single label, not three.
echo "Labels to add: $LABELS"
while IFS= read -r label; do
[ -n "$label" ] || continue
echo "Adding label: $label"
if gh label create "$label" --color "0366d6" --description "Auto-created by AI reviewer" 2>/dev/null; then
echo "Created new label: $label"
else
echo "Label already exists: $label"
fi
if gh issue edit "$PR_NUMBER" --add-label "$label" 2>/dev/null; then
echo "Successfully added label: $label"
else
echo "Failed to add label: $label"
fi
done <<< "$LABELS"
# Handle workflow decision (after posting review and labels)
echo "AI decision: $DECISION"
# Store the decision as a step output for the downstream
# "Fail Workflow if Requested" step. $GITHUB_OUTPUT is the modern
# mechanism — values are scoped to steps.<id>.outputs.<name> and
# cannot re-enter the runner's process environment, eliminating the
# $GITHUB_ENV injection class (zizmor github-env audit #8017).
#
# Defense-in-depth: an allowlist before the write blocks newline
# injection. If a manipulated model response put a newline in
# $DECISION, the bare echo above could write an additional output
# key. Outputs can't reach the process environment so impact is
# minimal, but the case statement closes the door cheaply and
# replaces the lost-on-rework intent of #4985.
case "$DECISION" in pass|fail) ;; *) DECISION="fail" ;; esac
echo "DECISION=$DECISION" >> "$GITHUB_OUTPUT"
# Remove the ai_code_review label so re-adding it re-triggers a review.
echo "Removing ai_code_review label to allow easy re-triggering"
if gh issue edit "$PR_NUMBER" --remove-label "ai_code_review" 2>/dev/null; then
echo "Successfully removed ai_code_review label"
else
echo "Failed to remove ai_code_review label (may have been already removed)"
fi
# If every reviewer failed, the posted comment carries only failure
# notes — surface that as a red workflow run rather than a silent pass,
# matching the original single-review behavior on an unusable response.
if [ "$SUCCESS_COUNT" -eq 0 ]; then
echo "❌ All ${#MODELS[@]} reviewer(s) failed to produce a usable review."
exit 1
fi
echo "✅ AI review completed successfully ($SUCCESS_COUNT/${#MODELS[@]} reviewers)"
- name: Fail Workflow if Requested
if: env.FAIL_ON_REQUESTED_CHANGES == 'true' && steps.ai-review.outputs.DECISION == 'fail'
run: |
echo "❌ AI requested changes and FAIL_ON_REQUESTED_CHANGES is enabled. Failing workflow."
echo "The review has been posted above. Please address the requested changes."
exit 1
- name: Cleanup
if: always()
run: |
rm -f diff.txt
# Per-reviewer response/log/exit-code files from the parallel fan-out,
# plus the assembly helper's output files.
rm -f resp_*.json err_*.log code_* comment_body.md labels.txt decision.txt success_count.txt
# Only remove ai_response.txt if it exists (only created in debug mode)
if [ -f "ai_response.txt" ]; then
rm -f ai_response.txt
fi