feat: SonarQube Server integration with PAT auth#12
Conversation
…onfig test - http.ts: read response.text() before JSON.parse so empty 200 bodies (common on Data Center PUT/DELETE) return undefined instead of crashing with SyntaxError: Unexpected end of JSON input - http.ts: fix dry-run exit in jira/bitbucket/confluence — replace async stderr callback + pending Promise with fs.writeSync + process.exitCode so Node drains streams naturally; also removes the need for a hung promise - output.ts: replace process.exit() in fail() with process.exitCode + throw so stderr flushes before Node exits; throw propagates to cli.ts catch handler - cli.ts: top-level catch now skips "Fatal:" when exitCode is already set, preventing double-write when fail() or dry-run threw to bubble up - config/commands.ts: replace all process.exit() in wizard cancellation paths with process.exitCode + return for the same stream-drain reason - config test: implement real connectivity checks (Jira /myself, Bitbucket /application-properties, Confluence /space) instead of Phase 2 stub message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements full SonarQube Server (Data Center) integration following
the existing 4-layer pattern (types/config/http/client/commands).
- New commands: sonar quality-gate, issues, measures, projects, hotspots
- PAT Bearer auth via PNCLI_SONAR_TOKEN / pncli config init
- defaults.sonar.project config for project-key fallback (--project optional)
- sonarPaginate helper for page-number-based pagination
- config test now checks SonarQube connectivity
- Fixes shared error parser to handle SonarQube's {errors:[{msg}]} shape
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds full SonarQube Server integration to pncli, including configuration, HTTP plumbing, a typed client, and new CLI commands for quality gate/metrics/issues/projects/hotspots.
Changes:
- Introduces SonarQube API types and a
SonarClientwith page-number pagination support. - Adds
sonarconfig + env var support (PNCLI_SONAR_BASE_URL,PNCLI_SONAR_TOKEN) anddefaults.sonar.projectto make--projectoptional. - Expands CLI surface area: 5 new
pncli sonar ...commands,config initwizard prompts, andconfig testconnectivity checks (incl. Sonar).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types/sonar.ts | Adds SonarQube Server Web API response types used by the client/commands. |
| src/types/config.ts | Extends config types to include sonar settings and sonar defaults. |
| src/services/sonar/commands.ts | Implements the pncli sonar command group with 5 subcommands and --all pagination. |
| src/services/sonar/client.ts | Adds Sonar client wrapper around the HTTP layer for Sonar endpoints + pagination helpers. |
| src/services/config/commands.ts | Updates config init wizard to include Sonar; implements real config test connectivity checks. |
| src/lib/output.ts | Changes fail() to set process.exitCode and throw after emitting an error envelope. |
| src/lib/http.ts | Adds Sonar auth + request method and sonarPaginate; adjusts dry-run behavior and error parsing. |
| src/lib/config.ts | Loads Sonar config from env/global config; masks Sonar token in config show. |
| src/cli.ts | Updates help text and global error handler behavior for new exit-code-based flow. |
| README.md | Documents Sonar support and new env vars; updates service status table. |
| copilot-instructions.md | Updates agent usage docs to include Sonar workflows and command reference. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (this.dryRun) { | ||
| const safeHeaders = { ...headers, Authorization: '[REDACTED]' }; | ||
| const msg = `DRY RUN: ${init.method} ${url}\nHeaders: ${JSON.stringify(safeHeaders, null, 2)}\n` | ||
| + (opts.body ? `Body: ${JSON.stringify(opts.body, null, 2)}\n` : ''); | ||
| process.stderr.write(msg, () => process.exit(ExitCode.SUCCESS)); | ||
| return new Promise<never>(() => { /* exit pending */ }); | ||
| fs.writeSync(process.stderr.fd, msg); | ||
| process.exitCode = ExitCode.SUCCESS; | ||
| throw new PncliError('dry-run', 0); | ||
| } |
There was a problem hiding this comment.
Dry-run requests currently throw PncliError('dry-run', 0). Because status=0 maps to ExitCode.NETWORK_ERROR and most commands wrap handlers with catch (...) { fail(...) }, --dry-run ends up being reported as an error (and may exit non-zero) instead of a successful early-exit. Consider throwing a dedicated sentinel error (e.g., DryRunError) or using a distinct PncliError status, and ensure the error path bypasses fail() / exit-code mapping for dry-run control-flow.
| export function fail( | ||
| err: unknown, | ||
| service: string, | ||
| action: string, | ||
| startTime: number | ||
| ): never { | ||
| const errorDetail: ErrorDetail = { | ||
| status: err instanceof PncliError ? err.status : 1, | ||
| message: err instanceof Error ? err.message : String(err), | ||
| url: err instanceof PncliError ? (err.url ?? null) : null | ||
| }; | ||
|
|
||
| const envelope: ErrorEnvelope = { | ||
| ok: false, | ||
| error: errorDetail, | ||
| meta: buildMeta(service, action, startTime) | ||
| }; | ||
|
|
||
| const msg = globalOptions.pretty | ||
| ? chalk.red('✗ Error: ') + errorDetail.message | ||
| : null; | ||
|
|
||
| if (msg) process.stderr.write(msg + '\n'); | ||
|
|
||
| const output = (globalOptions.pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)) + '\n'; | ||
| const exitCode = err instanceof PncliError ? exitCodeFromStatus(err.status) : ExitCode.GENERAL_ERROR; | ||
| try { | ||
| fs.writeSync(process.stdout.fd, output); | ||
| } catch (writeErr) { | ||
| if ((writeErr as NodeJS.ErrnoException).code !== 'EPIPE') throw writeErr; | ||
| } | ||
| process.exit(exitCode); | ||
| process.exitCode = exitCode; | ||
| throw new PncliError(errorDetail.message, errorDetail.status); | ||
| } |
There was a problem hiding this comment.
fail() now always writes an error envelope and maps PncliError.status to an exit code. This interacts badly with the new dry-run flow (which throws PncliError('dry-run', 0)): dry-run becomes a printed error and can overwrite the intended success exit code. Add a special-case to treat dry-run as control-flow (e.g., rethrow without emitting an error envelope / without changing process.exitCode), or handle a dedicated DryRunError type here.
| try { | ||
| const opts = program.optsWithGlobals(); | ||
| const cfg = loadConfig({ configPath: opts.config }); | ||
| const http = createHttpClient(cfg); |
There was a problem hiding this comment.
config test creates the HTTP client without passing the global --dry-run flag, so pncli config test --dry-run will still execute real requests (and won’t print the request details). Pass Boolean(opts.dryRun) into createHttpClient for consistency with other commands.
| const http = createHttpClient(cfg); | |
| const http = createHttpClient(cfg, Boolean(opts.dryRun)); |
| process.stderr.write('Aborted.\n'); | ||
| process.exit(ExitCode.SUCCESS); | ||
| process.exitCode = ExitCode.SUCCESS; | ||
| return; |
There was a problem hiding this comment.
The return; inside the .pncli.json already exists / !overwrite branch is mis-indented, which is likely to fail linting/formatting checks and makes the control flow harder to read. Align indentation with the surrounding block.
| return; | |
| return; |
Summary
sonar quality-gate,sonar issues,sonar measures,sonar projects,sonar hotspotsPNCLI_SONAR_TOKENenv var orpncli config initwizarddefaults.sonar.projectconfig key makes--projectoptional on all project-scoped commands (same pattern asdefaults.jira.project)sonarPaginatehelper handles SonarQube's page-number-based pagination; all commands support--allfor auto-paginationconfig testnow checks SonarQube connectivity{errors:[{msg}]}shape (other APIs usemessage)Test plan
npm run typecheckpassesnpm run lintpassespncli sonar --helpshows all 5 subcommandspncli config showincludes maskedsonarblockpncli sonar quality-gate --project my-proj --dry-runprints request URL/headers without executingpncli config testreportssonar: { ok: true }against a real SonarQube instancepncli config initpresents SonarQube section and writes to config🤖 Generated with Claude Code