Skip to content

Commit 5a8da3e

Browse files
feat(nuxt-doctor): wire --push flag to push privacy-stripped findings to the SaaS
Mirrors the vue-doctor --push implementation, adjusted for the Nuxt audit pipeline: --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 The push runs after the audit and after the reporter renders. 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. 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 the singular/plural wording in the success log. Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>
1 parent 29d8005 commit 5a8da3e

3 files changed

Lines changed: 424 additions & 3 deletions

File tree

packages/nuxt-doctor/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ nuxt-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/nuxt-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,
@@ -296,6 +297,9 @@ interface CliFlags {
296297
prComment?: string | true;
297298
fix?: boolean;
298299
fixExclude?: string | string[];
300+
push?: boolean;
301+
pushUrl: string;
302+
apiKey?: string;
299303
}
300304

301305
interface PreparedOptions {
@@ -493,6 +497,42 @@ function emitReport(
493497
}
494498
}
495499

500+
async function maybePushFindings(
501+
report: AuditReport,
502+
flags: CliFlags,
503+
): Promise<void> {
504+
if (flags.push !== true) return;
505+
if (!flags.apiKey) {
506+
process.stderr.write(
507+
'nuxt-doctor: --push enabled but --api-key is not set; skipping push (no findings were sent).\n',
508+
);
509+
return;
510+
}
511+
const url = flags.pushUrl;
512+
const project: string = 'nuxt-doctor';
513+
const result = await pushFindings({
514+
project,
515+
score: report.score,
516+
errorCount: report.errorCount,
517+
warnCount: report.warnCount,
518+
infoCount: report.infoCount,
519+
diagnostics: report.diagnostics,
520+
url,
521+
apiKey: flags.apiKey,
522+
});
523+
if (result.ok) {
524+
process.stderr.write(
525+
`nuxt-doctor: pushed ${report.diagnostics.length} finding${report.diagnostics.length === 1 ? '' : 's'} to ${url} (HTTP ${result.status}).\n`,
526+
);
527+
} else {
528+
const detail =
529+
result.status !== undefined ? ` (HTTP ${result.status})` : '';
530+
process.stderr.write(
531+
`nuxt-doctor: --push failed${detail}: ${result.error}. Audit exit code is unchanged.\n`,
532+
);
533+
}
534+
}
535+
496536
async function runProjectAudits(
497537
rootDir: string,
498538
flags: CliFlags,
@@ -714,6 +754,21 @@ export async function run(argv: string[] = process.argv): Promise<number> {
714754
'--fix-exclude <ruleId>',
715755
'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.',
716756
)
757+
.option(
758+
'--push',
759+
'After the audit, POST privacy-stripped findings to the SaaS. Requires --api-key. Negatable with --no-push.',
760+
)
761+
.option(
762+
'--push-url <url>',
763+
'SaaS endpoint for --push (default: https://app.the-doctor.report/api/v1/findings)',
764+
{
765+
default: 'https://app.the-doctor.report/api/v1/findings',
766+
},
767+
)
768+
.option(
769+
'--api-key <key>',
770+
'API key for the SaaS (sent as the x-api-key header for both --score-mode and --push).',
771+
)
717772
.action(async (path: string | undefined, flags: CliFlags) => {
718773
const reporter = resolveFormat(flags);
719774
if (flags.failOn !== undefined && !isFailOnLevel(flags.failOn)) {
@@ -806,10 +861,10 @@ export async function run(argv: string[] = process.argv): Promise<number> {
806861
}
807862
if (flags.score) {
808863
process.stdout.write(`${report.score}\n`);
809-
process.exitCode = report.exitCode;
810-
return;
864+
} else {
865+
emitReport(report, reporter, flags);
811866
}
812-
emitReport(report, reporter, flags);
867+
await maybePushFindings(report, flags);
813868
process.exitCode = report.exitCode;
814869
} catch (err) {
815870
const msg = err instanceof Error ? err.message : String(err);

0 commit comments

Comments
 (0)