feat(qa): fire a multica webhook autopilot for each sonar GitHub issue
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
557983d247
commit
9f78a1552f
@ -6,14 +6,15 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- **Sonar triage can fire a multica webhook autopilot** after each GitHub issue (URL from `~/.config/multica-webhook` or `MULTICA_WEBHOOK_URL`, passed to curl on stdin only); the multica CLI stays as fallback.
|
||||
### Changed
|
||||
- QA: the Sonar triage script groups medium/large findings per rule instead of per (rule, file): one GitHub issue per rule with a file/line table (max 40 rows, 'and N more'; split into 'part n/m' beyond 40 locations); dedupe marker still carries all Sonar keys.
|
||||
|
||||
### Changed
|
||||
- Conversation Playground: the round microphone button is red (pressed, "Stop microphone") while the mic is hot (live or recording) and blue ("Start microphone") when off; it returns to blue on stop, auto-stop and mic errors. The pulse ring only animates when reduced motion is not requested.
|
||||
- Conversation Playground: the metrics (Token Usage and STT / LLM first token / TTS first audio TTFB charts) now sit in the right-hand Latency column below Turn history instead of a second "Metrics" tab; the Conversation | Metrics tablist is removed. Charts gain axis titles ("Time (ms)", "Time"), a legend and three y-ticks.
|
||||
|
||||
### Fixed
|
||||
- Sonar: removed 43 small Sonar findings in 20 files: unused variables and dead helper functions (frontend), unused locals and a misread comment (backend); behaviour unchanged. 7 further small findings were deliberately left (used via globals, or entangled with other dead code).
|
||||
- Conversation Playground: Turn history rows (#n, time, error rows) were in the DOM but squeezed to zero height by the flex column once charts filled the Latency column; the list no longer shrinks.
|
||||
- Conversation Playground: the metrics charts render at the column width (re-rendered on resize) so axis, tick and legend text is 11px instead of ~8px; a single point is centred with a larger dot.
|
||||
|
||||
|
||||
@ -31,6 +31,8 @@ SonarQube /api/issues/search (GET, paged, OPEN/CONFIRMED/REOPENED)
|
||||
One per rule (all files); title `[sonar] <rule>: <short> in N file(s)`; body has a table of file, line, Sonar key, message (capped at 40 rows, then 'and N more'), rule link, effort, approach, acceptance. A rule with more than 40 locations is split into numbered parts (`part 1/3`), 40 locations each. Hidden marker `<!-- sonar-keys: k1,k2 -->` lists every key of the part; an open `sonar` issue containing any of the keys suppresses a new one. Bodies contain paths/lines only, no code. If `gh auth status` fails the script reports and continues.
|
||||
|
||||
## multica hookup
|
||||
Preferred: a multica **webhook autopilot** (Autopilots -> New autopilot -> Trigger: Webhook, assignee `sonar-fixer`, output mode Create issue, project Voice Creator Clone & Design 2). Multica shows the webhook URL after the autopilot is created; it carries its own secret, so save it only in `~/.config/multica-webhook` (chmod 600) or `MULTICA_WEBHOOK_URL`, never in the repo. After each GitHub issue the script POSTs JSON (`source, rule, title, github_issue_url, files, sonar_keys, effort_min, body`) to it, passing the URL to curl on stdin only. Fallback if no webhook is configured: the CLI command below.
|
||||
|
||||
After a GitHub issue is created the script runs `multica issue create --project <id> --assignee sonar-fixer --description-stdin` (the multica issue links the GitHub URL; assigning starts the agent run). Override with `MULTICA_AGENT`, `MULTICA_PROJECT`. Failure is reported, not fatal. Agent `sonar-fixer` instructions: see the end of this file.
|
||||
|
||||
## --fix-small
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Local SonarQube triage loop. Dry-run by default. See architecture/sonar-triage.md.
|
||||
import { readFileSync, writeFileSync, mkdtempSync, existsSync } from 'node:fs';
|
||||
import { readFileSync, writeFileSync, mkdtempSync, existsSync, rmSync } from 'node:fs';
|
||||
import { homedir, tmpdir } from 'node:os';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
@ -125,6 +125,23 @@ export function hasMarker(existingBodies, keys) {
|
||||
|
||||
const run = (cmd, args, input) => spawnSync(cmd, args, { input, encoding: 'utf8', cwd: ROOT, timeout: 600000 });
|
||||
|
||||
// The webhook URL carries its own secret: read it from env/file, pass it to curl on stdin only (never argv, never logged).
|
||||
export function webhookUrl(env = process.env, file = join(homedir(), '.config', 'multica-webhook')) {
|
||||
const u = (env.MULTICA_WEBHOOK_URL || (existsSync(file) ? readFileSync(file, 'utf8') : '')).trim();
|
||||
return /^https:\/\/[^\s"]+$/.test(u) ? u : '';
|
||||
}
|
||||
|
||||
export function postWebhook(url, payload) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'sonar-hook-'));
|
||||
const f = join(dir, 'payload.json');
|
||||
writeFileSync(f, JSON.stringify(payload), { mode: 0o600 });
|
||||
const cfg = `url = "${url}"\nrequest = "POST"\nheader = "Content-Type: application/json"\ndata-binary = "@${f}"\nsilent\nmax-time = 30\noutput = "/dev/null"\nwrite-out = "%{http_code}"\n`;
|
||||
const r = spawnSync('curl', ['-K', '-'], { input: cfg, encoding: 'utf8', timeout: 40000 });
|
||||
try { rmSync(dir, { recursive: true, force: true }); } catch { /* temp dir cleanup is best effort */ }
|
||||
const code = Number((r.stdout || '').trim()) || 0;
|
||||
return { ok: code >= 200 && code < 300, code };
|
||||
}
|
||||
|
||||
function ghIssues(groups, apply, cap, log) {
|
||||
if (run('gh', ['auth', 'status']).status !== 0) { log('gh unauthenticated: skipping GitHub issues (run `gh auth login`).'); return []; }
|
||||
const list = run('gh', ['issue', 'list', '--state', 'open', '--label', 'sonar', '--limit', '300', '--json', 'number,body']);
|
||||
@ -139,9 +156,16 @@ function ghIssues(groups, apply, cap, log) {
|
||||
if (r.status !== 0) { log(` gh create failed for ${g.title}`); continue; }
|
||||
const url = r.stdout.trim().split('\n').pop();
|
||||
log(` created ${url}`);
|
||||
// Mechanism: a multica issue (assigned to the agent) that points at the GitHub issue.
|
||||
// Mechanism 1: a multica autopilot webhook (URL is a secret: env MULTICA_WEBHOOK_URL or ~/.config/multica-webhook).
|
||||
// Mechanism 2 (fallback): the multica CLI, which refuses to run in an agent context.
|
||||
const hook = webhookUrl();
|
||||
if (hook) {
|
||||
const w = postWebhook(hook, { source: 'sonar-triage', rule: g.rule, title: g.title, github_issue_url: url, files: g.files, sonar_keys: g.keys, effort_min: g.effort, body: issueBody(g) });
|
||||
log(w.ok ? ` multica autopilot fired (HTTP ${w.code})` : ` multica webhook failed (HTTP ${w.code || 'none'}): assign the issue manually`);
|
||||
} else {
|
||||
const m = run('multica', ['issue', 'create', '--title', g.title, '--project', MULTICA_PROJECT, '--assignee', MULTICA_AGENT, '--description-stdin'], `GitHub issue: ${url}\n\n${issueBody(g)}`);
|
||||
log(m.status === 0 ? ` multica issue assigned to ${MULTICA_AGENT}` : ` multica hookup failed (create it manually): ${(m.stderr || '').split('\n')[0]}`);
|
||||
}
|
||||
done.push(url);
|
||||
}
|
||||
return done;
|
||||
|
||||
@ -83,3 +83,11 @@ test('dry-run makes only GET requests and never prints the token', async () => {
|
||||
assert.ok(out.join('\n').includes('SMALL (1)'));
|
||||
assert.ok(!out.join('\n').includes('SECRET-TOKEN-123'));
|
||||
});
|
||||
|
||||
test('webhookUrl accepts only https URLs and never invents one', async () => {
|
||||
const { webhookUrl } = await import('../scripts/sonar_triage.mjs');
|
||||
assert.equal(webhookUrl({ MULTICA_WEBHOOK_URL: 'https://example.org/hook/abc' }, '/nonexistent'), 'https://example.org/hook/abc');
|
||||
assert.equal(webhookUrl({ MULTICA_WEBHOOK_URL: 'http://example.org/hook' }, '/nonexistent'), '');
|
||||
assert.equal(webhookUrl({ MULTICA_WEBHOOK_URL: 'https://a b"c' }, '/nonexistent'), '');
|
||||
assert.equal(webhookUrl({}, '/nonexistent'), '');
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user