Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The bare `sonar analyze` command accepts `-p, --project <project>` for the agent

`sonar analyze dependency-risks` pre-scans discovered manifest files for secrets (via `sonar-secrets`) before the SCA scan and aborts if any are found. In the `analyze` path the secrets binary is a hard prerequisite (install failure aborts the run); in the git pre-commit hook path the manifest secrets *scan* fails open on a scan error (skips/warns). See `dependency-risk-helpers/manifest-secrets-guard.ts`. This scan-error fail-open is distinct from the git/agent secrets-hook auth+binary gate: an unauthenticated user or a missing `sonar-secrets` binary blocks the commit/push (git hooks throw `MissingDependenciesError` → exit 1).

`sonar import` (hidden while in development) imports a repository from a connected DevOps platform into SonarQube by provisioning a bound project. Accepts `--org <key>` and `--repo <slug>` (prompted interactively when omitted) plus `--non-interactive`. Implementation lives in `src/commands/import/`.
`sonar import` (hidden while in development) imports a repository from a connected DevOps platform into SonarQube by provisioning a bound project. Accepts `--org <key>` and `--repo <slug>` (prompted interactively when omitted) plus `--non-interactive`. On success it prints the organization's onboarding dashboard link (`{serverUrl}/organizations/{orgKey}/onboarding-dashboard`) before the final outro. Implementation lives in `src/commands/import/`.

Declarative integration registry helpers live in `src/commands/integrate/_common/registry/index.ts`. New integration descriptors should use that public entrypoint for dependency/resource factories, operations, and registry validation. Command handlers should keep command-specific validation, prompts, and target resolution thin, then delegate feature selection, generic install messages, dependency/resource application, and state recording to `src/commands/integrate/_common/installer.ts`.

Expand Down
32 changes: 28 additions & 4 deletions src/commands/import/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,17 @@ function reportSkipped(skipped: readonly SkippedRepo[]): void {
}
}

function reportOutcome(succeeded: number, failed: number, skippedCount: number): void {
/** Builds the SonarQube Cloud onboarding dashboard link for an organization. */
function buildOnboardingDashboardUrl(serverUrl: string, orgKey: string): string {
return `${serverUrl.replace(/\/$/, '')}/organizations/${orgKey}/onboarding-dashboard`;
}

function reportOutcome(
succeeded: number,
failed: number,
skippedCount: number,
dashboardUrl: string,
): void {
const skippedSuffix = skippedCount > 0 ? ` (${skippedCount} skipped)` : '';

if (failed > 0) {
Expand All @@ -192,7 +202,11 @@ function reportOutcome(succeeded: number, failed: number, skippedCount: number):
}

const succeededNoun = succeeded === 1 ? 'repository' : 'repositories';
outro(`Imported ${succeeded} ${succeededNoun}${skippedSuffix}`, 'success');
outro(
`Imported ${succeeded} ${succeededNoun}${skippedSuffix}`,
'success',
`Dashboard: ${dashboardUrl}`,
);
}

export async function importHandler(options: ImportOptions, auth: ResolvedAuth): Promise<void> {
Expand All @@ -210,7 +224,12 @@ export async function importHandler(options: ImportOptions, auth: ResolvedAuth):
resolution.collection,
);
reportSkipped(skipped);
reportOutcome(succeeded, failed, skipped.length);
reportOutcome(
succeeded,
failed,
skipped.length,
buildOnboardingDashboardUrl(auth.serverUrl, resolution.orgKey),
);
return;
}

Expand All @@ -231,5 +250,10 @@ export async function importHandler(options: ImportOptions, auth: ResolvedAuth):
);

const { succeeded, failed } = progress.finish();
reportOutcome(succeeded, failed, skipped.length);
reportOutcome(
succeeded,
failed,
skipped.length,
buildOnboardingDashboardUrl(auth.serverUrl, resolution.orgKey),
);
}
16 changes: 12 additions & 4 deletions src/ui/components/sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

// Structural markers — intro, outro

import { bold, green, isTTY, red } from '../colors.js';
import { bold, cyan, green, isTTY, red } from '../colors.js';
import { isMockActive, recordCall } from '../mock.js';

const DIVIDER_BASE_WIDTH = 40;
Expand All @@ -44,9 +44,13 @@ export function intro(title: string, subtitle?: string): void {
}
}

export function outro(message: string, status: 'success' | 'error' = 'success'): void {
export function outro(
message: string,
status: 'success' | 'error' = 'success',
detail?: string,
): void {
if (isMockActive()) {
recordCall('outro', message, status);
recordCall('outro', message, status, detail);
return;
}

Expand All @@ -56,8 +60,12 @@ export function outro(message: string, status: 'success' | 'error' = 'success'):
if (isTTY) {
process.stdout.write(`\n ${DIVIDER}\n`);
process.stdout.write(` ${icon} ${bold(colorFn(message))}\n`);
// Aligns under the message text: 2 leading + 2-col emoji icon + 2 gap.
if (detail) process.stdout.write(` ${bold(cyan(detail))}\n`);
process.stdout.write(` ${DIVIDER}\n\n`);
} else {
process.stdout.write(`\n=== ${message} ===\n\n`);
process.stdout.write(`\n=== ${message} ===\n`);
if (detail) process.stdout.write(`${detail}\n`);
process.stdout.write('\n');
}
}
3 changes: 3 additions & 0 deletions tests/integration/specs/import/import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,9 @@ describe('sonar import', () => {
);
expect(result.stdout).toContain('Manual — choose repositories yourself');
expect(result.stdout).toContain('Imported 2 repositories');
expect(result.stdout).toContain(
`Dashboard: ${serverUrl}/organizations/my-org/onboarding-dashboard`,
);
const recorded = server.getRecordedRequests();
const provisionRequests = recorded.filter(
(r) => r.path === '/api/alm_integration/provision_projects',
Expand Down