Repository navigation
Use bundled Qwen Code for PR review automation #4067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 33 commits
ac27ac2
65b4111
7275f86
c02ac19
03e0dbc
6818309
79fd36c
9b86ab8
4cd8549
596a780
8d5e041
0e1eb30
0919a11
dbb875b
0c9ccb9
bcb2127
596edce
11a491e
f157309
7e83c5e
39a314a
db9bc44
b3c2361
204837c
f00f2af
2e77544
fa3dbd9
5217988
e542b0d
e9ac834
d253035
11d45c7
4140257
660398a
584fe30
36043db
424b459
f63a944
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| repo_root="$(git rev-parse --show-toplevel)" | ||
| cd "$repo_root" | ||
|
|
||
| usage() { | ||
| cat >&2 <<'EOF' | ||
| Usage: | ||
| .github/scripts/act-smoke-qwen-review.sh [pr-number] [case] | ||
|
|
||
| Cases: | ||
| pr-edited pull_request_target.edited body change; runs Design Gate only | ||
| design-gate-comment issue_comment "@qwen /design-gate"; runs Design Gate only | ||
| oversized pull_request_target.edited with a low size threshold; stops at size gate | ||
| all runs all safe local act cases above | ||
|
|
||
| The smoke cases run with ACT=true, do not post PR comments, and do not run the | ||
| deep /review action. | ||
| EOF | ||
| } | ||
|
|
||
| if ! command -v colima >/dev/null 2>&1; then | ||
| echo "colima is required. Install it with: brew install colima" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if ! command -v docker >/dev/null 2>&1; then | ||
| echo "docker CLI is required. Install it with: brew install docker" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if ! docker info >/dev/null 2>&1; then | ||
| echo "Starting Colima for local GitHub Actions smoke..." | ||
| colima start --cpu 2 --memory 4 --disk 20 | ||
| fi | ||
|
|
||
| if ! command -v act >/dev/null 2>&1; then | ||
| echo "act is required. Install it with: brew install act" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| pr_number="${1:-}" | ||
| case_name="${2:-pr-edited}" | ||
| if [ -z "$pr_number" ] && command -v gh >/dev/null 2>&1; then | ||
| pr_number="$(gh pr view --json number --jq '.number' 2>/dev/null || true)" | ||
| fi | ||
|
|
||
| if [ -z "$pr_number" ]; then | ||
| usage | ||
| echo "Could not infer a PR number from the current branch." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| token="${GITHUB_TOKEN:-}" | ||
| if [ -z "$token" ] && command -v gh >/dev/null 2>&1; then | ||
| token="$(gh auth token 2>/dev/null || true)" | ||
| fi | ||
|
|
||
| if [ -z "$token" ]; then | ||
| echo "GITHUB_TOKEN is required for gh pr view/diff calls inside the workflow." >&2 | ||
| echo "Set GITHUB_TOKEN or authenticate gh before running this smoke." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| tmp_dir="$(mktemp -d)" | ||
| log_dir="${ACT_SMOKE_LOG_DIR:-.qwen/tmp/act-smoke/$(date +%Y%m%d-%H%M%S)}" | ||
| mkdir -p "$log_dir" | ||
|
|
||
| secret_file="$tmp_dir/secrets" | ||
| chmod 700 "$tmp_dir" | ||
| printf 'GITHUB_TOKEN=%s\n' "$token" > "$secret_file" | ||
| chmod 600 "$secret_file" | ||
|
|
||
| cleanup_generated_files() { | ||
| rm -f \ | ||
| qwen-pr-review-size-comment.md \ | ||
| qwen-pr-review-fork-comment.md \ | ||
| qwen-design-gate-override-comment.md \ | ||
| .qwen/tmp/qwen-review-pr-"$pr_number"-shape.json \ | ||
| .qwen/tmp/qwen-review-pr-"$pr_number"-history.json \ | ||
| .qwen/tmp/qwen-review-pr-"$pr_number"-design-gate.json \ | ||
| .qwen/tmp/qwen-review-pr-"$pr_number"-design-gate-comment.md \ | ||
| .qwen/tmp/qwen-review-pr-"$pr_number"-design-gate-prompt.md | ||
| } | ||
|
|
||
| trap 'rm -rf "$tmp_dir"; cleanup_generated_files' EXIT | ||
|
|
||
| platform_args=() | ||
| case "$(uname -m)" in | ||
| arm64|aarch64) | ||
| platform_args=(--container-architecture linux/arm64) | ||
| ;; | ||
| esac | ||
|
|
||
| write_event() { | ||
| local name="$1" | ||
| local event_file="$2" | ||
|
|
||
| case "$name" in | ||
| pr-edited|oversized) | ||
| cat > "$event_file" <<JSON | ||
| { | ||
| "action": "edited", | ||
| "changes": { | ||
| "body": { | ||
| "from": "previous body" | ||
| } | ||
| }, | ||
| "pull_request": { | ||
| "number": $pr_number, | ||
| "author_association": "MEMBER" | ||
| }, | ||
| "repository": { | ||
| "full_name": "${GITHUB_REPOSITORY:-QwenLM/qwen-code}" | ||
| }, | ||
| "sender": { | ||
| "login": "${GITHUB_ACTOR:-local-act-smoke}" | ||
| } | ||
| } | ||
| JSON | ||
| ;; | ||
| design-gate-comment) | ||
| cat > "$event_file" <<JSON | ||
| { | ||
| "action": "created", | ||
| "issue": { | ||
| "number": $pr_number, | ||
| "pull_request": { | ||
| "url": "https://api.github.com/repos/${GITHUB_REPOSITORY:-QwenLM/qwen-code}/pulls/$pr_number" | ||
| } | ||
| }, | ||
| "comment": { | ||
| "body": "@qwen /design-gate", | ||
| "author_association": "MEMBER" | ||
| }, | ||
| "repository": { | ||
| "full_name": "${GITHUB_REPOSITORY:-QwenLM/qwen-code}" | ||
| }, | ||
| "sender": { | ||
| "login": "${GITHUB_ACTOR:-local-act-smoke}" | ||
| } | ||
| } | ||
| JSON | ||
| ;; | ||
| *) | ||
| usage | ||
| echo "Unknown smoke case: $name" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
| } | ||
|
|
||
| event_name_for_case() { | ||
| case "$1" in | ||
| pr-edited|oversized) | ||
| printf 'pull_request_target' | ||
| ;; | ||
| design-gate-comment) | ||
| printf 'issue_comment' | ||
| ;; | ||
| esac | ||
| } | ||
|
|
||
| max_lines_for_case() { | ||
| case "$1" in | ||
| oversized) | ||
| printf '100' | ||
| ;; | ||
| *) | ||
| printf '50000' | ||
| ;; | ||
| esac | ||
| } | ||
|
|
||
| summarize_log() { | ||
| local name="$1" | ||
| local log_file="$2" | ||
|
|
||
| echo "----- $name summary -----" | ||
| grep -E 'Run Main (Resolve PR context|Check PR size|Generate PR shape|Scan review history|Run Design Gate)|::set-output:: (should_run_review|gate_only|bypass_design_gate|should_review|status)=|Review scope:|Design Gate status:|Job succeeded|Job failed|Failure -' "$log_file" || true | ||
| echo "Log: $log_file" | ||
| } | ||
|
|
||
| run_case() { | ||
| local name="$1" | ||
| local event_file="$tmp_dir/$name.json" | ||
| local log_file="$log_dir/$name.log" | ||
| local event_name | ||
| local max_lines | ||
|
|
||
| event_name="$(event_name_for_case "$name")" | ||
| max_lines="$(max_lines_for_case "$name")" | ||
| write_event "$name" "$event_file" | ||
| cleanup_generated_files | ||
|
|
||
| echo "===== Running act smoke case: $name ($event_name) =====" | ||
| if act "$event_name" \ | ||
| --bind \ | ||
| -W .github/workflows/qwen-code-pr-review.yml \ | ||
| -j review-pr \ | ||
| -e "$event_file" \ | ||
| -P ubuntu-latest="${ACT_UBUNTU_IMAGE:-catthehacker/ubuntu:act-latest}" \ | ||
| --var QWEN_PR_REVIEW_MODEL=local-act-smoke \ | ||
| --var QWEN_PR_REVIEW_MAX_CHANGED_LINES="$max_lines" \ | ||
| --var QWEN_DESIGN_GATE_LLM=false \ | ||
| --env ACT=true \ | ||
| --secret-file "$secret_file" \ | ||
| "${platform_args[@]}" 2>&1 | tee "$log_file"; then | ||
| summarize_log "$name" "$log_file" | ||
| else | ||
| summarize_log "$name" "$log_file" | ||
| exit 1 | ||
| fi | ||
| } | ||
|
|
||
| case "$case_name" in | ||
| all) | ||
| run_case pr-edited | ||
| run_case design-gate-comment | ||
| run_case oversized | ||
| ;; | ||
| pr-edited|design-gate-comment|oversized) | ||
| run_case "$case_name" | ||
| ;; | ||
| *) | ||
| usage | ||
| echo "Unknown smoke case: $case_name" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { | ||
| parseArgs, | ||
| readJson, | ||
| requireArg, | ||
| run, | ||
| writeJson, | ||
| writeText, | ||
| } from './lib/cli.mjs'; | ||
| import { loadAnchors } from './lib/anchors.mjs'; | ||
| import { | ||
| evaluateDesignGate, | ||
| formatProcessComment, | ||
| formatPromptAppend, | ||
| } from './lib/design-gate-core.mjs'; | ||
| import { runQwenJson } from './lib/llm.mjs'; | ||
|
|
||
| function buildLlmPrompt({ pr, shape, history, anchors }) { | ||
| return [ | ||
| 'You are Qwen Code Design Gate. Return JSON only.', | ||
| 'Schema: {"findings":[{"gate":"product_direction|scope|architecture|claude_code","severity":"advisory|blocking","message":"...","citations":["..."]}]}', | ||
| 'Blocking findings require citations. Claude Code comparison findings must be advisory.', | ||
| '', | ||
| 'PR:', | ||
| JSON.stringify({ title: pr.title, body: pr.body }, null, 2), | ||
| '', | ||
| 'PR shape:', | ||
| JSON.stringify(shape, null, 2), | ||
| '', | ||
| 'History scan:', | ||
| JSON.stringify(history.findings ?? [], null, 2), | ||
| '', | ||
| 'Anchors:', | ||
| JSON.stringify(anchors.loaded, null, 2), | ||
| ].join('\n'); | ||
| } | ||
|
|
||
| async function maybeRunLlm({ pr, shape, history, anchors }) { | ||
| if (process.env.QWEN_DESIGN_GATE_LLM !== 'true') { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] These new Please extend that override to include the new GitHub helper scripts, e.g. add — gpt-5.5 via Qwen Code /review |
||
| return { findings: [] }; | ||
| } | ||
|
|
||
| try { | ||
| const result = await runQwenJson({ | ||
| prompt: buildLlmPrompt({ pr, shape, history, anchors }), | ||
| }); | ||
| return result.json && Array.isArray(result.json.findings) | ||
| ? result.json | ||
| : { findings: [] }; | ||
| } catch (error) { | ||
| return { | ||
| findings: [ | ||
| { | ||
| gate: 'product_direction', | ||
| severity: 'advisory', | ||
| message: `Design Gate LLM check skipped: ${error.message}`, | ||
| citations: [], | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| async function main() { | ||
| const args = parseArgs(); | ||
| const repo = requireArg(args, 'repo'); | ||
| const prNumber = requireArg(args, 'pr'); | ||
| const shape = await readJson(requireArg(args, 'shape')); | ||
| const history = await readJson(requireArg(args, 'history'), { findings: [] }); | ||
| const out = requireArg(args, 'out'); | ||
|
|
||
| const pr = | ||
| (await readJson(args['pr-json'], undefined)) ?? | ||
|
yiliang114 marked this conversation as resolved.
|
||
| JSON.parse( | ||
| await run('gh', [ | ||
| 'pr', | ||
| 'view', | ||
| prNumber, | ||
| '--repo', | ||
| repo, | ||
| '--json', | ||
| 'title,body,url,baseRefName,headRefName', | ||
| ]), | ||
| ); | ||
|
|
||
| const anchors = await loadAnchors({ | ||
| rootDir: args.root ?? process.cwd(), | ||
| changedFiles: shape.changed_files, | ||
| }); | ||
| const llm = await maybeRunLlm({ pr, shape, history, anchors }); | ||
| const result = evaluateDesignGate({ pr, shape, history, anchors, llm }); | ||
|
|
||
| await writeJson(out, result); | ||
| if (args['process-comment-out']) { | ||
| await writeText(args['process-comment-out'], formatProcessComment(result)); | ||
| } | ||
| if (args['prompt-append-out']) { | ||
| await writeText(args['prompt-append-out'], formatPromptAppend(result)); | ||
| } | ||
|
|
||
| console.log(`Design Gate status: ${result.status}`); | ||
| console.log(result.summary); | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| console.error(error.stack ?? error.message); | ||
| process.exit(1); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { parseArgs, readJson, requireArg, run, writeJson } from './lib/cli.mjs'; | ||
| import { scanHistory } from './lib/gh-search.mjs'; | ||
|
|
||
| async function main() { | ||
| const args = parseArgs(); | ||
| const repo = requireArg(args, 'repo'); | ||
| const prNumber = requireArg(args, 'pr'); | ||
| const shape = await readJson(requireArg(args, 'shape')); | ||
| const out = requireArg(args, 'out'); | ||
|
|
||
| const pr = | ||
| (await readJson(args['pr-json'], undefined)) ?? | ||
| JSON.parse( | ||
| await run('gh', [ | ||
| 'pr', | ||
| 'view', | ||
| prNumber, | ||
| '--repo', | ||
| repo, | ||
| '--json', | ||
| 'title,body,url', | ||
| ]), | ||
| ); | ||
|
|
||
| const history = await scanHistory({ | ||
| repo, | ||
| pr, | ||
| shape, | ||
| limit: Number(args.limit ?? 20), | ||
| }); | ||
|
|
||
| await writeJson(out, history); | ||
| console.log(`Wrote history scan to ${out}`); | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| console.error(error.stack ?? error.message); | ||
| process.exit(1); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.