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
16 changes: 12 additions & 4 deletions .github/actions/run-integration-and-e2e/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,25 @@ runs:
run: SONARQUBE_CLI_USE_COVERAGE=1 bun run test:integration

# Workaround for CAG-933: CAG deduces the fake SonarQube Cloud API endpoint
# as api.localhost. That resolution is platform-specific; Windows CI does not
# resolve it by default. Pin api.localhost to loopback so the e2e test
# "forwards a Claude Bash hook payload to the real CAG binary and applies
# Gradle compression" can reach the fake server until CAG fixes this.
# as api.localhost. GitHub-hosted Ubuntu resolves *.localhost per RFC 6761;
# Windows and some self-hosted Linux images (sonar-m) do not. Pin
# api.localhost to loopback so the e2e tests that exercise Gradle
# compression can reach the fake server until CAG fixes this.
- name: Resolve fake SonarQube Cloud API host (Windows)
if: ${{ inputs.os == 'windows' }}
shell: powershell
run: |
Add-Content -Path "$env:windir\System32\drivers\etc\hosts" -Value "`n127.0.0.1 api.localhost`n::1 api.localhost"
Resolve-DnsName api.localhost

- name: Resolve fake SonarQube Cloud API host (Linux)
if: ${{ inputs.os == 'linux' }}
shell: bash
run: |
echo '127.0.0.1 api.localhost' | sudo tee -a /etc/hosts
echo '::1 api.localhost' | sudo tee -a /etc/hosts
getent hosts api.localhost

- name: Run e2e tests (Windows)
if: ${{ inputs.os == 'windows' }}
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/PullRequestClosed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
jobs:
PullRequestMerged_job:
name: Pull Request Merged
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
permissions:
id-token: write
pull-requests: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/PullRequestCreated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
jobs:
PullRequestCreated_job:
name: Pull Request Created
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
permissions:
id-token: write
# For external PR, ticket should be created manually
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/SubmitReview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
jobs:
SubmitReview_job:
name: Submit Review
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
permissions:
id-token: write
# For external PR, ticket should be moved manually
Expand Down
18 changes: 10 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ permissions:
jobs:
prepare:
name: Prepare Build
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
outputs:
BUILD_NUMBER: ${{ steps.get-build-number.outputs.BUILD_NUMBER }}
PROJECT_VERSION: ${{ steps.project_version.outputs.PROJECT_VERSION }}
Expand All @@ -49,7 +49,7 @@ jobs:

build-binaries:
name: Build All Binaries
runs-on: github-ubuntu-latest-m
runs-on: sonar-m
needs: prepare
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -79,6 +79,8 @@ jobs:
shell: bash
env:
PROJECT_VERSION: ${{ needs.prepare.outputs.PROJECT_VERSION }}
ARTIFACTORY_PRIVATE_READER_PASSWORD: ${{ fromJSON(steps.secrets.outputs.vault).ARTIFACTORY_ACCESS_TOKEN }}
BUN_CONFIG_REGISTRY: https://repox.jfrog.io/artifactory/api/npm/npm/
run: |
set -euo pipefail

Expand Down Expand Up @@ -245,7 +247,7 @@ jobs:

publish-binaries:
name: Publish Binaries to Artifactory
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
needs:
- prepare
- build-binaries
Expand Down Expand Up @@ -335,7 +337,7 @@ jobs:

lint:
name: Lint
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand Down Expand Up @@ -364,7 +366,7 @@ jobs:

unit-tests-linux:
name: Unit Tests - linux
runs-on: github-ubuntu-latest-m
runs-on: sonar-m
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand Down Expand Up @@ -419,7 +421,7 @@ jobs:

integration-and-e2e-linux:
name: Integration and E2E Tests - linux
runs-on: github-ubuntu-latest-m
runs-on: sonar-m
needs: [prepare, build-binaries]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -473,7 +475,7 @@ jobs:

scan:
name: SonarQube Analysis
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
# Coverage artifacts are Linux-only; Windows tests still gate publish and promote.
needs: [prepare, lint, unit-tests-linux, integration-and-e2e-linux]
steps:
Expand Down Expand Up @@ -521,7 +523,7 @@ jobs:
.github/scripts/sonarqube-analysis.sh

promote:
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
name: Promote
needs:
- prepare
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/full-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:
jobs:
check-draft-exists:
name: Check draft release exists
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
# contents: write is required — draft releases are only visible to tokens with write access.
permissions:
contents: write
Expand Down Expand Up @@ -66,7 +66,7 @@ jobs:

bump-version:
name: Create a PR to bump version
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
needs:
- release
permissions:
Expand Down Expand Up @@ -126,7 +126,7 @@ jobs:

publish-release-notes:
name: Copy reviewed notes to published release
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
needs: [check-draft-exists, release]
permissions:
contents: write
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/notify-failure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ permissions:

jobs:
notify:
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
name: Send Slack Notification
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prepare-release-notes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
jobs:
prepare-release-notes:
name: Generate notes and create draft release
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
permissions:
id-token: write # required for Vault OIDC auth
contents: write # required to create the draft GitHub release
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/releasability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:

jobs:
update_releasability_status:
runs-on: github-ubuntu-latest-s
runs-on: sonar-xs
name: Releasability status
permissions:
id-token: write
Expand Down
52 changes: 40 additions & 12 deletions build-scripts/build-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import { join } from 'node:path';

import { resolveDistribution } from '@/core/host/distribution.ts';

import {
compileTargetMatchesHost,
downloadCompileTargetExecutable,
REPOX_NPM_REGISTRY,
} from './compile-target-runtime.ts';

const PROJECT_ROOT = join(import.meta.dir, '..');
const DEFAULT_OUTFILE = join(PROJECT_ROOT, 'dist', 'sonarqube-cli');
const DISTRIBUTION_DEFINE_KEY = 'process.env.SONARQUBE_CLI_DISTRIBUTION';
Expand All @@ -40,18 +46,40 @@ const target = process.env.SONARQUBE_CLI_TARGET as BuildTarget | undefined;

console.log(`Building CLI binary for distribution: ${distribution} (${target ?? 'host'})`);

const result = await Bun.build({
entrypoints: [join(PROJECT_ROOT, 'src/index.ts')],
compile: target ? { target, outfile } : { outfile },
define: {
[DISTRIBUTION_DEFINE_KEY]: JSON.stringify(distribution),
},
});

if (!result.success) {
const logs = result.logs.map((log) => log.message ?? JSON.stringify(log)).join('\n');
process.stderr.write(`${logs || 'Failed to build CLI binary'}\n`);
process.exit(1);
const compile: { target?: BuildTarget; outfile: string; executablePath?: string } = target
? { target, outfile }
: { outfile };

let cleanupCompileRuntime: (() => void) | undefined;
const artifactoryToken = process.env.ARTIFACTORY_PRIVATE_READER_PASSWORD;
if (target && artifactoryToken && !compileTargetMatchesHost(target)) {
const { executablePath, cleanup } = await downloadCompileTargetExecutable({
target,
bunVersion: Bun.version,
registryUrl: process.env.BUN_CONFIG_REGISTRY ?? REPOX_NPM_REGISTRY,
token: artifactoryToken,
});
compile.executablePath = executablePath;
cleanupCompileRuntime = cleanup;
console.log(`Using compile runtime from Repox: ${executablePath}`);
}

try {
const result = await Bun.build({
entrypoints: [join(PROJECT_ROOT, 'src/index.ts')],
compile,
define: {
[DISTRIBUTION_DEFINE_KEY]: JSON.stringify(distribution),
},
});

if (!result.success) {
const logs = result.logs.map((log) => log.message ?? JSON.stringify(log)).join('\n');
process.stderr.write(`${logs || 'Failed to build CLI binary'}\n`);
process.exit(1);
}
} finally {
cleanupCompileRuntime?.();
}

console.log(`CLI binary built: ${outfile}`);
139 changes: 139 additions & 0 deletions build-scripts/compile-target-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* SonarQube CLI
* Copyright (C) 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.
*/

import { chmodSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

/**
* Virtual npm repo used by bunfig.toml `[install].registry`. bun compile does
* not honor that setting (it hardcodes registry.npmjs.org), so cross-compile
* fetches the `@oven/bun-*` tarball from here and passes `executablePath`.
*/
export const REPOX_NPM_REGISTRY = 'https://repox.jfrog.io/artifactory/api/npm/npm';

const COMPILE_TARGET_NPM_PACKAGES: Record<string, string> = {
'bun-linux-x64': 'bun-linux-x64',
'bun-linux-arm64': 'bun-linux-aarch64',
'bun-linux-aarch64': 'bun-linux-aarch64',
'bun-darwin-arm64': 'bun-darwin-aarch64',
'bun-darwin-aarch64': 'bun-darwin-aarch64',
'bun-windows-x64': 'bun-windows-x64',
};

export function npmPackageForCompileTarget(target: string): string | undefined {
return COMPILE_TARGET_NPM_PACKAGES[target];
}

const UNIX_EXECUTABLE_MODE = 0o755;

function hostCompileOs(): 'darwin' | 'windows' | 'linux' {
if (process.platform === 'darwin') {
return 'darwin';
}
if (process.platform === 'win32') {
return 'windows';
}
return 'linux';
}

function hostCompileArch(): 'arm64' | 'x64' {
return process.arch === 'arm64' ? 'arm64' : 'x64';
}

export function compileTargetMatchesHost(target: string): boolean {
const os = hostCompileOs();
const arch = hostCompileArch();
return target === `bun-${os}-${arch}` || (arch === 'arm64' && target === `bun-${os}-aarch64`);
}

export function compileTargetTarballUrl(
registryUrl: string,
npmPackage: string,
version: string,
): string {
const base = registryUrl.replace(/\/$/, '');
return `${base}/@oven/${npmPackage}/-/${npmPackage}-${version}.tgz`;
}

export async function downloadCompileTargetExecutable(options: {
target: string;
bunVersion: string;
registryUrl: string;
token: string;
fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;
}): Promise<{ executablePath: string; cleanup: () => void }> {
const npmPackage = npmPackageForCompileTarget(options.target);
if (!npmPackage) {
throw new Error(`No npm package mapping for compile target '${options.target}'`);
}

const url = compileTargetTarballUrl(options.registryUrl, npmPackage, options.bunVersion);
const extractDir = mkdtempSync(join(tmpdir(), 'bun-compile-target-'));
const cleanup = (): void => {
rmSync(extractDir, { recursive: true, force: true });
};

try {
const fetchImpl = options.fetchImpl ?? fetch;
const response = await fetchImpl(url, {
headers: { Authorization: `Bearer ${options.token}` },
});
if (!response.ok) {
throw new Error(
`Failed to download @oven/${npmPackage}@${options.bunVersion} from Repox: HTTP ${response.status}`,
);
}

const tarballPath = join(extractDir, 'bun.tgz');
await Bun.write(tarballPath, response);

const extracted = Bun.spawnSync(['tar', '-xzf', tarballPath, '-C', extractDir], {
stderr: 'pipe',
});
if (extracted.exitCode !== 0) {
const detail = extracted.stderr.toString().trim();
const message = `Failed to extract @oven/${npmPackage}@${options.bunVersion} tarball`;
throw new Error(detail ? `${message}: ${detail}` : message);
}

const unixBin = join(extractDir, 'package', 'bin', 'bun');
const windowsBin = join(extractDir, 'package', 'bin', 'bun.exe');
let executablePath: string | undefined;
if (existsSync(unixBin)) {
executablePath = unixBin;
} else if (existsSync(windowsBin)) {
executablePath = windowsBin;
}
if (!executablePath) {
throw new Error(
`Extracted @oven/${npmPackage}@${options.bunVersion} tarball is missing package/bin/bun`,
);
}
if (executablePath === unixBin) {
chmodSync(executablePath, UNIX_EXECUTABLE_MODE);
}

return { executablePath, cleanup };
} catch (error) {
cleanup();
throw error;
}
}
Loading