From 762c6bf1ed9ae12179013ba190f5595f41af7d70 Mon Sep 17 00:00:00 2001 From: Jayadeep Kinavoor Madam Date: Fri, 4 Sep 2026 15:25:57 +0200 Subject: [PATCH 1/4] BUILD-12308 Migrate eligible jobs to self-hosted runners Move eligible Linux workflows to SonarSource self-hosted runners. Keep macOS signing and generated agent workflows unchanged pending supported migration paths. --- .github/workflows/PullRequestClosed.yml | 2 +- .github/workflows/PullRequestCreated.yml | 2 +- .github/workflows/SubmitReview.yml | 2 +- .github/workflows/build.yml | 16 ++++++++-------- .github/workflows/full-release.yml | 6 +++--- .github/workflows/notify-failure.yml | 2 +- .github/workflows/prepare-release-notes.yml | 2 +- .github/workflows/releasability.yml | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/PullRequestClosed.yml b/.github/workflows/PullRequestClosed.yml index 7ed1687d4..f1482e590 100644 --- a/.github/workflows/PullRequestClosed.yml +++ b/.github/workflows/PullRequestClosed.yml @@ -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 diff --git a/.github/workflows/PullRequestCreated.yml b/.github/workflows/PullRequestCreated.yml index 275b2e0f7..94610d84f 100644 --- a/.github/workflows/PullRequestCreated.yml +++ b/.github/workflows/PullRequestCreated.yml @@ -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 diff --git a/.github/workflows/SubmitReview.yml b/.github/workflows/SubmitReview.yml index 9f4b748a8..9cb936ede 100644 --- a/.github/workflows/SubmitReview.yml +++ b/.github/workflows/SubmitReview.yml @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f162e5129..ba197cfa9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} @@ -49,7 +49,7 @@ jobs: build-binaries: name: Build All Binaries - runs-on: github-ubuntu-latest-m + runs-on: warp-custom-ubuntu-24-04 needs: prepare steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -245,7 +245,7 @@ jobs: publish-binaries: name: Publish Binaries to Artifactory - runs-on: github-ubuntu-latest-s + runs-on: sonar-xs needs: - prepare - build-binaries @@ -335,7 +335,7 @@ jobs: lint: name: Lint - runs-on: github-ubuntu-latest-s + runs-on: sonar-xs steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -364,7 +364,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 @@ -419,7 +419,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 @@ -473,7 +473,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: @@ -521,7 +521,7 @@ jobs: .github/scripts/sonarqube-analysis.sh promote: - runs-on: github-ubuntu-latest-s + runs-on: sonar-xs name: Promote needs: - prepare diff --git a/.github/workflows/full-release.yml b/.github/workflows/full-release.yml index 1db07b94a..ea1202c83 100644 --- a/.github/workflows/full-release.yml +++ b/.github/workflows/full-release.yml @@ -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 @@ -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: @@ -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 diff --git a/.github/workflows/notify-failure.yml b/.github/workflows/notify-failure.yml index ef9384026..f77d72847 100644 --- a/.github/workflows/notify-failure.yml +++ b/.github/workflows/notify-failure.yml @@ -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: diff --git a/.github/workflows/prepare-release-notes.yml b/.github/workflows/prepare-release-notes.yml index bb2b22075..928f85b23 100644 --- a/.github/workflows/prepare-release-notes.yml +++ b/.github/workflows/prepare-release-notes.yml @@ -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 diff --git a/.github/workflows/releasability.yml b/.github/workflows/releasability.yml index 9ec628514..785a9f477 100644 --- a/.github/workflows/releasability.yml +++ b/.github/workflows/releasability.yml @@ -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 From af1dd6db69593f061e9d6ac407d0e408bb712d62 Mon Sep 17 00:00:00 2001 From: Damien Urruty Date: Tue, 15 Sep 2026 16:18:43 +0200 Subject: [PATCH 2/4] BUILD-12308 Fetch cross-compile Bun runtimes from Repox --- .github/workflows/build.yml | 2 + build-scripts/build-binary.ts | 52 +++++-- build-scripts/compile-target-runtime.ts | 139 +++++++++++++++++ .../compile-target-runtime.test.ts | 142 ++++++++++++++++++ 4 files changed, 323 insertions(+), 12 deletions(-) create mode 100644 build-scripts/compile-target-runtime.ts create mode 100644 tests/unit/build-scripts/compile-target-runtime.test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba197cfa9..4ab5ae8d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/build-scripts/build-binary.ts b/build-scripts/build-binary.ts index e009d6db3..1f23806a4 100644 --- a/build-scripts/build-binary.ts +++ b/build-scripts/build-binary.ts @@ -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'; @@ -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}`); diff --git a/build-scripts/compile-target-runtime.ts b/build-scripts/compile-target-runtime.ts new file mode 100644 index 000000000..b90516ad6 --- /dev/null +++ b/build-scripts/compile-target-runtime.ts @@ -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 = { + '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; +}): 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; + } +} diff --git a/tests/unit/build-scripts/compile-target-runtime.test.ts b/tests/unit/build-scripts/compile-target-runtime.test.ts new file mode 100644 index 000000000..bce2c1b4e --- /dev/null +++ b/tests/unit/build-scripts/compile-target-runtime.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, it } from 'bun:test'; + +import { + compileTargetMatchesHost, + compileTargetTarballUrl, + downloadCompileTargetExecutable, + npmPackageForCompileTarget, + REPOX_NPM_REGISTRY, +} from '../../../build-scripts/compile-target-runtime.ts'; + +describe('npmPackageForCompileTarget()', () => { + it('maps CI compile targets to the @oven npm package names bun embeds', () => { + expect(npmPackageForCompileTarget('bun-linux-x64')).toBe('bun-linux-x64'); + expect(npmPackageForCompileTarget('bun-linux-arm64')).toBe('bun-linux-aarch64'); + expect(npmPackageForCompileTarget('bun-darwin-arm64')).toBe('bun-darwin-aarch64'); + expect(npmPackageForCompileTarget('bun-windows-x64')).toBe('bun-windows-x64'); + }); + + it('returns undefined for an unknown target', () => { + expect(npmPackageForCompileTarget('bun-linux-ppc64')).toBeUndefined(); + }); +}); + +describe('compileTargetTarballUrl()', () => { + it('builds the npm tarball path under the Repox virtual registry', () => { + expect(compileTargetTarballUrl(REPOX_NPM_REGISTRY, 'bun-linux-aarch64', '1.4.0')).toBe( + 'https://repox.jfrog.io/artifactory/api/npm/npm/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.4.0.tgz', + ); + }); + + it('strips a trailing slash from the registry URL', () => { + expect(compileTargetTarballUrl(`${REPOX_NPM_REGISTRY}/`, 'bun-windows-x64', '1.4.0')).toBe( + 'https://repox.jfrog.io/artifactory/api/npm/npm/@oven/bun-windows-x64/-/bun-windows-x64-1.4.0.tgz', + ); + }); +}); + +describe('compileTargetMatchesHost()', () => { + it('matches the current OS and architecture', () => { + const os = + process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'windows' : 'linux'; + const arch = process.arch === 'arm64' ? 'arm64' : 'x64'; + + expect(compileTargetMatchesHost(`bun-${os}-${arch}`)).toBe(true); + expect(compileTargetMatchesHost('bun-plan9-x64')).toBe(false); + }); +}); + +describe('downloadCompileTargetExecutable()', () => { + const cleanups: Array<() => void> = []; + + afterEach(() => { + for (const cleanup of cleanups.splice(0)) { + cleanup(); + } + }); + + it('downloads the tarball with a bearer token and returns the extracted bun binary', async () => { + const staging = mkdtempSync(join(tmpdir(), 'compile-target-fixture-')); + cleanups.push(() => rmSync(staging, { recursive: true, force: true })); + mkdirSync(join(staging, 'package', 'bin'), { recursive: true }); + writeFileSync(join(staging, 'package', 'bin', 'bun'), 'fake-bun-runtime'); + const tarballPath = join(staging, 'pkg.tgz'); + const packed = Bun.spawnSync(['tar', '-czf', tarballPath, '-C', staging, 'package']); + expect(packed.exitCode).toBe(0); + + const server = Bun.serve({ + hostname: '127.0.0.1', + port: 0, + fetch(req) { + if (req.headers.get('authorization') !== 'Bearer test-token') { + return new Response('unauthorized', { status: 401 }); + } + if ( + new URL(req.url).pathname !== '/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.4.0.tgz' + ) { + return new Response('not found', { status: 404 }); + } + return new Response(Bun.file(tarballPath)); + }, + }); + cleanups.push(() => { + void server.stop(true); + }); + + const downloaded = await downloadCompileTargetExecutable({ + target: 'bun-linux-arm64', + bunVersion: '1.4.0', + registryUrl: `http://127.0.0.1:${server.port}`, + token: 'test-token', + }); + cleanups.push(downloaded.cleanup); + + expect(readFileSync(downloaded.executablePath, 'utf8')).toBe('fake-bun-runtime'); + }); + + it('fails without leaking the token when the registry rejects the download', async () => { + const fetchImpl = (): Promise => + Promise.resolve(new Response('nope', { status: 401 })); + + try { + await downloadCompileTargetExecutable({ + target: 'bun-linux-arm64', + bunVersion: '1.4.0', + registryUrl: REPOX_NPM_REGISTRY, + token: 'super-secret-token', + fetchImpl, + }); + throw new Error('expected the download to fail'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toBe( + 'Failed to download @oven/bun-linux-aarch64@1.4.0 from Repox: HTTP 401', + ); + expect((error as Error).message).not.toContain('super-secret-token'); + } + }); +}); From 2a8df749dbcf797b0b709a96b82edcc96d818df1 Mon Sep 17 00:00:00 2001 From: Damien Urruty Date: Tue, 15 Sep 2026 16:48:59 +0200 Subject: [PATCH 3/4] BUILD-12308 Run binary builds on sonar-m --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4ab5ae8d6..ff6ddfd5d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: build-binaries: name: Build All Binaries - runs-on: warp-custom-ubuntu-24-04 + runs-on: sonar-m needs: prepare steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 From fdc155a39ac7307b91f40d8ed0575710e5bb6374 Mon Sep 17 00:00:00 2001 From: Damien Urruty Date: Tue, 15 Sep 2026 20:43:42 +0200 Subject: [PATCH 4/4] BUILD-12308 Pin api.localhost on Linux e2e runners --- .../actions/run-integration-and-e2e/action.yml | 16 ++++++++++++---- tests/CLAUDE.md | 2 +- .../integration/harness/fake-sonarqube-server.ts | 5 +++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/actions/run-integration-and-e2e/action.yml b/.github/actions/run-integration-and-e2e/action.yml index 37b2a0bee..5a0ea32fc 100644 --- a/.github/actions/run-integration-and-e2e/action.yml +++ b/.github/actions/run-integration-and-e2e/action.yml @@ -86,10 +86,10 @@ 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 @@ -97,6 +97,14 @@ runs: 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 diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md index 80c3747d6..2b92d6da5 100644 --- a/tests/CLAUDE.md +++ b/tests/CLAUDE.md @@ -8,7 +8,7 @@ Follow the structure of existing tests for the command or feature area you are w - Unit tests: `tests/unit/` — inject `FakeConsole` (`tests/_common/fake-console.ts`) via `CommandInvocationContext` / `SonarCommand({ console })`. Use `tests/unit/core/host/keychain-test-handle.ts` for keychain. - Integration tests: `tests/integration/specs//` — run the compiled binary against fake servers. Use `TestHarness` from `tests/integration/harness/`. -- E2E tests: `tests/e2e/` — real external dependencies that cannot be faked: OS keychain, install scripts with real network, real SonarQube server calls, and integration with external tools. Those tests are black-box tests and exercise the product from the outside. `tests/e2e/context/` is the offline real-binary suite for `sonar-context-augmentation`: it seeds CAG state in `state.json`, lets `runPostUpdateActions()` re-download CAG from `binaries.sonarsource.com`, and covers post-update refresh (`cag-offline.test.ts`), edge cases like missing project roots / global skills / multi-skill refresh / stale-binary cleanup (`cag-edge-cases.test.ts`), passthrough behaviors including unauthenticated errors and exit-code propagation (`cag-passthrough.test.ts`), the `copilot-cli` skill path (`cag-copilot.test.ts`), the `codex` skill path (`cag-codex.test.ts`), and `sonar integrate` pre-flight skip paths against a fake server — SonarQube Server connections and disabled entitlement (`cag-integrate.test.ts`; the happy path is deferred because CAG's daemon socket path exceeds the AF_UNIX limit under the harness `tmpdir` layout). Shared scaffolding lives in `_helpers.ts`. No SonarQube/Cloud access required — only the binaries CDN. +- E2E tests: `tests/e2e/` — real external dependencies that cannot be faked: OS keychain, install scripts with real network, real SonarQube server calls, and integration with external tools. Those tests are black-box tests and exercise the product from the outside. `tests/e2e/context/` is the offline real-binary suite for `sonar-context-augmentation`: it seeds CAG state in `state.json`, lets `runPostUpdateActions()` re-download CAG from `binaries.sonarsource.com`, and covers post-update refresh (`cag-offline.test.ts`), edge cases like missing project roots / global skills / multi-skill refresh / stale-binary cleanup (`cag-edge-cases.test.ts`), passthrough behaviors including unauthenticated errors and exit-code propagation (`cag-passthrough.test.ts`), the `copilot-cli` skill path (`cag-copilot.test.ts`), the `codex` skill path (`cag-codex.test.ts`), and `sonar integrate` pre-flight skip paths against a fake server — SonarQube Server connections and disabled entitlement (`cag-integrate.test.ts`; the happy path is deferred because CAG's daemon socket path exceeds the AF_UNIX limit under the harness `tmpdir` layout). Shared scaffolding lives in `_helpers.ts`. No SonarQube/Cloud access required — only the binaries CDN. Cloud-mode CAG talks to the fake server as `api.localhost` (CAG-933); CI pins that name to loopback in `.github/actions/run-integration-and-e2e` because Windows and some self-hosted Linux images do not resolve `*.localhost`. Before writing a test, find an existing spec for the same command area and follow its structure. diff --git a/tests/integration/harness/fake-sonarqube-server.ts b/tests/integration/harness/fake-sonarqube-server.ts index 0d6acc1e5..2f33ec7d2 100644 --- a/tests/integration/harness/fake-sonarqube-server.ts +++ b/tests/integration/harness/fake-sonarqube-server.ts @@ -436,8 +436,9 @@ export class FakeSonarQubeServer { baseUrl(): string { // Use `localhost` (not `127.0.0.1`) so CAG's Cloud-API URL transformation - // (host → `api.`) lands on `api.localhost`, which resolves to a - // loopback address per RFC 6761 and reaches the same server. + // (host → `api.`) lands on `api.localhost`. RFC 6761 says that + // should loop back, but Windows and some Linux CI images do not resolve + // it — `.github/actions/run-integration-and-e2e` pins it in `/etc/hosts`. // `api.127.0.0.1` would not resolve, breaking any test that exercises // Cloud-mode CAG. return `http://localhost:${this.server.port}`;