CLI-20 CLI update command wip#48
CLI-20 CLI update command wip#48sophio-japharidze-sonarsource wants to merge 2 commits intomasterfrom
Conversation
c0ff0ea to
aeee352
Compare
There was a problem hiding this comment.
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-updatecommand with update checking and installation capabilities - Implements atomic binary replacement with rollback on verification failure
- Removes the deprecated
build-scripts/update-version.shscript (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.
aeee352 to
ccb531a
Compare
1262aa7 to
2a86446
Compare
SonarQube reviewer guideSummary: Replaces manual version management script with a new Review Focus: The self-update mechanism relies on fetching install scripts from hardcoded GitHub gist URLs. Verify:
Start review at:
|
There was a problem hiding this comment.
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.
| 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'); |
There was a problem hiding this comment.
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.
| 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, | |
| }); | |
| } |
| it('exposes the PowerShell install script URL constant', () => { | ||
| expect(INSTALL_SCRIPT_URL_PS1).toContain('install.ps1'); | ||
| }); |
There was a problem hiding this comment.
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.
|
|
||
| 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'; |
There was a problem hiding this comment.
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.
| export const SONARQUBE_CLI_DIST_PREFIX = 'Distribution/sonarqube-cli'; |
| /** | ||
| * 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), | ||
| }); |
There was a problem hiding this comment.
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.
| 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), | ||
| }); |
There was a problem hiding this comment.
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.
| const url = `https://gist.githubusercontent.com/sophio-japharidze-sonarsource/ba819f4ad09141c2391ed26db7336a36/raw/ab6769b7d7fee430fd0388d6b27be86344e850b4/latest-version.txt`; | ||
|
|
||
| const response = await fetch(url, { |
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.' | |
| ); |




No description provided.