Skip to content
Merged
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
9 changes: 1 addition & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
run: |
npm run build
npx knip
(cd build && npm pack)
env:
VERSION: SNAPSHOT

Expand Down Expand Up @@ -90,14 +91,6 @@ jobs:
npm config set //repox.jfrog.io/artifactory/api/npm/:_authToken=${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_ACCESS_TOKEN }}
npm config set registry https://repox.jfrog.io/artifactory/api/npm/npm/

- name: Setup integration test
run: |
(cd build && npm pack)
cp build/sonar-scan-SNAPSHOT.tgz test/integration
(cd test/integration && npm install --no-save sonar-scan-SNAPSHOT.tgz)
(cd tools/orchestrator && npm run build)
shell: bash

Comment on lines -93 to -100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

- name: Run integration tests
env:
ARTIFACTORY_ACCESS_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_DEPLOY_ACCESS_TOKEN }}
Expand Down
176 changes: 176 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

`@sonar/scan` is an NPM module that triggers SonarQube Server and SonarCloud analyses on JavaScript codebases without requiring specific tools or Java runtime installation. The scanner detects server capabilities and either uses JRE provisioning (preferred, SonarQube 10.6+) or falls back to native sonar-scanner-cli.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove any mention of versions in the this md file


## Common Commands

| Command | Purpose |
| -------------------------- | ------------------------------------------------------------------ |
| `npm run build` | Compile TypeScript, run license check, generate build/package.json |
| `npm test` | Run Jest unit tests with coverage |
| `npm run test-integration` | Run integration tests from test/integration/ |
| `npm run format` | Format code with Prettier |
| `npm run check-format` | Check formatting without changes |
| `npm run license-fix` | Auto-fix missing license headers |

Run a single test file:

```bash
npx jest test/unit/properties.test.ts
```

Run tests matching a pattern:

```bash
npx jest --testNamePattern="should build properties"
```

## Architecture

### Core Flow (src/scan.ts)

1. **Configuration**: Build scanner properties from env vars, files, CLI args, defaults
2. **Server Detection**: Check server version to determine JRE provisioning support
3. **Execution**: Either provision JRE + run Scanner Engine JAR, or fallback to sonar-scanner-cli

### Key Files

| File | Purpose |
| ----------------------- | ------------------------------------------------------------- |
| `src/index.ts` | Public API: `scan()`, `customScanner()`, `scanWithCallback()` |
| `src/runner.ts` | CLI entry point (commander-based) |
| `src/scan.ts` | Main orchestration logic |
| `src/scanner-engine.ts` | Download and run Scanner Engine JAR |
| `src/java.ts` | JRE detection, provisioning, version checking |
| `src/properties.ts` | Configuration building from multiple sources |
| `src/scanner-cli.ts` | Fallback to native sonar-scanner-cli |
| `src/request.ts` | HTTP requests/downloads with proxy support |
| `src/types.ts` | TypeScript interfaces and enums |

### Configuration Priority

Properties are resolved in this order (highest to lowest):

1. ScanOptions passed to `scan()`
2. CLI arguments (-D properties)
3. Environment variables (SONAR*\* or npm_config_sonar*\*)
4. sonar-project.properties file
5. package.json sonar config
6. Default values

All configuration uses `ScannerProperty` enum (src/types.ts) and `ScannerProperties` map.

### Caching

Downloads (JRE, Scanner Engine) cached in `~/.sonar/cache/` with SHA256 validation.

## Code Conventions

### License Headers

Every source file requires LGPL-3.0-only header. Run `npm run license-fix` to auto-add. ESLint enforces this.

### Logging

Use structured logging from src/logging.ts:

```typescript
log(LogLevel.DEBUG, 'message', ...args);
logWithPrefix(LogLevel.INFO, 'ComponentName', 'message');
```

### Formatting

Prettier config: 100 char width, trailing commas, single quotes, LF line endings. Pre-commit hook auto-formats staged files.

## Testing

### Unit Tests

Location: `test/unit/`

```
test/unit/
├── tsconfig.json # TypeScript config for unit tests
├── setup.ts # Jest setup - mocks logging to suppress output
├── mocks/ # Test mocks (ChildProcessMock, FakeProjectMock)
├── fixtures/ # Test fixture projects
└── *.test.ts # Test files
```

Run a single unit test:

```bash
npx jest test/unit/properties.test.ts
npx jest --testNamePattern="should build properties"
```

### Integration Tests

Integration tests run end-to-end scans against a real SonarQube instance. Uses Node's native test runner with tsx.

**Structure:**

```
test/integration/
├── package.json # Separate npm project (ESM, tsx, node:test)
├── tsconfig.json # TypeScript config for integration tests
├── scanner.test.ts # Integration tests (API and CLI)
├── orchestrator/ # SonarQube lifecycle management
│ ├── download.ts # Download SonarQube
│ ├── sonarqube.ts # Start/stop, API calls
│ ├── index.ts # Exports
│ └── stop.java # Java helper to stop SonarQube
└── fixtures/
└── fake_project_for_integration/
└── src/index.js # Test project with intentional code issue
```

**How to run:**

```bash
# From project root (requires build first)
npm run build && npm run test-integration

# From integration directory
cd test/integration && npm test
```

**Orchestrator functions (from `test/integration/orchestrator/`):**

Manages a local SonarQube instance:

- Downloads SonarQube Community Edition (cached in `~/.sonar/sonarqube/`)
- Starts/stops SonarQube on localhost:9000
- Generates authentication tokens
- Creates test projects
- Polls for analysis completion

Key functions:

- `getLatestSonarQube()` - Download/cache SonarQube
- `startAndReady(sqPath, maxWaitMs)` - Start and wait until operational
- `stop(sqPath)` - Stop SonarQube instance
- `generateToken()` - Generate GLOBAL_ANALYSIS_TOKEN
- `createProject()` - Create project with random key
- `waitForAnalysisFinished(maxWaitMs)` - Poll until analysis queue empty
- `getIssues(projectKey)` - Fetch detected issues
Comment on lines +155 to +161
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that a bit too much specifics of the implementation?


**What the integration test validates:**

1. SonarQube provisioning and startup
2. Token generation and project creation
3. Scanner invocation via API (`scan()` function)
4. Scanner invocation via CLI (`npx sonar`)
5. Issue detection (fake project has intentional issue at line 21)
6. Result verification (asserts exactly 1 issue found)

**Configuration:**

- Timeout: 500 seconds (SonarQube startup is slow)
- SonarQube credentials: admin:admin (hardcoded in orchestrator)
- Optional env vars: `ARTIFACTORY_URL`, `ARTIFACTORY_ACCESS_TOKEN` for SonarQube download
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ module.exports = {
testResultsProcessor: 'jest-sonar-reporter',
testMatch: ['<rootDir>/test/unit/**/*.test.ts'],
testTimeout: 20000,
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
setupFilesAfterEnv: ['<rootDir>/test/unit/setup.ts'],
};
8 changes: 4 additions & 4 deletions knip.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"workspaces": {
".": {
"entry": ["bin/sonar-scanner.js", "test/integration/scanner.test.js", "src/index.ts"],
"entry": ["bin/sonar-scanner.js", "src/index.ts"],
"project": "src/*.ts"
},
"tools/orchestrator": {
"entry": ["scripts/*"],
"ignore": ["dist/**/*"]
"test/integration": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should unit tests be also included here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will once i remove jest

"entry": ["scanner.test.ts"],
"project": "**/*.ts"
}
},
"ignore": ["build/**/*", "**/fixtures/**/*", "scripts/ci-analysis.js"],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"scripts": {
"build": "npm run license && tsc && tsx scripts/generate-package-json.ts",
"test": "node_modules/.bin/jest --coverage",
"test-integration": "cd test/integration && npm test",
"test-integration": "cd test/integration && npm install && npm test",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this command change where the user is? perhaps it should be in parenthesis?

"format": "prettier --write .",
"check-format": "prettier --list-different .",
"license": "eslint",
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci-analysis.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
const path = require('path');
const path = require('node:path');

// Regular users will call 'require('@sonar/scan')' - but not here: eat your own dog food! :-)
const scanner = require('../build').scan;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import * as https from 'https';
import * as fs from 'fs';
import { execSync } from 'child_process';
import * as path from 'path';
import * as https from 'node:https';
import * as fs from 'node:fs';
import { execSync } from 'node:child_process';
import * as path from 'node:path';
import * as mkdirp from 'mkdirp';
import * as os from 'os';
import * as os from 'node:os';
Comment on lines +21 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have this as a rule already? or was it accepted?


const DEFAULT_VERSION = '9.7.1.62043';
const ARTIFACTORY_URL = process.env.ARTIFACTORY_URL || 'https://repox.jfrog.io';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

export * from './download';
export * from './sonarqube';
export * from './download.js';
export * from './sonarqube.js';
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import * as path from 'path';
import { ChildProcess, spawn, exec } from 'child_process';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { ChildProcess, spawn, exec } from 'node:child_process';
import axios from 'axios';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const DEFAULT_FOLDER = path.join(
__dirname,
'..',
Expand Down Expand Up @@ -165,7 +169,8 @@ async function isSonarQubeReady(logs: string[], startIndex: number): Promise<any
* @returns
*/
export function stop(sqPath: string = DEFAULT_FOLDER) {
const cp = exec(`java ${__dirname}/stop.java ${sqPath}`, undefined, (error, stdout, stderr) => {
const stopJavaPath = path.join(__dirname, 'stop.java');
const cp = exec(`java ${stopJavaPath} ${sqPath}`, undefined, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
Expand Down
Loading
Loading