-
Notifications
You must be signed in to change notification settings - Fork 0
merge: sync 264 upstream QwenLM/qwen-code commits into HopCode #82
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 all commits
c90e6e7
8babaa4
1af1bb8
35a8851
0af3cd8
e2b8555
f11bb31
565e4bc
d442a15
e00fe6a
fce79dd
8b4871b
c302e3e
05be7b1
7a04512
f3694dd
7f34cda
e1ddcdc
f37c64c
ea536d3
cc0a639
d545536
c5fb9fd
ceb1d30
f23357d
91f3e8a
1609bda
52f875a
a756676
7b9e318
c48dc32
e92fcba
52fdc10
a974594
cf6323b
6c36ac2
e5cf48e
4746bc1
c9e1546
afb4b07
71fa676
60f8937
ff70f79
4d6c7cc
c958e6f
fc184f2
e324104
4fd72b3
fe866b7
0cbd687
66521d8
855a2b3
17fbaa2
891c59f
b4fe437
df3e753
def96c8
15d30dd
1287ac1
b9b3bce
95c49fc
1a46df5
1abc958
6a5ad45
dab218f
f3ea17b
487fb45
1467ed3
427b5ad
8d5e47a
32d2f46
bb7b4d8
e0e90cc
9031d09
1f028da
4ce6a08
ea50153
2c95d93
46814e4
fd76550
b1c5950
4537cc3
6d26e1e
3026db3
5054303
60c2a63
f3b3b99
55e425c
bf8bdd9
813bd74
262186c
489bea9
227a60c
727bd47
230f4af
ca61d78
897fdc8
1fd47b3
4b372d3
e1e99f7
6509e8d
76addf4
250ead3
75645ca
848386a
5c9e73f
39f3108
2e669c0
89ddc70
2126474
686a137
e6e939e
2cb0031
badf851
9019251
b16baf1
62e11a5
dc8e155
1931425
8de93b8
e2e94bc
ea0749e
31883d5
9605bcc
0633b8a
8dfa761
006ac59
c1235d8
e5ec601
e743f0e
183ad54
e3076e2
b1ec04f
23c6c70
da22360
9658dcc
972cb04
9ea3099
2a21963
5cd459e
fe3dd93
081c46c
6accb8e
8123c6b
68ff698
3911b1d
e1fc45d
67da781
cc64d7c
4e3fd29
a6a1258
7417805
9f87c90
5dc2e15
9b2fb30
3282c17
18e2eed
1a227f0
ad7e23f
cdf83d8
d3bd265
2d12c29
2a6a951
c37cb23
9c96249
e48999d
0e684a3
59e771c
548ca3d
9b3aa52
abec702
bd9bf50
aa4bccf
4400fe6
ba7561a
62c0a0a
90e1e3d
e9a7917
905c5c9
e23c8e8
e1f5d21
2b732f5
1c2a643
4675274
015ee42
acfb00e
bfce93a
e390639
54ba259
50d027f
52a190b
adda526
06fc052
9042ede
03c5447
1ead34c
6a726f6
7a528d0
fe816f6
60b9c92
b13032d
fa81e0a
feaeeb2
7605c8b
5d0733f
b8e3550
0d1d240
49b000d
8146b7f
c7170b5
802c382
3bf0fa0
82fb6a4
a8a99f0
11c874d
b23f888
1b58ede
fc701f8
be0b074
47f62a4
edc0555
fa6e0f9
9a63c03
1783ae8
170ce79
0c28ddc
6f2f21f
b726b7c
5c8af1a
cb49633
c7fa13d
350191e
3744cd0
f20925e
879b854
063535f
57b3dcd
5f7b57f
4292102
7281eb5
2fc6b08
0f98842
ac123bb
57326e5
881d682
d56bd1d
be7e874
bcdb44c
2a9ff7c
339278e
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,102 @@ | ||
| #!/usr/bin/env node | ||
| import { readFileSync } from 'node:fs'; | ||
|
|
||
| export const CI_PROFILES = { | ||
| DOCS_ONLY: 'docs_only', | ||
| GITHUB_CI_ONLY: 'github_ci_only', | ||
| FULL: 'full', | ||
| }; | ||
|
|
||
| export const GITHUB_CI_ONLY_FILES = new Set([ | ||
| '.github/scripts/pr-safety-precheck.mjs', | ||
| '.github/scripts/pr-safety-precheck.test.mjs', | ||
| '.github/workflows/qwen-pr-safety-precheck.yml', | ||
| ]); | ||
|
|
||
| function isDocsOnlyFile(file) { | ||
| const normalized = file.replace(/\\/g, '/'); | ||
| return ( | ||
| /^docs\/.+\.(?:md|mdx)$/i.test(normalized) || | ||
| /^(?:README|CHANGELOG|CONTRIBUTING|CODE_OF_CONDUCT|SECURITY|SUPPORT|LICENSE|NOTICE)(?:\.[^/]*)?$/i.test( | ||
| normalized, | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| function classifyPath(file) { | ||
| if (isDocsOnlyFile(file)) return CI_PROFILES.DOCS_ONLY; | ||
| if (GITHUB_CI_ONLY_FILES.has(file)) return CI_PROFILES.GITHUB_CI_ONLY; | ||
| return CI_PROFILES.FULL; | ||
| } | ||
|
|
||
| function classifyFileEntry(entry) { | ||
| if (typeof entry === 'string') return classifyPath(entry); | ||
|
|
||
| const filename = entry?.filename; | ||
| if (!filename) return CI_PROFILES.FULL; | ||
|
|
||
| const profile = classifyPath(filename); | ||
| if (entry.status !== 'renamed') return profile; | ||
|
|
||
| const previousProfile = entry.previous_filename | ||
| ? classifyPath(entry.previous_filename) | ||
| : CI_PROFILES.FULL; | ||
| return previousProfile === profile ? profile : CI_PROFILES.FULL; | ||
| } | ||
|
|
||
| export function classifyChangedFiles(files) { | ||
| const changedFiles = files.filter(Boolean); | ||
| if (changedFiles.length === 0) return CI_PROFILES.FULL; | ||
|
|
||
| if ( | ||
| changedFiles.every( | ||
| (entry) => classifyFileEntry(entry) === CI_PROFILES.DOCS_ONLY, | ||
| ) | ||
| ) { | ||
| return CI_PROFILES.DOCS_ONLY; | ||
| } | ||
|
|
||
| if ( | ||
| changedFiles.every( | ||
| (entry) => classifyFileEntry(entry) === CI_PROFILES.GITHUB_CI_ONLY, | ||
| ) | ||
| ) { | ||
| return CI_PROFILES.GITHUB_CI_ONLY; | ||
| } | ||
|
|
||
| return CI_PROFILES.FULL; | ||
| } | ||
|
|
||
| function parseChangedFiles(text) { | ||
| return text | ||
| .split(/\r?\n/) | ||
| .filter(Boolean) | ||
| .map((line) => { | ||
| try { | ||
| return JSON.parse(line); | ||
| } catch { | ||
| return line; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| function main() { | ||
| const filePath = process.argv[2]; | ||
|
Check failure on line 84 in .github/scripts/ci/classify-profile.mjs
|
||
| if (!filePath) { | ||
| console.log(CI_PROFILES.FULL); | ||
|
Check failure on line 86 in .github/scripts/ci/classify-profile.mjs
|
||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const files = parseChangedFiles(readFileSync(filePath, 'utf8')); | ||
| console.log(classifyChangedFiles(files)); | ||
|
Check failure on line 92 in .github/scripts/ci/classify-profile.mjs
|
||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| console.error(`::warning::Failed to read changed files: ${message}`); | ||
|
Check failure on line 95 in .github/scripts/ci/classify-profile.mjs
|
||
| console.log(CI_PROFILES.FULL); | ||
|
Check failure on line 96 in .github/scripts/ci/classify-profile.mjs
|
||
| } | ||
| } | ||
|
|
||
| if (import.meta.url === `file://${process.argv[1]}`) { | ||
|
Check failure on line 100 in .github/scripts/ci/classify-profile.mjs
|
||
| main(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import test from 'node:test'; | ||
|
|
||
| import { | ||
| GITHUB_CI_ONLY_FILES, | ||
| classifyChangedFiles, | ||
| } from './classify-profile.mjs'; | ||
|
|
||
| test('uses docs_only for markdown-only changes', () => { | ||
| assert.equal( | ||
| classifyChangedFiles(['README.md', 'docs/usage.md', '.qwen/design/foo.md']), | ||
| 'full', | ||
| ); | ||
| assert.equal( | ||
| classifyChangedFiles(['README.md', 'docs/usage.md']), | ||
| 'docs_only', | ||
| ); | ||
| }); | ||
|
|
||
| test('uses docs_only for uppercase and extensionless docs', () => { | ||
| assert.equal( | ||
| classifyChangedFiles(['README.MD', 'docs/guide.MDX', 'LICENSE', 'README']), | ||
| 'docs_only', | ||
| ); | ||
| }); | ||
|
|
||
| test('falls back to full for root docs names used as directories', () => { | ||
| assert.equal(classifyChangedFiles(['README.md/evil.ts']), 'full'); | ||
| assert.equal(classifyChangedFiles(['LICENSE.txt/src/index.ts']), 'full'); | ||
| }); | ||
|
|
||
| test('uses github_ci_only for the allowed GitHub CI helper files', () => { | ||
| assert.equal( | ||
| classifyChangedFiles([...GITHUB_CI_ONLY_FILES]), | ||
| 'github_ci_only', | ||
| ); | ||
| }); | ||
|
|
||
| test('uses github_ci_only for each allowed GitHub CI helper file', () => { | ||
| for (const file of GITHUB_CI_ONLY_FILES) { | ||
| assert.equal(classifyChangedFiles([file]), 'github_ci_only'); | ||
| } | ||
| }); | ||
|
|
||
| test('falls back to full for case-mismatched GitHub CI helper paths', () => { | ||
| assert.equal( | ||
| classifyChangedFiles(['.GITHUB/SCRIPTS/PR-SAFETY-PRECHECK.MJS']), | ||
| 'full', | ||
| ); | ||
| }); | ||
|
|
||
| test('classifies renamed files using both old and new paths', () => { | ||
| assert.equal( | ||
| classifyChangedFiles([ | ||
| { | ||
| filename: 'docs/new.md', | ||
| previous_filename: 'packages/core/src/runtime.ts', | ||
| status: 'renamed', | ||
| }, | ||
| ]), | ||
| 'full', | ||
| ); | ||
| assert.equal( | ||
| classifyChangedFiles([ | ||
| { | ||
| filename: 'docs/new.md', | ||
| previous_filename: 'docs/old.md', | ||
| status: 'renamed', | ||
| }, | ||
| ]), | ||
| 'docs_only', | ||
| ); | ||
| }); | ||
|
|
||
| test('falls back to full when changed files are unavailable', () => { | ||
| assert.equal(classifyChangedFiles([]), 'full'); | ||
| assert.equal(classifyChangedFiles(['', null, undefined]), 'full'); | ||
| }); | ||
|
|
||
| test('falls back to full for source or mixed changes', () => { | ||
| assert.equal( | ||
| classifyChangedFiles(['README.md', 'packages/cli/src/index.ts']), | ||
| 'full', | ||
| ); | ||
| assert.equal( | ||
| classifyChangedFiles([ | ||
| 'README.md', | ||
| '.github/scripts/pr-safety-precheck.mjs', | ||
| ]), | ||
| 'full', | ||
| ); | ||
| }); | ||
|
|
||
| test('falls back to full for main CI workflow changes', () => { | ||
| assert.equal(classifyChangedFiles(['.github/workflows/ci.yml']), 'full'); | ||
| assert.equal(classifyChangedFiles(['.github/workflows/codeql.yml']), 'full'); | ||
| }); | ||
|
|
||
| test('falls back to full for classifier changes', () => { | ||
| assert.equal( | ||
| classifyChangedFiles(['.github/scripts/ci/classify-profile.mjs']), | ||
| 'full', | ||
| ); | ||
| assert.equal( | ||
| classifyChangedFiles(['.github/scripts/ci/classify-profile.test.mjs']), | ||
| 'full', | ||
| ); | ||
| }); | ||
|
|
||
| test('falls back to full for runtime markdown assets and instruction files', () => { | ||
| assert.equal( | ||
| classifyChangedFiles(['packages/core/src/skills/bundled/foo/SKILL.md']), | ||
| 'full', | ||
| ); | ||
| assert.equal(classifyChangedFiles(['AGENTS.md']), 'full'); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| #!/usr/bin/env node | ||
| import { readFileSync, writeFileSync, appendFileSync } from 'node:fs'; | ||
|
|
||
| const SECRET_NAME_PATTERN = String.raw`secrets\.[A-Z0-9_]+|process\.env\.[A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD|_PAT)|\b(?:GITHUB_TOKEN|GH_TOKEN|OPENAI_API_KEY)\b`; | ||
| const LOGGING_SINK_PATTERN = String.raw`\b(?:console\.\w+|process\.(?:stdout|stderr)\.write)\s*\(`; | ||
| const NETWORK_SINK_PATTERN = String.raw`\b(?:fetch|axios|curl|wget)\b`; | ||
|
|
||
| function secretSinkPattern(sinkPattern) { | ||
| return new RegExp( | ||
| String.raw`(?:${sinkPattern}[\s\S]{0,500}(?:${SECRET_NAME_PATTERN})|(?:${SECRET_NAME_PATTERN})[\s\S]{0,500}${sinkPattern})`, | ||
| 'i', | ||
| ); | ||
| } | ||
|
|
||
| const SENSITIVE_DIFF_PATTERNS = [ | ||
| ['sensitive_diff:secret_logging', secretSinkPattern(LOGGING_SINK_PATTERN)], | ||
| ['sensitive_diff:secret_network', secretSinkPattern(NETWORK_SINK_PATTERN)], | ||
| ]; | ||
|
|
||
| const SECRET_VALUE_PATTERNS = [ | ||
| ['secret_value:private_key', /-----BEGIN [A-Z ]*PRIVATE KEY-----/], | ||
| [ | ||
| 'secret_value:github_token', | ||
| /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b/, | ||
| ], | ||
| ['secret_value:openai_key', /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/], | ||
| ['secret_value:aws_access_key', /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/], | ||
| ['secret_value:slack_token', /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/], | ||
| [ | ||
| 'secret_value:bearer_token', | ||
| /\bAuthorization\s*:\s*Bearer\s+[A-Za-z0-9._~+/=-]{20,}\b/i, | ||
| ], | ||
| [ | ||
| 'secret_value:access_token_param', | ||
| /\baccess_token=[A-Za-z0-9._~+/=-]{20,}\b/i, | ||
| ], | ||
| [ | ||
| 'secret_value:url_credentials', | ||
| /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^@\s]{20,}@/i, | ||
| ], | ||
| [ | ||
| 'secret_value:assignment', | ||
| /(?:^|[^A-Za-z0-9])(?:[A-Z0-9_-]*(?:api[_-]?key|token|secret|password|pat))\b[^=\n:]{0,32}(?::?=|:)\s*['"`][A-Za-z0-9._~+/=-]{20,}['"`]/i, | ||
| ], | ||
| ]; | ||
|
|
||
| const PROMPT_INJECTION_PATTERNS = [ | ||
| [ | ||
| 'prompt_injection:ignore_previous', | ||
| /ignore (?:all )?(?:previous|above) instructions/i, | ||
| ], | ||
| ['prompt_injection:system_prompt', /\bsystem prompt\b/i], | ||
| ['prompt_injection:developer_message', /\bdeveloper message\b/i], | ||
| [ | ||
| 'prompt_injection:print_secrets', | ||
| /\b(?:print|dump|exfiltrate|reveal)\b[^\n]*(?:secret|token|key)s?\b/i, | ||
| ], | ||
| ['prompt_injection:run_gh', /\brun\b[^\n]*\bgh\b/i], | ||
| ['prompt_injection:approve_pr', /\bapprove (?:this )?pr\b/i], | ||
| [ | ||
| 'prompt_injection:qwen_command', | ||
| /@qwen-code\s+\/(?:triage|review|resolve|tmux)\b/i, | ||
| ], | ||
| ]; | ||
|
|
||
| function addReason(reasons, code) { | ||
| if (!reasons.includes(code)) reasons.push(code); | ||
| } | ||
|
|
||
| function checkPatterns(text, patterns, reasons) { | ||
| for (const [code, pattern] of patterns) { | ||
| if (pattern.test(text)) addReason(reasons, code); | ||
| } | ||
| } | ||
|
|
||
| export function assessPullRequestSafety({ pr, diff, trustedAuthor = false }) { | ||
| const reasons = []; | ||
| const headSha = typeof pr?.headRefOid === 'string' ? pr.headRefOid : ''; | ||
| const diffText = typeof diff === 'string' ? diff : ''; | ||
| let addedText = ''; | ||
|
|
||
| if (!headSha) addReason(reasons, 'input:missing_head_sha'); | ||
|
|
||
| if (trustedAuthor && reasons.length === 0) { | ||
| return { | ||
| decision: 'allow_triage', | ||
| head_sha: headSha, | ||
| reason_codes: [], | ||
| }; | ||
| } | ||
|
|
||
| if (!diffText) { | ||
| addReason(reasons, 'input:diff_unavailable'); | ||
| } else { | ||
| addedText = diffText | ||
| .split('\n') | ||
| .filter((line) => line.startsWith('+') && !line.startsWith('+++')) | ||
| .map((line) => line.slice(1)) | ||
| .join('\n'); | ||
| checkPatterns(addedText, SENSITIVE_DIFF_PATTERNS, reasons); | ||
| } | ||
|
|
||
| const prText = `${pr?.title ?? ''}\n${pr?.body ?? ''}\n${addedText}`; | ||
| checkPatterns(prText, SECRET_VALUE_PATTERNS, reasons); | ||
| checkPatterns(prText, PROMPT_INJECTION_PATTERNS, reasons); | ||
|
|
||
| return { | ||
| decision: reasons.length === 0 ? 'allow_triage' : 'manual_required', | ||
| head_sha: headSha, | ||
| reason_codes: reasons, | ||
| }; | ||
| } | ||
|
|
||
| export function renderManualRequiredComment(result) { | ||
| const reasons = result.reason_codes.length | ||
| ? result.reason_codes.map((reason) => `- \`${reason}\``).join('\n') | ||
| : '- `unknown`'; | ||
|
|
||
| return `<!-- qwen-pr-precheck:manual-required --> | ||
| Qwen precheck requires maintainer approval before automated triage/review. | ||
|
|
||
| Head SHA: \`${result.head_sha || 'unknown'}\` | ||
|
|
||
| Reason: | ||
| ${reasons} | ||
|
|
||
| A maintainer with write access can inspect the PR and manually request a run with \`@qwen-code /triage\` or \`@qwen-code /review\`. A new push requires a fresh precheck.`; | ||
| } | ||
|
|
||
| function parseArgs(argv) { | ||
| const args = {}; | ||
| for (let i = 0; i < argv.length; i += 1) { | ||
| const arg = argv[i]; | ||
| if (!arg.startsWith('--')) throw new Error(`Unexpected argument: ${arg}`); | ||
| const key = arg.slice(2); | ||
| const value = argv[i + 1]; | ||
| if (!value || value.startsWith('--')) { | ||
| throw new Error(`Missing value for --${key}`); | ||
| } | ||
| args[key] = value; | ||
| i += 1; | ||
| } | ||
| return args; | ||
| } | ||
|
|
||
| function writeGithubOutput(path, result) { | ||
| if (!path) return; | ||
| appendFileSync(path, [`decision=${result.decision}`, ''].join('\n')); | ||
| } | ||
|
|
||
| function main() { | ||
| const args = parseArgs(process.argv.slice(2)); | ||
|
Check failure on line 152 in .github/scripts/pr-safety-precheck.mjs
|
||
| if (!args.pr) throw new Error('Missing --pr'); | ||
| if (!args.diff) throw new Error('Missing --diff'); | ||
|
|
||
| const pr = JSON.parse(readFileSync(args.pr, 'utf8')); | ||
| const diff = readFileSync(args.diff, 'utf8'); | ||
| const trustedAuthor = args['trusted-author'] === 'true'; | ||
| const result = assessPullRequestSafety({ pr, diff, trustedAuthor }); | ||
|
|
||
| if (args.comment) { | ||
| writeFileSync( | ||
| args.comment, | ||
| result.decision === 'manual_required' | ||
| ? renderManualRequiredComment(result) | ||
| : '', | ||
| ); | ||
| } | ||
| writeGithubOutput(args.output ?? process.env.GITHUB_OUTPUT, result); | ||
|
Check failure on line 169 in .github/scripts/pr-safety-precheck.mjs
|
||
| console.log(JSON.stringify(result, null, 2)); | ||
|
Check failure on line 170 in .github/scripts/pr-safety-precheck.mjs
|
||
| } | ||
|
|
||
| if (import.meta.url === `file://${process.argv[1]}`) { | ||
|
Check failure on line 173 in .github/scripts/pr-safety-precheck.mjs
|
||
| main(); | ||
| } | ||
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.
This precheck runs in the HopCode repository, where the automation command prefix is
@hopcode, but the prompt-injection pattern only recognizes@qwen-code. A fork PR body or diff containing the HopCode command form such as@hopcode /reviewor@hopcode /triagewill be classified asallow_triageinstead ofmanual_required, bypassing the command-specific safety signal this check is meant to enforce.Useful? React with 👍 / 👎.