Skip to content

Commit 29d8005

Browse files
feat(vue-doctor): wire --push flag to push privacy-stripped findings to the SaaS
Adds three new flags: --push POST findings to the SaaS after the audit --push-url <url> Endpoint (default: https://app.the-doctor.report/api/v1/findings) --api-key <key> API key for the x-api-key header Defaults to --no-push. The push happens after the audit AND after the reporter renders, so --format json --output report.json still writes the file first. The audit's exit code is unaffected by push failure: 5xx, network errors, and abort timeouts all log a warning but do not flip the exit code. If --push is set without --api-key, a warning prints to stderr and the push is skipped (audit continues). 9 e2e tests in tests/push.integration.test.ts use a real local HTTP server (http.createServer().listen(0)) to capture the request and assert the 6-field privacy boundary, the x-api-key header, the URL, the CI env forwarding, and that a giant codeSnippet in the source never appears in the request body. Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>
1 parent e6dbbd6 commit 29d8005

3 files changed

Lines changed: 427 additions & 3 deletions

File tree

packages/vue-doctor/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ vue-doctor --score
5959
| `--no-respect-inline-disables` | Surface findings even inside `doctor-disable` comments. |
6060
| `--threshold <n>` | Minimum passing score, integer `0``100`. |
6161
| `--score` | Print only the numeric score, for piping. |
62+
| `--push` | After the audit, POST privacy-stripped findings to the SaaS. Requires `--api-key`. Negatable with `--no-push`. |
63+
| `--no-push` | Skip the SaaS push (default). |
64+
| `--push-url <url>` | SaaS endpoint for `--push` (default: `https://app.the-doctor.report/api/v1/findings`). |
65+
| `--api-key <key>` | API key for the SaaS, sent as the `x-api-key` header. |
6266
| `--annotations` | Emit GitHub Actions `::error::` / `::warning::` lines. |
6367
| `--ci` | Auto-enable CI behavior (annotations on GitHub Actions). |
6468
| `--no-ci` | Disable CI auto-detection even when a CI env is set. |

packages/vue-doctor/src/cli.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
mergeCliOverrides,
1818
normalizeInitAnswers,
1919
planInit,
20+
pushFindings,
2021
renderVerboseTrace,
2122
scoreDiagnostics,
2223
type AuditReport,
@@ -288,6 +289,9 @@ interface CliFlags {
288289
prComment?: string | true;
289290
fix?: boolean;
290291
fixExclude?: string | string[];
292+
push?: boolean;
293+
pushUrl: string;
294+
apiKey?: string;
291295
}
292296

293297
interface PreparedOptions {
@@ -485,6 +489,42 @@ function emitReport(
485489
}
486490
}
487491

492+
async function maybePushFindings(
493+
report: AuditReport,
494+
flags: CliFlags,
495+
): Promise<void> {
496+
if (flags.push !== true) return;
497+
if (!flags.apiKey) {
498+
process.stderr.write(
499+
'vue-doctor: --push enabled but --api-key is not set; skipping push (no findings were sent).\n',
500+
);
501+
return;
502+
}
503+
const url = flags.pushUrl;
504+
const project: string = 'vue-doctor';
505+
const result = await pushFindings({
506+
project,
507+
score: report.score,
508+
errorCount: report.errorCount,
509+
warnCount: report.warnCount,
510+
infoCount: report.infoCount,
511+
diagnostics: report.diagnostics,
512+
url,
513+
apiKey: flags.apiKey,
514+
});
515+
if (result.ok) {
516+
process.stderr.write(
517+
`vue-doctor: pushed ${report.diagnostics.length} finding${report.diagnostics.length === 1 ? '' : 's'} to ${url} (HTTP ${result.status}).\n`,
518+
);
519+
} else {
520+
const detail =
521+
result.status !== undefined ? ` (HTTP ${result.status})` : '';
522+
process.stderr.write(
523+
`vue-doctor: --push failed${detail}: ${result.error}. Audit exit code is unchanged.\n`,
524+
);
525+
}
526+
}
527+
488528
async function runProjectAudits(
489529
rootDir: string,
490530
flags: CliFlags,
@@ -706,6 +746,21 @@ export async function run(argv: string[] = process.argv): Promise<number> {
706746
'--fix-exclude <ruleId>',
707747
'Mark a rule as not-auto-fixable (repeatable). Note: oxlint applies built-in fixes globally, so this is enforced only for doctor plugin fixes; built-in vue/* fixes still apply.',
708748
)
749+
.option(
750+
'--push',
751+
'After the audit, POST privacy-stripped findings to the SaaS. Requires --api-key. Negatable with --no-push.',
752+
)
753+
.option(
754+
'--push-url <url>',
755+
'SaaS endpoint for --push (default: https://app.the-doctor.report/api/v1/findings)',
756+
{
757+
default: 'https://app.the-doctor.report/api/v1/findings',
758+
},
759+
)
760+
.option(
761+
'--api-key <key>',
762+
'API key for the SaaS (sent as the x-api-key header for both --score-mode and --push).',
763+
)
709764
.action(async (path: string | undefined, flags: CliFlags) => {
710765
const reporter = resolveFormat(flags);
711766
if (flags.failOn !== undefined && !isFailOnLevel(flags.failOn)) {
@@ -798,10 +853,10 @@ export async function run(argv: string[] = process.argv): Promise<number> {
798853
}
799854
if (flags.score) {
800855
process.stdout.write(`${report.score}\n`);
801-
process.exitCode = report.exitCode;
802-
return;
856+
} else {
857+
emitReport(report, reporter, flags);
803858
}
804-
emitReport(report, reporter, flags);
859+
await maybePushFindings(report, flags);
805860
process.exitCode = report.exitCode;
806861
} catch (err) {
807862
const msg = err instanceof Error ? err.message : String(err);

0 commit comments

Comments
 (0)