Skip to content

Commit 8d320f6

Browse files
CLI-244 sonar callback — command infrastructure
1 parent 8e2ad74 commit 8d320f6

4 files changed

Lines changed: 130 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Please use the exception types defined in `src/cli/commands/_common/error.ts` fo
5050

5151
## Tests
5252

53-
Please try to create integration tests in priority. If the test is too complicated to set up, write unit tests.
53+
Always prefer end-to-end integration tests. Unit tests are a last resort — only when e2e is genuinely impractical (e.g. the dependency cannot be controlled or isolated at all).
5454
Try to get inspiration from other tests to follow the same structure.
5555

5656
- Unit tests: `tests/unit/` — run with `bun test:unit`

src/cli/command-tree.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,15 @@ COMMAND_TREE.command('self-update')
229229
.option('--force', 'Install the latest version even if already up to date')
230230
.anonymousAction((options: SelfUpdateOptions) => selfUpdate(options));
231231

232+
// Hidden callback command — internal handlers for agent and git hooks.
233+
// Shell hook scripts call `sonar callback <event>` to delegate all business logic to TypeScript.
234+
export const callbackCommand = COMMAND_TREE.command('callback', { hidden: true })
235+
.description('Internal callback handlers for agent and git hooks')
236+
.enablePositionalOptions()
237+
.anonymousAction(function (this: Command) {
238+
this.outputHelp();
239+
});
240+
232241
// Hidden flush command — only registered when running as a telemetry worker.
233242
if (process.env[TELEMETRY_FLUSH_MODE_ENV]) {
234243
COMMAND_TREE.command('flush-telemetry', { hidden: true }).anonymousAction(flushTelemetry);

src/cli/commands/callback/stdin.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* SonarQube CLI
3+
* Copyright (C) 2026 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
// Stdin reader used by all sonar callback handlers.
22+
// Hook agents pipe their JSON event payload to the callback process via stdin.
23+
24+
const STDIN_TIMEOUT_MS = 5000;
25+
26+
/**
27+
* Read stdin, parse as JSON, and return the result typed as T.
28+
* Throws if stdin is not valid JSON or if the read times out.
29+
*/
30+
export async function readStdinJson<T>(): Promise<T> {
31+
const raw = await readRawStdin();
32+
try {
33+
return JSON.parse(raw) as T;
34+
} catch {
35+
throw new Error('Failed to parse stdin as JSON');
36+
}
37+
}
38+
39+
async function readRawStdin(): Promise<string> {
40+
return Promise.race([
41+
new Promise<string>((resolve, reject) => {
42+
const chunks: Buffer[] = [];
43+
process.stdin.on('data', (chunk: Buffer) => chunks.push(chunk));
44+
process.stdin.on('end', () => {
45+
resolve(Buffer.concat(chunks).toString('utf-8'));
46+
});
47+
process.stdin.on('error', reject);
48+
}),
49+
new Promise<never>((_, reject) =>
50+
setTimeout(() => {
51+
reject(new Error(`stdin read timed out after ${STDIN_TIMEOUT_MS}ms`));
52+
}, STDIN_TIMEOUT_MS),
53+
),
54+
]);
55+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* SonarQube CLI
3+
* Copyright (C) 2026 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
// Integration tests for `sonar hook` command infrastructure
22+
23+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
24+
import { TestHarness } from '../../harness';
25+
26+
describe('sonar hook', () => {
27+
let harness: TestHarness;
28+
29+
beforeEach(async () => {
30+
harness = await TestHarness.create();
31+
});
32+
33+
afterEach(async () => {
34+
await harness.dispose();
35+
});
36+
37+
it(
38+
'is hidden — does not appear in root help',
39+
async () => {
40+
const result = await harness.run('');
41+
expect(result.exitCode).toBe(0);
42+
expect(result.stdout).not.toContain('callback');
43+
},
44+
{ timeout: 15000 },
45+
);
46+
47+
it(
48+
'sonar hook --help exits 0 and shows usage',
49+
async () => {
50+
const result = await harness.run('hook --help');
51+
expect(result.exitCode).toBe(0);
52+
expect(result.stdout).toContain('hook');
53+
},
54+
{ timeout: 15000 },
55+
);
56+
57+
it(
58+
'sonar hook exits 0 (shows help)',
59+
async () => {
60+
const result = await harness.run('hook');
61+
expect(result.exitCode).toBe(0);
62+
},
63+
{ timeout: 15000 },
64+
);
65+
});

0 commit comments

Comments
 (0)