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
121 changes: 121 additions & 0 deletions .github/workflows/bazel-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: Bazel on Windows

on:
Comment thread
mtorp marked this conversation as resolved.
Resolved
pull_request:
branches: [main, v1.x]
paths:
- .github/workflows/bazel-windows.yml
- package.json
- pnpm-lock.yaml
- src/commands/manifest/bazel/**
- test/fixtures/manifest-bazel/**
push:
branches: [main, v1.x]
paths:
- .github/workflows/bazel-windows.yml
- package.json
- pnpm-lock.yaml
- src/commands/manifest/bazel/**
- test/fixtures/manifest-bazel/**

permissions:
contents: read

concurrency:
group: bazel-windows-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
maven:
name: Maven on Windows x64
runs-on: windows-2022
timeout-minutes: 45
defaults:
run:
shell: pwsh
steps:
- name: Checkout source
env:
CHECKOUT_REF: ${{ github.sha }}
GITHUB_TOKEN: ${{ github.token }}
SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
run: |
$PSNativeCommandUseErrorActionPreference = $true
git init -q
git config --local advice.detachedHead false
git remote add origin "$env:SERVER_URL/$env:REPOSITORY"
$auth = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("x-access-token:$env:GITHUB_TOKEN"))
$env:GIT_CONFIG_COUNT = '1'
$env:GIT_CONFIG_KEY_0 = "http.$env:SERVER_URL/.extraheader"
$env:GIT_CONFIG_VALUE_0 = "AUTHORIZATION: basic $auth"
git fetch --no-tags --depth=1 origin $env:CHECKOUT_REF
git checkout -q --detach FETCH_HEAD

- name: Install pinned toolchains
run: |
$tools = Join-Path $env:RUNNER_TEMP 'bazel-tools'
New-Item -ItemType Directory -Force "$tools/bin" | Out-Null
function Get-VerifiedTool($url, $file, $sha256) {
Invoke-WebRequest -Uri $url -OutFile $file
if ((Get-FileHash $file -Algorithm SHA256).Hash.ToLowerInvariant() -ne $sha256) {
throw "Checksum mismatch for $file"
}
}
Get-VerifiedTool 'https://nodejs.org/dist/v24.14.1/node-v24.14.1-win-x64.zip' "$tools/node.zip" '6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70'
Get-VerifiedTool 'https://api.nuget.org/v3-flatcontainer/python/3.12.10/python.3.12.10.nupkg' "$tools/python.zip" '0eb85c2dfccccf1b17352de4c397f69194035b7d37149eacc16f1147d93de3b8'
Get-VerifiedTool 'https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.6%2B7/OpenJDK21U-jdk_x64_windows_hotspot_21.0.6_7.zip' "$tools/java.zip" '897c8eebb0f85a99ccecbd482ebae9a45d88c19d6077054f6529ebab49b6d259'
Get-VerifiedTool 'https://github.com/bazelbuild/bazelisk/releases/download/v1.26.0/bazelisk-windows-amd64.exe' "$tools/bin/bazelisk.exe" '023734f33ed6b9c6d65468fe20bb2c5fb32473ccb8aca2fc5bf1521e61ce1622'
Get-VerifiedTool 'https://github.com/pnpm/pnpm/releases/download/v10.33.0/pnpm-win-x64.exe' "$tools/bin/pnpm.exe" 'afc96009dc39fe23a835d65192049e6a995f342496b175585dc2beda7d42d33f'
Expand-Archive "$tools/node.zip" "$tools/node"
Expand-Archive "$tools/python.zip" "$tools/python"
Expand-Archive "$tools/java.zip" "$tools/java"
"$tools/node/node-v24.14.1-win-x64" | Add-Content $env:GITHUB_PATH
"$tools/python/tools" | Add-Content $env:GITHUB_PATH
"$tools/java/jdk-21.0.6+7/bin" | Add-Content $env:GITHUB_PATH
"$tools/bin" | Add-Content $env:GITHUB_PATH
"JAVA_HOME=$tools/java/jdk-21.0.6+7" | Add-Content $env:GITHUB_ENV
'USE_BAZEL_VERSION=7.4.1' | Add-Content $env:GITHUB_ENV
'SOCKET_BAZEL_OUTPUT_ROOT=C:/sb' | Add-Content $env:GITHUB_ENV
New-Item -ItemType Directory -Force 'C:/sb' | Out-Null

- name: Record versions and verify prerequisites
run: |
$PSNativeCommandUseErrorActionPreference = $true
git rev-parse HEAD
node --version
pnpm --version
bazelisk --version
python --version
java -version
Write-Output "PowerShell $($PSVersionTable.PSVersion)"
Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, OSArchitecture
Get-Command node, bazelisk, python, java | Select-Object Name, Source

- name: Install dependencies and build
run: |
$PSNativeCommandUseErrorActionPreference = $true
pnpm install --frozen-lockfile
pnpm build

- name: Test executable handling and prerequisites
run: |
$PSNativeCommandUseErrorActionPreference = $true
pnpm test:unit src/commands/manifest/bazel/bazel-bin-detect.test.mts src/commands/manifest/bazel/bazel-process.test.mts src/commands/manifest/bazel/bazel-java-shim.test.mts src/commands/manifest/bazel/bazel-python-shim.test.mts src/commands/manifest/bazel/bazel-query-runner.test.mts src/commands/manifest/bazel/bazel-cquery.test.mts

- name: Install the packaged CLI
run: |
$PSNativeCommandUseErrorActionPreference = $true
$packages = Join-Path $env:RUNNER_TEMP 'bazel-packages'
$installation = Join-Path $env:RUNNER_TEMP 'bazel-installation'
pnpm pack --pack-destination $packages
$tarball = Get-ChildItem "$packages/*.tgz" | Select-Object -First 1
npm install --ignore-scripts --no-audit --no-fund --prefix $installation $tarball.FullName
"SOCKET_BAZEL_TEST_CLI=$installation/node_modules/socket/bin/cli.js" | Add-Content $env:GITHUB_ENV

- name: Verify Maven manifests and cleanup
env:
SOCKET_BAZEL_INTEGRATION: '1'
run: |
$PSNativeCommandUseErrorActionPreference = $true
pnpm exec vitest run --config vitest.e2e.config.mts src/commands/manifest/bazel/bazel-native.e2e.test.mts
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- Set `SOCKET_BAZEL_OUTPUT_ROOT` to choose a short parent directory for temporary Bazel output, including on Windows.

### Changed
- `--dynamic-sbom-inference` now builds every Maven POM a scan uploads, not only `pom.xml`: `<artifactId>-<version>.pom` and `.pom.xml` files, a bare `pom`, and names in any case, each as a build of its own. Like the upload, it skips POMs matched by `.gitignore` or by `projectIgnorePaths` in `socket.yml`.

### Fixed
- Bazel Maven extraction handles native Windows executables, batch launchers, and JavaScript entry points while preserving arguments and the selected executable path.
- Bazel prerequisite checks use the extraction environment and verify Python 3. Windows requires a working `python.exe` without creating a Python symlink.
- Quoted Bazel flags preserve spaces and Windows backslashes. Quotes open at the start of a value or after `=`, so apostrophes inside a value stay literal. Unclosed quotes produce an error.
- Bazel cleanup uses the extraction workspace, environment, and startup options, retries temporary file locks, and only stops Bazel servers the scan started.
- Bazel timeouts and cancellation stop every process a Bazel launcher started. Cleanup still runs after Ctrl-C and keeps temporary output until the scan's own Bazel server has exited.
- A relative `--bazel` path resolves against the current directory. A relative `bazel` path in `socket.json` resolves against the scanned directory.

## [1.11.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.11.0) - 2026-10-26

### Added
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
"typescript-eslint": "8.43.0",
"unplugin-purge-polyfills": "0.1.0",
"vitest": "3.2.4",
"which": "=5.0.0",
"yaml": "2.8.1",
"yargs-parser": "21.1.1",
"yoctocolors-cjs": "2.1.3",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 13 additions & 7 deletions src/commands/manifest/bazel/bazel-bin-detect.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { existsSync } from 'node:fs'
import path from 'node:path'

import { whichBin } from '@socketsecurity/registry/lib/bin'

import {
findBazelExecutable,
resolveBazelExecutable,
} from './bazel-process.mts'
import { InputError } from '../../../utils/errors.mts'

/**
Expand All @@ -15,21 +18,24 @@ import { InputError } from '../../../utils/errors.mts'
*/
export async function resolveBazelBinary(
explicit: string | undefined,
env: NodeJS.ProcessEnv,
cwd: string,
): Promise<string> {
if (explicit) {
if (!existsSync(explicit)) {
const resolved = path.resolve(cwd, explicit)
if (!existsSync(resolved)) {
Comment thread
mtorp marked this conversation as resolved.
Resolved
throw new InputError(
`--bazel path does not exist: ${explicit}. Install bazelisk or bazel, or pass an existing path via --bazel.`,
`--bazel path does not exist: ${resolved}. Install bazelisk or bazel, or pass an existing path via --bazel.`,
)
}
return explicit
return resolveBazelExecutable(resolved)
}
// Prefer bazelisk: respects .bazelversion in the workspace.
const bazelisk = await whichBin('bazelisk', { nothrow: true })
const bazelisk = await findBazelExecutable('bazelisk', env, cwd)
if (bazelisk) {
return bazelisk
}
const bazel = await whichBin('bazel', { nothrow: true })
const bazel = await findBazelExecutable('bazel', env, cwd)
if (bazel) {
return bazel
}
Expand Down
104 changes: 73 additions & 31 deletions src/commands/manifest/bazel/bazel-bin-detect.test.mts
Original file line number Diff line number Diff line change
@@ -1,53 +1,95 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
copyFileSync,
mkdtempSync,
realpathSync,
rmSync,
statSync,
} from 'node:fs'
import os from 'node:os'
import path from 'node:path'

// Mock whichBin so tests run with no bazel on PATH.
vi.mock('@socketsecurity/registry/lib/bin', () => ({
whichBin: vi.fn(),
}))

import { whichBin } from '@socketsecurity/registry/lib/bin'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'

import { resolveBazelBinary } from './bazel-bin-detect.mts'
import { getBazelEnvironment } from './bazel-process.mts'

describe('resolveBazelBinary', () => {
const mocked = vi.mocked(whichBin)
let cwd: string

beforeEach(() => {
mocked.mockReset()
cwd = realpathSync(mkdtempSync(path.join(os.tmpdir(), 'bazel binaries ')))
})

it('returns explicit path when it exists', async () => {
// Use a path that definitely exists on every dev machine.
const existing = process.execPath
await expect(resolveBazelBinary(existing)).resolves.toBe(existing)
afterEach(() => {
rmSync(cwd, { recursive: true, force: true, maxRetries: 5 })
})

it('throws InputError when explicit path does not exist', async () => {
await expect(
resolveBazelBinary('/no/such/bazel/binary/xyz'),
).rejects.toThrow(/--bazel path does not exist/)
function install(name: string): string {
const bin = path.join(
cwd,
`${name}${process.platform === 'win32' ? '.exe' : ''}`,
)
copyFileSync(process.execPath, bin)
return bin
}

it('resolves an explicit path relative to the scan directory', async () => {
const bin = install('bazel')
const resolved = await resolveBazelBinary(
path.basename(bin),
getBazelEnvironment(),
cwd,
)
expect(statSync(resolved).ino).toBe(statSync(bin).ino)
})

it('preserves an explicit executable outside PATH', async () => {
const bin = install('selected')
const resolved = await resolveBazelBinary(
bin,
getBazelEnvironment({ PATH: '' }),
cwd,
)
expect(statSync(resolved).ino).toBe(statSync(bin).ino)
})

it('returns bazelisk when on PATH', async () => {
mocked.mockResolvedValueOnce('/usr/local/bin/bazelisk')
await expect(resolveBazelBinary(undefined)).resolves.toBe(
'/usr/local/bin/bazelisk',
it('prefers Bazelisk over Bazel', async () => {
const bazelisk = install('bazelisk')
install('bazel')
const resolved = await resolveBazelBinary(
undefined,
getBazelEnvironment({ PATH: cwd }),
cwd,
)
expect(statSync(resolved).ino).toBe(statSync(bazelisk).ino)
})

it('falls back to bazel when bazelisk is missing', async () => {
mocked
.mockResolvedValueOnce(null)
.mockResolvedValueOnce('/usr/local/bin/bazel')
await expect(resolveBazelBinary(undefined)).resolves.toBe(
'/usr/local/bin/bazel',
it('falls back to Bazel when Bazelisk is absent', async () => {
const bazel = install('bazel')
const resolved = await resolveBazelBinary(
undefined,
getBazelEnvironment({ PATH: cwd }),
cwd,
)
expect(statSync(resolved).ino).toBe(statSync(bazel).ino)
})

it('throws InputError when neither is on PATH', async () => {
mocked.mockResolvedValue(null)
await expect(resolveBazelBinary(undefined)).rejects.toThrow(
/Could not find bazelisk or bazel/,
it('finds tools in a quoted PATH entry', async () => {
const bazel = install('bazel')
const resolved = await resolveBazelBinary(
undefined,
getBazelEnvironment({ PATH: `"${cwd}"` }),
os.tmpdir(),
)
expect(statSync(resolved).ino).toBe(statSync(bazel).ino)
})

it('reports missing tools', async () => {
await expect(
resolveBazelBinary(undefined, getBazelEnvironment({ PATH: cwd }), cwd),
).rejects.toThrow(/Could not find bazelisk or bazel/)
await expect(
resolveBazelBinary(path.join(cwd, 'missing'), getBazelEnvironment(), cwd),
).rejects.toThrow(/--bazel path does not exist/)
})
})
Loading
Loading