feat(qa): automatic sonar-fixer loop (file, deliver, verify, repeat) with safety stops
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8f856c8f8b
commit
776c5cba4c
47
scripts/sonar_auto.mjs
Normal file
47
scripts/sonar_auto.mjs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Automatic sonar-fixer loop: file a batch -> deliver it one issue at a time (--drain) -> verify -> repeat.
|
||||||
|
// Stops (never pushes, never reverts) when: an issue stalls, the quick QA fails, a fix commit looks too big, the tree gets dirty,
|
||||||
|
// nothing is left to file, or --batches is reached. Log: progress lines on stdout; run it detached: `nohup node scripts/sonar_auto.mjs &`.
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const arg = (n, d) => { const i = process.argv.indexOf(n); return i > 0 ? process.argv[i + 1] : d; };
|
||||||
|
const BATCHES = +arg('--batches', 20), CAP = +arg('--cap', 5), TIMEOUT = +arg('--timeout', 60), MAX_FILES = +arg('--max-files', 25);
|
||||||
|
const defer = JSON.parse(readFileSync(join(ROOT, 'scripts', 'sonar_triage.defer.json'), 'utf8')).rules.join(',');
|
||||||
|
const log = (m) => console.log(`[${new Date().toISOString().slice(11, 19)}] ${m}`);
|
||||||
|
const run = (cmd, args, o = {}) => spawnSync(cmd, args, { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 << 20, ...o });
|
||||||
|
const triage = (...a) => run('node', ['scripts/sonar_triage.mjs', ...a], { timeout: 4 * 3600 * 1000 });
|
||||||
|
const dirtyTracked = () => run('git', ['status', '--short']).stdout.split('\n').filter((l) => l && !l.startsWith('??')).length;
|
||||||
|
const openSonar = () => JSON.parse(run('gh', ['issue', 'list', '--state', 'open', '--label', 'sonar', '--label', 'agent-fix', '--json', 'number']).stdout || '[]').length;
|
||||||
|
const head = () => run('git', ['rev-parse', 'HEAD']).stdout.trim();
|
||||||
|
|
||||||
|
function stop(why) { log(`STOPPED: ${why}`); process.exit(2); }
|
||||||
|
|
||||||
|
for (let b = 1; b <= BATCHES; b++) {
|
||||||
|
if (dirtyTracked()) stop('tracked working tree is dirty (an agent is mid-edit or left changes); clean it, then rerun');
|
||||||
|
const before = head();
|
||||||
|
if (openSonar() === 0) {
|
||||||
|
const f = triage('--all', '--cap', String(CAP), '--apply', '--no-webhook', '--exclude-rules', defer);
|
||||||
|
const created = (f.stdout.match(/created https:\/\/\S+/g) || []).length;
|
||||||
|
log(`batch ${b}: filed ${created} issue(s)`);
|
||||||
|
if (!created) { log('nothing left to file for this lane. DONE.'); process.exit(0); }
|
||||||
|
} else log(`batch ${b}: open issues from an earlier run, delivering those first`);
|
||||||
|
const d = triage('--apply', '--drain', '--timeout', String(TIMEOUT));
|
||||||
|
log(d.stdout.trim().split('\n').slice(-6).join(' | '));
|
||||||
|
if (/stalled/.test(d.stdout)) stop('an issue stalled (no answer after a resend); look at multica, then rerun');
|
||||||
|
const commits = run('git', ['log', '--format=%h', `${before}..HEAD`]).stdout.split('\n').filter(Boolean);
|
||||||
|
for (const c of commits) {
|
||||||
|
const n = run('git', ['show', '--name-only', '--format=', c]).stdout.split('\n').filter(Boolean).length;
|
||||||
|
log(` commit ${c}: ${n} file(s) - ${run('git', ['show', '-s', '--format=%s', c]).stdout.trim().slice(0, 90)}`);
|
||||||
|
if (n > MAX_FILES) stop(`commit ${c} touches ${n} files (> ${MAX_FILES}); review it by hand`);
|
||||||
|
}
|
||||||
|
if (dirtyTracked()) stop('tree is dirty after the batch');
|
||||||
|
const q = run('node', ['scripts/qa.mjs', '--fast'], { timeout: 30 * 60 * 1000 });
|
||||||
|
const ok = q.status === 0;
|
||||||
|
log(`batch ${b}: quick QA ${ok ? 'OK' : 'FAILED'}`);
|
||||||
|
if (!ok) stop('quick QA failed after the batch; see `node scripts/qa.mjs --fast`; no revert was done');
|
||||||
|
}
|
||||||
|
log(`DONE: ${BATCHES} batches processed.`);
|
||||||
55
scripts/sonar_triage.defer.json
Normal file
55
scripts/sonar_triage.defer.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Rules the automatic loop must NOT file for the sonar-fixer lane: design-sensitive, async/security, or low value (see architecture/sonar-triage.md). Complexity (S3776) has its own lane.",
|
||||||
|
"rules": [
|
||||||
|
"python:S3776",
|
||||||
|
"javascript:S3776",
|
||||||
|
"python:S1192",
|
||||||
|
"javascript:S1192",
|
||||||
|
"python:S8415",
|
||||||
|
"javascript:S8415",
|
||||||
|
"python:S3358",
|
||||||
|
"javascript:S3358",
|
||||||
|
"python:S5850",
|
||||||
|
"javascript:S5850",
|
||||||
|
"python:S2189",
|
||||||
|
"javascript:S2189",
|
||||||
|
"python:S7488",
|
||||||
|
"javascript:S7488",
|
||||||
|
"python:S7493",
|
||||||
|
"javascript:S7493",
|
||||||
|
"python:S7499",
|
||||||
|
"javascript:S7499",
|
||||||
|
"python:S7503",
|
||||||
|
"javascript:S7503",
|
||||||
|
"python:S5443",
|
||||||
|
"javascript:S5443",
|
||||||
|
"python:S2004",
|
||||||
|
"javascript:S2004",
|
||||||
|
"python:S2703",
|
||||||
|
"javascript:S2703",
|
||||||
|
"python:S3516",
|
||||||
|
"javascript:S3516",
|
||||||
|
"python:S1313",
|
||||||
|
"javascript:S1313",
|
||||||
|
"python:S8410",
|
||||||
|
"javascript:S8410",
|
||||||
|
"python:S2486",
|
||||||
|
"javascript:S2486",
|
||||||
|
"python:S2681",
|
||||||
|
"javascript:S2681",
|
||||||
|
"python:S1874",
|
||||||
|
"javascript:S1874",
|
||||||
|
"python:S8786",
|
||||||
|
"javascript:S8786",
|
||||||
|
"python:S5843",
|
||||||
|
"javascript:S5843",
|
||||||
|
"python:S2925",
|
||||||
|
"javascript:S2925",
|
||||||
|
"python:S7721",
|
||||||
|
"javascript:S7721",
|
||||||
|
"python:S5781",
|
||||||
|
"javascript:S5781",
|
||||||
|
"python:S4624",
|
||||||
|
"javascript:S4624"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user