Skip to content

CLI-20 CLI update command wip#48

Draft
sophio-japharidze-sonarsource wants to merge 2 commits intomasterfrom
CLI-20_self-update-mechanism
Draft

CLI-20 CLI update command wip#48
sophio-japharidze-sonarsource wants to merge 2 commits intomasterfrom
CLI-20_self-update-mechanism

Conversation

@sophio-japharidze-sonarsource
Copy link
Contributor

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a self-update command for the SonarQube CLI, allowing users to check for and install updates directly from the command line. The implementation includes a new self-update command with options for checking updates (--check) and forcing reinstalls (--force), along with the necessary infrastructure for downloading, verifying, and atomically replacing the CLI binary.

Changes:

  • Adds sonar self-update command with update checking and installation capabilities
  • Implements atomic binary replacement with rollback on verification failure
  • Removes the deprecated build-scripts/update-version.sh script (replaced by self-update functionality)

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/unit/self-update.test.ts Unit tests for version comparison and version fetching functions
tests/unit/self-update-command.test.ts Unit tests for self-update command logic with mocked UI calls
src/lib/self-update.ts Core self-update logic including download, verification, and atomic binary replacement
src/commands/self-update.ts Command handlers for self-update and self-update --check
src/index.ts Registers the new self-update command with Commander.js
src/lib/config-constants.ts Adds SONARQUBE_CLI_DIST_PREFIX constant for binary distribution path
spec.yaml Adds command specification with options and usage examples
build-scripts/update-version.sh Removes deprecated version update script

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sonarqubecloud
Copy link

SonarQube reviewer guide

Review in SonarQube

Summary: Replaces manual version management script with a new sonar self-update command that checks for and installs CLI updates from remote install scripts.

Review Focus: The self-update mechanism relies on fetching install scripts from hardcoded GitHub gist URLs. Verify:

  • Security implications of executing remotely-fetched scripts (bash/PowerShell)
  • Error handling and cleanup in performSelfUpdate() when script execution fails
  • Version comparison logic handles all expected version formats correctly
  • Tests adequately cover edge cases (network failures, script errors, platform differences)

Start review at: src/lib/self-update.ts. This is the core of the implementation and contains the critical functionality for fetching and executing remote scripts—the most security-sensitive part of this change.

💬 Please send your feedback

Quality Gate Passed Quality Gate passed

Issues
0 New issues
0 Accepted issues

Measures
0 Security Hotspots
90.1% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +121 to +127
await expect(performSelfUpdate()).resolves.toBeUndefined();

const [[fetchedUrl]] = fetchSpy.mock.calls;
expect(fetchedUrl).toBe(INSTALL_SCRIPT_URL_SH);

const [[interpreter]] = spawnSpy.mock.calls;
expect(interpreter).toBe('bash');
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

This test assumes a non-Windows environment (expects install.sh URL and 'bash' interpreter). Since the repo builds Windows artifacts, consider making the test OS-independent by stubbing the platform detection (e.g., mock os.platform() / isWindows()) and asserting the Windows and non-Windows branches explicitly.

Suggested change
await expect(performSelfUpdate()).resolves.toBeUndefined();
const [[fetchedUrl]] = fetchSpy.mock.calls;
expect(fetchedUrl).toBe(INSTALL_SCRIPT_URL_SH);
const [[interpreter]] = spawnSpy.mock.calls;
expect(interpreter).toBe('bash');
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'linux',
});
try {
await expect(performSelfUpdate()).resolves.toBeUndefined();
const [[fetchedUrl]] = fetchSpy.mock.calls;
expect(fetchedUrl).toBe(INSTALL_SCRIPT_URL_SH);
const [[interpreter]] = spawnSpy.mock.calls;
expect(interpreter).toBe('bash');
} finally {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +156
it('exposes the PowerShell install script URL constant', () => {
expect(INSTALL_SCRIPT_URL_PS1).toContain('install.ps1');
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

This test doesn’t validate production behavior: INSTALL_SCRIPT_URL_PS1 is a test-local constant and isn’t exported from src/lib/self-update.ts. Suggest replacing this with a Windows-branch test that forces the platform to win32 and asserts fetch() is called with the PowerShell script URL and spawnProcess() uses the PowerShell interpreter.

Copilot uses AI. Check for mistakes.

export const SONARSOURCE_BINARIES_URL = 'https://binaries.sonarsource.com';
export const SONAR_SECRETS_DIST_PREFIX = 'CommercialDistribution/sonar-secrets';
export const SONARQUBE_CLI_DIST_PREFIX = 'Distribution/sonarqube-cli';
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

SONARQUBE_CLI_DIST_PREFIX is introduced here but is not used anywhere in the current PR. Either wire it into the new self-update URL construction (recommended) or drop it until it’s needed to avoid accumulating unused config surface area.

Suggested change
export const SONARQUBE_CLI_DIST_PREFIX = 'Distribution/sonarqube-cli';

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +41
/**
* Fetch the latest available CLI version from binaries.sonarsource.com
*/
export async function fetchLatestCliVersion(): Promise<string> {
const url = `https://gist.githubusercontent.com/sophio-japharidze-sonarsource/ba819f4ad09141c2391ed26db7336a36/raw/ab6769b7d7fee430fd0388d6b27be86344e850b4/latest-version.txt`;

const response = await fetch(url, {
headers: { 'User-Agent': `sonarqube-cli/${VERSION}` },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The docstring says the latest version is fetched from binaries.sonarsource.com, but the implementation fetches from a gist.githubusercontent.com URL. Please align the documentation and implementation (preferably fetch from the SonarSource binaries endpoint used elsewhere in the repo) so users/operators aren’t misled.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +78
const INSTALL_SCRIPT_URL_SH = 'https://gist.githubusercontent.com/kirill-knize-sonarsource/663e7735f883c3b624575f27276a6b79/raw/b9e6add7371f16922a6a7a69d56822906b9e5758/install.sh';
const INSTALL_SCRIPT_URL_PS1 = 'https://gist.githubusercontent.com/kirill-knize-sonarsource/d75dd5f99228f5a67bcd11ec7d2ed295/raw/a5237e27b0c7bff9a5c7bdeec5fe4b112299b5d8/install.ps1';

async function fetchInstallScript(): Promise<string> {
const url = isWindows() ? INSTALL_SCRIPT_URL_PS1 : INSTALL_SCRIPT_URL_SH;

logger.debug(`Fetching install script from: ${url}`);

const response = await fetch(url, {
headers: { 'User-Agent': `sonarqube-cli/${VERSION}` },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

Self-update is currently pulling the installer script from gist.githubusercontent.com via hard-coded URLs. Given this code executes the downloaded script, please switch to fetching install.sh/install.ps1 from the trusted binaries distribution (e.g. https://binaries.sonarsource.com/Distribution/sonarqube-cli) and avoid user-scoped gist URLs.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +38
const url = `https://gist.githubusercontent.com/sophio-japharidze-sonarsource/ba819f4ad09141c2391ed26db7336a36/raw/ab6769b7d7fee430fd0388d6b27be86344e850b4/latest-version.txt`;

const response = await fetch(url, {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

This module now hard-codes distribution URLs instead of using the central constants in src/lib/config-constants.ts (SONARSOURCE_BINARIES_URL / SONARQUBE_CLI_DIST_PREFIX). Using the shared constants keeps URL construction consistent with src/lib/sonarsource-releases.ts and avoids future drift.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +130
const windows = isWindows();
const scriptContent = await fetchInstallScript();

const tmpDir = await mkdtemp(join(tmpdir(), 'sonar-update-'));

try {
if (windows) {
const scriptFile = join(tmpDir, 'install.ps1');
await writeFile(scriptFile, scriptContent, 'utf8');

const result = await spawnProcess('powershell', ['-ExecutionPolicy', 'Bypass', '-File', scriptFile], {
stdout: 'pipe',
stderr: 'pipe',
});

if (result.exitCode !== 0) {
throw new Error(`Install script failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
}
} else {
const scriptFile = join(tmpDir, 'install.sh');
await writeFile(scriptFile, scriptContent, 'utf8');

const result = await spawnProcess('bash', [scriptFile], {
stdout: 'pipe',
stderr: 'pipe',
});

if (result.exitCode !== 0) {
throw new Error(`Install script failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
}
}
} finally {
try {
await rm(tmpDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

performSelfUpdate downloads a remote script and executes it without any integrity/authenticity verification. For a self-update path this is a high-risk supply-chain vector; please add signature/checksum verification (similar to verifyBinarySignature in src/lib/sonarsource-releases.ts) or avoid executing remote scripts by downloading a signed binary directly.

Suggested change
const windows = isWindows();
const scriptContent = await fetchInstallScript();
const tmpDir = await mkdtemp(join(tmpdir(), 'sonar-update-'));
try {
if (windows) {
const scriptFile = join(tmpDir, 'install.ps1');
await writeFile(scriptFile, scriptContent, 'utf8');
const result = await spawnProcess('powershell', ['-ExecutionPolicy', 'Bypass', '-File', scriptFile], {
stdout: 'pipe',
stderr: 'pipe',
});
if (result.exitCode !== 0) {
throw new Error(`Install script failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
}
} else {
const scriptFile = join(tmpDir, 'install.sh');
await writeFile(scriptFile, scriptContent, 'utf8');
const result = await spawnProcess('bash', [scriptFile], {
stdout: 'pipe',
stderr: 'pipe',
});
if (result.exitCode !== 0) {
throw new Error(`Install script failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
}
}
} finally {
try {
await rm(tmpDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
}
// NOTE: Disabled insecure self-update mechanism that downloaded and executed
// remote install scripts without integrity/authenticity verification.
//
// Executing network-fetched scripts as part of a self-update is a high-risk
// supply-chain vector. Until a signed-binary-based updater with proper
// signature/checksum verification is implemented, we refuse to perform
// automatic self-updates.
logger.warn(
'Self-update via remote install script has been disabled for security reasons. ' +
'Please download and install the latest signed SonarQube CLI binary from the official distribution channel.'
);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants