Skip to content
Draft
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
88 changes: 0 additions & 88 deletions build-scripts/update-version.sh

This file was deleted.

25 changes: 25 additions & 0 deletions spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,31 @@ commands:
- command: sonar list projects --page 2 --page-size 50
description: Paginate through projects

# Self-update the CLI
- name: self-update
description: Update the sonar CLI to the latest version
handler: ./src/commands/self-update.ts
options:
- name: check
type: boolean
description: Check for updates without installing
default: false

- name: force
type: boolean
description: Force reinstall even if already on the latest version
default: false

examples:
- command: sonar self-update
description: Update the CLI to the latest version

- command: sonar self-update --check
description: Check if an update is available without installing

- command: sonar self-update --force
description: Reinstall the latest version even if already up to date

# Analyze code for security issues
- name: analyze
description: Analyze code for security issues
Expand Down
97 changes: 97 additions & 0 deletions src/commands/self-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* SonarQube CLI
* Copyright (C) 2026 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

// CLI self-update command

import { version as VERSION } from '../../package.json';
import {
fetchLatestCliVersion,
compareVersions,
performSelfUpdate,
} from '../lib/self-update.js';
import { text, blank, success, warn, info, withSpinner } from '../ui/index.js';

/**
* Core version check: compare current version against latest, return update info.
*/
async function checkVersion(): Promise<{
current: string;
latest: string;
hasUpdate: boolean;
}> {
const latest = await withSpinner('Checking for updates', fetchLatestCliVersion);
const hasUpdate = compareVersions(latest, VERSION) > 0;
return { current: VERSION, latest, hasUpdate };
}

/**
* sonar self-update --check
* Check for updates and notify the user, but do not install.
*/
export async function selfUpdateCheckCommand(): Promise<void> {
text(`\nCurrent version: v${VERSION}\n`);

const { latest, hasUpdate } = await checkVersion();

if (!hasUpdate) {
blank();
success(`Already on the latest version (v${VERSION})`);
return;
}

blank();
warn(`Update available: v${VERSION} → v${latest}`);
text(` Run: sonar self-update`);
}

/**
* sonar self-update
* Check for updates and install if one is available (binary installs only).
* With --force, reinstall even if already on the latest version.
*/
export async function selfUpdateCommand(options: { check?: boolean; force?: boolean }): Promise<void> {
if (options.check) {
await selfUpdateCheckCommand();
return;
}

text(`\nCurrent version: v${VERSION}\n`);

const { latest, hasUpdate } = await checkVersion();

if (options.force) {
info(`Forcing reinstall of v${latest}`);
} else if (hasUpdate) {
info(`Update available: v${VERSION} → v${latest}`);
} else {
blank();
success(`Already on the latest version (v${VERSION})`);
return;
}

blank();
await withSpinner(
`Installing v${latest}`,
performSelfUpdate
);

blank();
success(`Updated to v${latest}`);
}
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { authLoginCommand, authLogoutCommand, authPurgeCommand, authStatusComman
import { secretInstallCommand, secretStatusCommand } from './commands/secret.js';
import { analyzeSecretsCommand } from './commands/analyze.js';
import { projectsSearchCommand } from './commands/projects.js';
import { selfUpdateCommand } from './commands/self-update.js';

const program = new Command();

Expand Down Expand Up @@ -154,6 +155,16 @@ auth
await authStatusCommand();
});

// Check for and install CLI updates
program
.command('self-update')
.description('Check for and install latest SonarQube CLI')
.option('--check', 'Check for updates without installing')
.option('--force', 'Force update even if already on the latest version')
.action(async (options) => {
await runCommand(() => selfUpdateCommand(options));
});

// Analyze code for security issues
const analyze = program
.command('analyze')
Expand Down
1 change: 1 addition & 0 deletions src/lib/config-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const BIN_DIR = join(CLI_DIR, 'bin');

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.

// ---------------------------------------------------------------------------
// SonarCloud
Expand Down
131 changes: 131 additions & 0 deletions src/lib/self-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* SonarQube CLI
* Copyright (C) 2026 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

// Self-update logic for the sonarqube-cli binary

import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir, platform } from 'node:os';
import { spawnProcess } from './process.js';
import { version as VERSION } from '../../package.json';
import logger from './logger.js';

const REQUEST_TIMEOUT_MS = 30000;

/**
* 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, {
Comment on lines +36 to +38
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.
headers: { 'User-Agent': `sonarqube-cli/${VERSION}` },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
Comment on lines +32 to +41
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.

if (!response.ok) {
throw new Error(`Failed to fetch latest version: ${response.status} ${response.statusText}`);
}

const version = (await response.text()).trim();
if (!version) {
throw new Error('Could not determine latest version');
}

return version;
}

/**
* Compare two dot-separated version strings numerically.
* Returns negative if a < b, positive if a > b, 0 if equal.
*/
export function compareVersions(a: string, b: string): number {
return a.localeCompare(b, undefined, { numeric: true });
}

function isWindows(): boolean {
return platform() === 'win32';
}

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),
});
Comment on lines +67 to +78
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.

if (!response.ok) {
throw new Error(`Failed to fetch install script: ${response.status} ${response.statusText}`);
}

return response.text();
}

/**
* Perform a self-update by fetching and running the platform install script.
* On Unix/macOS: downloads install.sh and runs it with bash.
* On Windows: downloads install.ps1 and runs it with PowerShell.
*/
export async function performSelfUpdate(): Promise<void> {
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
}
}
Comment on lines +93 to +130
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.
}
6 changes: 3 additions & 3 deletions tests/unit/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@

import { it, expect } from 'bun:test';

import { mkdirSync, rmSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { mkdirSync, rmSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { loadConfig, saveConfig, newConfig } from '../../src/bootstrap/config.js';

it('config: save and load', async () => {
Expand Down
Loading