Skip to content

Commit e376d65

Browse files
CLI-77 Auto-install sonar-secrets during sonar integrate claude
1 parent 0f255b6 commit e376d65

File tree

6 files changed

+78
-209
lines changed

6 files changed

+78
-209
lines changed

README.md

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -119,40 +119,6 @@ sonar auth status
119119

120120
---
121121

122-
### `sonar install`
123-
124-
Install Sonar tools
125-
126-
#### `sonar install secrets`
127-
128-
Install sonar-secrets binary from https://binaries.sonarsource.com
129-
130-
**Options:**
131-
132-
| Option | Type | Required | Description | Default |
133-
| ---------- | ------- | -------- | ----------------------------------------------- | ------- |
134-
| `--force` | boolean | No | Force reinstall even if already installed | - |
135-
| `--status` | boolean | No | Check installation status instead of installing | - |
136-
137-
**Examples:**
138-
139-
Install latest sonar-secrets binary
140-
```bash
141-
sonar install secrets
142-
```
143-
144-
Reinstall sonar-secrets (overwrite existing)
145-
```bash
146-
sonar install secrets --force
147-
```
148-
149-
Check if sonar-secrets is installed and up to date
150-
```bash
151-
sonar install secrets --status
152-
```
153-
154-
---
155-
156122
### `sonar integrate`
157123

158124
Setup SonarQube integration for AI coding agents, git and others.

src/cli/command-tree.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import { authLogin, type AuthLoginOptions } from './commands/auth/login';
2828
import { authLogout, type AuthLogoutOptions } from './commands/auth/logout';
2929
import { authPurge } from './commands/auth/purge';
3030
import { authStatus } from './commands/auth/status';
31-
import { installSecrets, type InstallSecretsOptions } from './commands/install/secrets';
3231
import { integrateClaude, type IntegrateClaudeOptions } from './commands/integrate/claude';
3332
import { integrateGit, type IntegrateGitOptions } from './commands/integrate/git/index';
3433
import { analyzeSecrets, type AnalyzeSecretsOptions } from './commands/analyze/secrets';
@@ -66,16 +65,6 @@ COMMAND_TREE.name('sonar')
6665
.addHelpText('beforeAll', getHelpBanner())
6766
.enablePositionalOptions();
6867

69-
// Install Sonar tools
70-
const install = COMMAND_TREE.command('install').description('Install Sonar tools');
71-
72-
install
73-
.command('secrets')
74-
.description('Install sonar-secrets binary from https://binaries.sonarsource.com')
75-
.option('--force', 'Force reinstall even if already installed')
76-
.option('--status', 'Check installation status instead of installing')
77-
.authenticatedAction((_auth, options: InstallSecretsOptions) => installSecrets(options));
78-
7968
// Setup SonarQube integration for AI coding agent
8069
const integrateCommand = COMMAND_TREE.command('integrate').description(
8170
'Setup SonarQube integration for AI coding agents, git and others.',
@@ -109,7 +98,7 @@ integrateCommand
10998
'--global',
11099
'Install hook globally for all repositories (sets git config --global core.hooksPath)',
111100
)
112-
.authenticatedAction((auth, options: IntegrateGitOptions) => integrateGit(options, auth));
101+
.authenticatedAction((_auth, options: IntegrateGitOptions) => integrateGit(options));
113102

114103
// List Sonar resources
115104
const list = COMMAND_TREE.command('list').description('List Sonar resources');

src/cli/commands/integrate/claude/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { blank, info, intro, note, outro, success, text, warn } from '../../../.
2929
import type { DiscoveredProject } from '../../_common/discovery';
3030
import { discoverProject } from '../../_common/discovery';
3131
import { CommandFailedError } from '../../_common/error';
32+
import { performSecretInstall } from '../../install/secrets';
3233
import { runHealthChecks } from './health';
3334
import { installHooks } from './hooks';
3435
import { repairToken } from './repair';
@@ -74,6 +75,10 @@ export async function integrateClaude(
7475
text('Phase 2/3: Health Check & Repair');
7576
blank();
7677

78+
text('Installing sonar-secrets...');
79+
await performSecretInstall({ force: false });
80+
blank();
81+
7782
const healthResult = await runHealthChecks(
7883
config.serverURL,
7984
token,

tests/integration/specs/install/install-secrets.test.ts

Lines changed: 0 additions & 163 deletions
This file was deleted.

tests/integration/specs/integrate/claude.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe('integrate claude', () => {
3232

3333
beforeEach(async () => {
3434
harness = await TestHarness.create();
35+
harness.state().withSecretsBinaryInstalled();
3536
});
3637

3738
afterEach(async () => {
@@ -458,6 +459,7 @@ describe('integrate claude — A3S entitlement guard', () => {
458459

459460
beforeEach(async () => {
460461
harness = await TestHarness.create();
462+
harness.state().withSecretsBinaryInstalled();
461463
});
462464

463465
afterEach(async () => {
@@ -571,6 +573,7 @@ describe('integrate claude — file placement (local vs global)', () => {
571573

572574
beforeEach(async () => {
573575
harness = await TestHarness.create();
576+
harness.state().withSecretsBinaryInstalled();
574577
});
575578

576579
afterEach(async () => {
@@ -873,6 +876,7 @@ describe('integrate claude — legacy state without agentExtensions', () => {
873876

874877
beforeEach(async () => {
875878
harness = await TestHarness.create();
879+
harness.state().withSecretsBinaryInstalled();
876880
});
877881

878882
afterEach(async () => {
@@ -1116,3 +1120,62 @@ describe('integrate — argument validation', () => {
11161120
{ timeout: 15000 },
11171121
);
11181122
});
1123+
1124+
// ─── sonar-secrets auto-install ───────────────────────────────────────────────
1125+
1126+
describe('integrate claude — sonar-secrets auto-install', () => {
1127+
let harness: TestHarness;
1128+
1129+
beforeEach(async () => {
1130+
harness = await TestHarness.create();
1131+
});
1132+
1133+
afterEach(async () => {
1134+
await harness.dispose();
1135+
});
1136+
1137+
it(
1138+
'downloads and installs sonar-secrets when binary is not present',
1139+
async () => {
1140+
const server = await harness
1141+
.newFakeServer()
1142+
.withAuthToken('test-token')
1143+
.withProject('my-project')
1144+
.start();
1145+
await harness.newFakeBinariesServer().start();
1146+
harness.cwd.writeFile(
1147+
'sonar-project.properties',
1148+
[`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=my-project'].join('\n'),
1149+
);
1150+
1151+
const result = await harness.run('integrate claude --token test-token --non-interactive');
1152+
1153+
expect(result.exitCode).toBe(0);
1154+
expect(harness.cliHome.file('bin', 'sonar-secrets').exists()).toBe(true);
1155+
},
1156+
{ timeout: 30000 },
1157+
);
1158+
1159+
it(
1160+
'skips download when sonar-secrets is already installed at the correct version',
1161+
async () => {
1162+
const server = await harness
1163+
.newFakeServer()
1164+
.withAuthToken('test-token')
1165+
.withProject('my-project')
1166+
.start();
1167+
const fakeBinariesServer = await harness.newFakeBinariesServer().start();
1168+
harness.state().withSecretsBinaryInstalled();
1169+
harness.cwd.writeFile(
1170+
'sonar-project.properties',
1171+
[`sonar.host.url=${server.baseUrl()}`, 'sonar.projectKey=my-project'].join('\n'),
1172+
);
1173+
1174+
const result = await harness.run('integrate claude --token test-token --non-interactive');
1175+
1176+
expect(result.exitCode).toBe(0);
1177+
expect(fakeBinariesServer.getRecordedRequests()).toHaveLength(0);
1178+
},
1179+
{ timeout: 30000 },
1180+
);
1181+
});

tests/unit/integrate.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import * as repair from '../../src/cli/commands/integrate/claude/repair';
3131
import * as state from '../../src/cli/commands/integrate/claude/state';
3232
import * as authResolver from '../../src/lib/auth-resolver';
3333
import type { ResolvedAuth } from '../../src/lib/auth-resolver';
34+
import * as installSecrets from '../../src/cli/commands/install/secrets';
3435
import * as migration from '../../src/lib/migration';
3536
import { getDefaultState } from '../../src/lib/state';
3637
import * as stateManager from '../../src/lib/state-manager';
@@ -79,6 +80,9 @@ describe('integrateCommand', () => {
7980
let updateStateAfterConfigurationSpy: Mock<
8081
Extract<(typeof state)['updateStateAfterConfiguration'], (...args: any[]) => any>
8182
>;
83+
let performSecretInstallSpy: Mock<
84+
Extract<(typeof installSecrets)['performSecretInstall'], (...args: any[]) => any>
85+
>;
8286

8387
beforeEach(() => {
8488
setMockUi(true);
@@ -97,6 +101,10 @@ describe('integrateCommand', () => {
97101
runMigrationsSpy = spyOn(migration, 'runMigrations');
98102
updateStateAfterConfigurationSpy = spyOn(state, 'updateStateAfterConfiguration');
99103

104+
performSecretInstallSpy = spyOn(installSecrets, 'performSecretInstall').mockResolvedValue(
105+
'/fake/path/sonar-secrets',
106+
);
107+
100108
mockDiscoveredProject({}); // Default mock to prevent tests from reading the real filesystem. Individual tests are overriding this with specific project data as needed.
101109
mockHealthCheck(); // Default mock to healthy checks. Individual tests are overriding this with specific health data as needed.
102110
});
@@ -114,6 +122,7 @@ describe('integrateCommand', () => {
114122
installHooksSpy.mockRestore();
115123
runMigrationsSpy.mockRestore();
116124
updateStateAfterConfigurationSpy.mockRestore();
125+
performSecretInstallSpy.mockRestore();
117126
});
118127

119128
it('shows intro message', async () => {

0 commit comments

Comments
 (0)