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
Closed
Closed
Changes from 36 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
ac27ac2
ci(review): use bundled PR review workflow
yiliang114 65b4111
fix(ci): correct misleading comment about quote stripping in review w…
yiliang114 7275f86
fix(ci): satisfy workflow yaml lint
yiliang114 c02ac19
ci(review): add manual dry-run mode
yiliang114 03e0dbc
fix(ci): harden bundled PR review against prompt injection
yiliang114 6818309
Merge remote-tracking branch 'origin/main' into codex/bundled-pr-revi…
yiliang114 79fd36c
style(ci): format review workflow action pins
yiliang114 9b86ab8
fix(ci): scope OpenAI secrets to the review step + document --ci
yiliang114 4cd8549
docs(review): align --ci command allowlist with what Step 9 actually …
yiliang114 596a780
ci(review): use review-specific OpenAI credentials
yiliang114 8d5e041
ci(review): add smoke mode for PR diff checks
yiliang114 0e1eb30
fix(ci): correct smoke review summary output
yiliang114 0919a11
fix(ci): tighten @qwen /review grep boundary and fix sed multi-line e…
yiliang114 dbb875b
refactor(ci): use QwenLM/qwen-code-action instead of building CLI fro…
yiliang114 0c9ccb9
fix(ci): align prompt indentation in review_prompt construction
yiliang114 bcb2127
refactor(ci): add checkout step, move review rules to .github/, rever…
yiliang114 596edce
fix(ci): strip leading blanks from additional_instructions and clarif…
yiliang114 11a491e
fix(ci): add reopened/ready_for_review triggers and edited comment types
yiliang114 f157309
fix(ci): align sed extraction boundary with grep for @qwen /review
yiliang114 7e83c5e
fix(ci): guard review-rules copy against missing file before merge
yiliang114 39a314a
refactor(ci): move review-rules to .qwen/, harden with cross-repo che…
yiliang114 db9bc44
refactor(ci): move review-rules to .qwen/, harden with cross-repo che…
yiliang114 b3c2361
docs(review): make product-direction gate blocking by default
yiliang114 204837c
fix(ci): prevent review workflow from approving PRs
yiliang114 f00f2af
docs(review): document code review automation design
yiliang114 2e77544
ci(review): add synchronize trigger and persist incremental review cache
yiliang114 fa3dbd9
fix(ci): include base SHA in review-cache key to prevent cross-base c…
yiliang114 5217988
fix(ci): use merge-base SHA in cache key and gate save on comment pub…
yiliang114 e542b0d
fix(ci): skip merge-base lookup for cross-repository PRs
yiliang114 e9ac834
fix(ci): address review workflow gate issues
yiliang114 d253035
feat(review): add design gate preflight scripts
yiliang114 11d45c7
feat(review): add design gate rerun controls
yiliang114 4140257
test(review): expand act smoke coverage
yiliang114 660398a
fix(review): improve direction gate history recall
yiliang114 584fe30
docs(code-review): format comparison tables
yiliang114 36043db
fix(review): harden design-gate scripts per dogfooded review
yiliang114 424b459
docs(review): separate review criteria from workflow process in revie…
yiliang114 f63a944
fix: update copyright year to 2026 in new test file [skip ci]
yiliang114 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import { | ||
| parseArgs, | ||
| readJson, | ||
| requireArg, | ||
| run, | ||
| writeJson, | ||
| writeText, | ||
| } from './lib/cli.mjs'; | ||
| import { loadAnchors } from './lib/anchors.mjs'; | ||
| import { | ||
| buildDesignGateLlmPrompt, | ||
| evaluateDesignGate, | ||
| formatProcessComment, | ||
| formatPromptAppend, | ||
| } from './lib/design-gate-core.mjs'; | ||
| import { runQwenJson } from './lib/llm.mjs'; | ||
|
|
||
| async function maybeRunLlm({ pr, shape, history, anchors }) { | ||
| if (process.env.QWEN_DESIGN_GATE_LLM !== 'true') { | ||
| return { findings: [] }; | ||
| } | ||
|
|
||
| try { | ||
| const result = await runQwenJson({ | ||
| prompt: buildDesignGateLlmPrompt({ 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'); | ||
| console.log('Design Gate: reading PR shape'); | ||
| const shape = await readJson(requireArg(args, 'shape')); | ||
| console.log('Design Gate: reading history scan'); | ||
| const history = await readJson(requireArg(args, 'history'), { findings: [] }); | ||
| const out = requireArg(args, 'out'); | ||
|
|
||
| console.log('Design Gate: resolving PR metadata'); | ||
| 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', | ||
| ]), | ||
| ); | ||
|
|
||
| console.log('Design Gate: loading anchors'); | ||
| const anchors = await loadAnchors({ | ||
| rootDir: args.root ?? process.cwd(), | ||
| changedFiles: shape.changed_files, | ||
| }); | ||
| console.log( | ||
| `Design Gate: running LLM check (enabled=${process.env.QWEN_DESIGN_GATE_LLM === 'true'})`, | ||
| ); | ||
| const llm = await maybeRunLlm({ pr, shape, history, anchors }); | ||
| console.log('Design Gate: evaluating findings'); | ||
| 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); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Critical] These new
.github/scripts/**/*.mjshelpers are not covered by the repo's Node-script ESLint override, sonpm run lint:ci -- --quietnow fails on the added scripts withno-undefforprocess/console(for example this line, plus the newconsole.logcalls and other helpers). The existing override ineslint.config.jsonly covers./scripts/**/*.js,./scripts/**/*.mjs,esbuild.config.js, andpackages/*/scripts/**/*.js.Please extend that override to include the new GitHub helper scripts, e.g. add
.github/scripts/**/*.mjs(and.github/scripts/**/*.jsif desired) so the repository lint job can pass.— gpt-5.5 via Qwen Code /review