Skip to content

Commit 5d4e857

Browse files
committed
feat(manifest): generate JVM Socket facts natively + single-run reachability sidecar (REA-613)
Move the JVM build-tool resolution scripts (Gradle init script, sbt plugin, Maven core extension) and the TypeScript SBOM assembler into socket-cli, so `socket manifest` and `--auto-manifest` generate `.socket.facts.json` natively instead of delegating to the Coana CLI's `manifest` command. On `socket scan create --reach --auto-manifest` the build tool now runs exactly once and emits both the SBOM and a resolved-paths sidecar, passed to `coana run --compute-artifacts-sidecar`. Coana no longer re-resolves the build at reach time, eliminating the divergence that broke dynamically-versioned projects (git-derived versions, CI build numbers, timestamps). Build-tool binary selection prefers the project's wrapper (./gradlew, ./mvnw) and falls back to gradle/mvn on PATH. Release-gated on the Coana CLI version that ships --compute-artifacts-sidecar; the CHANGELOG carries a placeholder for that bump.
1 parent 94484ea commit 5d4e857

67 files changed

Lines changed: 5165 additions & 411 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.config/rollup.dist.config.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,38 @@ async function copyInitGradle() {
7979
await fs.copyFile(filepath, destPath)
8080
}
8181

82+
// Copy the JVM build-tool resolution assets (Gradle init script, sbt plugin,
83+
// Maven extension jar) into dist/manifest-scripts, where run.mts resolves them
84+
// at runtime. The Maven jar is compiled by maven-extension/build-jar.sh (run in
85+
// CI / local dev) and is absent from a fresh checkout — copy it only if present;
86+
// run.mts surfaces a build hint when it's missing.
87+
async function copyManifestScripts() {
88+
const srcDir = path.join(constants.srcPath, 'commands/manifest/scripts')
89+
const destDir = path.join(constants.distPath, 'manifest-scripts')
90+
await fs.mkdir(path.join(destDir, 'maven-extension'), { recursive: true })
91+
await Promise.all([
92+
fs.copyFile(
93+
path.join(srcDir, 'socket-facts.init.gradle'),
94+
path.join(destDir, 'socket-facts.init.gradle'),
95+
),
96+
fs.copyFile(
97+
path.join(srcDir, 'socket-facts.plugin.scala'),
98+
path.join(destDir, 'socket-facts.plugin.scala'),
99+
),
100+
])
101+
const jarPath = path.join(
102+
srcDir,
103+
'maven-extension',
104+
'coana-maven-extension.jar',
105+
)
106+
if (existsSync(jarPath)) {
107+
await fs.copyFile(
108+
jarPath,
109+
path.join(destDir, 'maven-extension', 'coana-maven-extension.jar'),
110+
)
111+
}
112+
}
113+
82114
async function copyBashCompletion() {
83115
const filepath = path.join(
84116
constants.srcPath,
@@ -464,6 +496,7 @@ export default async () => {
464496
async writeBundle() {
465497
await Promise.all([
466498
copyInitGradle(),
499+
copyManifestScripts(),
467500
copyBashCompletion(),
468501
updatePackageJson(),
469502
// Remove dist/vendor.js.map file.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
name: Manifest compat
2+
3+
# Reusable: build-tool version compatibility for ONE ecosystem's manifest script.
4+
# Called by manifest-gradle.yml / manifest-sbt.yml / manifest-maven.yml when their
5+
# label is added to a PR, so each ecosystem is its own workflow (and its own PR
6+
# check group). Runs the real script across a version matrix to assert it emits
7+
# the expected RECORDS (the TS assembler is covered by unit tests).
8+
9+
on:
10+
workflow_call:
11+
inputs:
12+
tool:
13+
description: gradle | maven | sbt
14+
required: true
15+
type: string
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
# The label is a momentary "run" button: remove it as soon as the run starts so
22+
# re-adding it re-triggers a fresh run (and the PR doesn't stay labeled).
23+
unlabel:
24+
runs-on: ubuntu-latest
25+
permissions:
26+
pull-requests: write
27+
steps:
28+
- env:
29+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
REPO: ${{ github.repository }}
31+
PR: ${{ github.event.pull_request.number }}
32+
LABEL: ${{ github.event.label.name }}
33+
run: gh api --method DELETE "repos/$REPO/issues/$PR/labels/$LABEL" || true
34+
35+
setup:
36+
runs-on: ubuntu-latest
37+
outputs:
38+
matrix: ${{ steps.m.outputs.matrix }}
39+
steps:
40+
- id: m
41+
env:
42+
TOOL: ${{ inputs.tool }}
43+
run: |
44+
if [ "$TOOL" = gradle ]; then
45+
# 1.12: all three reflective fallbacks; 2.14.1: findProperty/firstLevel; 3.3: native
46+
# canBeResolved; 8.10.2: modern; 9.2.1: latest.
47+
matrix='{"include":[{"tool":"gradle","ver":"1.12","java":"8"},{"tool":"gradle","ver":"2.14.1","java":"8"},{"tool":"gradle","ver":"3.3","java":"8"},{"tool":"gradle","ver":"8.10.2","java":"17"},{"tool":"gradle","ver":"9.2.1","java":"21"}]}'
48+
elif [ "$TOOL" = maven ]; then
49+
# Runtime compat of the core extension across the Maven 3.6/3.8/3.9 line.
50+
matrix='{"include":[{"tool":"maven","ver":"3.6.3","java":"11"},{"tool":"maven","ver":"3.8.8","java":"17"},{"tool":"maven","ver":"3.9.9","java":"17"}]}'
51+
else
52+
# 0.13.18: Scala 2.10 / reflective 0.13 shims; then 1.x.
53+
matrix='{"include":[{"tool":"sbt","ver":"0.13.18","scala":"2.10.7","java":"8"},{"tool":"sbt","ver":"1.4.9","scala":"2.12.20","java":"11"},{"tool":"sbt","ver":"1.6.2","scala":"2.12.20","java":"17"},{"tool":"sbt","ver":"1.9.9","scala":"2.12.20","java":"17"}]}'
54+
fi
55+
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
56+
57+
compat:
58+
needs: setup
59+
name: ${{ matrix.tool }} ${{ matrix.ver }}
60+
runs-on: ubuntu-latest
61+
strategy:
62+
fail-fast: false
63+
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
64+
steps:
65+
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
66+
with:
67+
persist-credentials: false
68+
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
69+
with:
70+
distribution: 'temurin'
71+
java-version: ${{ matrix.java }}
72+
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
73+
with:
74+
python-version: '3.12'
75+
# matrix.* via env (not inlined): the matrix is generated dynamically, so its
76+
# values are treated as untrusted in run shells.
77+
- name: Download Gradle ${{ matrix.ver }}
78+
if: matrix.tool == 'gradle'
79+
env:
80+
VER: ${{ matrix.ver }}
81+
run: |
82+
curl -fsSL "https://services.gradle.org/distributions/gradle-$VER-bin.zip" -o gradle.zip
83+
unzip -q gradle.zip
84+
- name: Gradle smoke test (offline, local repo)
85+
if: matrix.tool == 'gradle'
86+
env:
87+
VER: ${{ matrix.ver }}
88+
run: bash src/commands/manifest/scripts/test/gradle-compat/smoke-test.sh "$PWD/gradle-$VER/bin/gradle"
89+
- uses: sbt/setup-sbt@3e125ece5c3e5248e18da9ed8d2cce3d335ec8dd # v1.1.14
90+
if: matrix.tool == 'sbt'
91+
with:
92+
sbt-runner-version: 1.9.9
93+
- name: SBT smoke test (sbt ${{ matrix.ver }})
94+
if: matrix.tool == 'sbt'
95+
env:
96+
VER: ${{ matrix.ver }}
97+
SCALA: ${{ matrix.scala }}
98+
run: bash src/commands/manifest/scripts/test/sbt-compat/smoke-test.sh "$VER" "$SCALA"
99+
# Build the extension jar from source with the runner's default Maven, then run the smoke project
100+
# on the matrix Maven version (runtime compat is what varies, not the build).
101+
- name: Build Maven extension jar
102+
if: matrix.tool == 'maven'
103+
run: bash src/commands/manifest/scripts/maven-extension/build-jar.sh
104+
- name: Download Maven ${{ matrix.ver }}
105+
if: matrix.tool == 'maven'
106+
env:
107+
VER: ${{ matrix.ver }}
108+
run: |
109+
curl -fsSL "https://archive.apache.org/dist/maven/maven-3/$VER/binaries/apache-maven-$VER-bin.zip" -o maven.zip
110+
unzip -q maven.zip
111+
- name: Maven smoke test (maven ${{ matrix.ver }})
112+
if: matrix.tool == 'maven'
113+
env:
114+
VER: ${{ matrix.ver }}
115+
run: |
116+
bash src/commands/manifest/scripts/test/maven-compat/smoke-test.sh \
117+
"$PWD/apache-maven-$VER/bin/mvn" \
118+
"$PWD/src/commands/manifest/scripts/maven-extension/coana-maven-extension.jar"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Manifest gradle compat
2+
3+
# On-demand: add the `manifest-gradle` label to a PR to run the Gradle init
4+
# script's compat matrix across Gradle versions (re-add to re-run). Its own
5+
# workflow so it shows as its own PR check group.
6+
7+
on:
8+
pull_request:
9+
types: [labeled]
10+
11+
permissions:
12+
contents: read
13+
14+
concurrency:
15+
group: ${{ github.ref }}-manifest-gradle
16+
cancel-in-progress: true
17+
18+
jobs:
19+
gradle:
20+
if: github.event.label.name == 'manifest-gradle'
21+
permissions:
22+
contents: read
23+
pull-requests: write
24+
uses: ./.github/workflows/manifest-compat.yml
25+
with:
26+
tool: gradle
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Manifest maven compat
2+
3+
# On-demand: add the `manifest-maven` label to a PR to compile the Maven core
4+
# extension and run its compat matrix across Maven versions (re-add to re-run).
5+
# Its own workflow so it shows as its own PR check group.
6+
7+
on:
8+
pull_request:
9+
types: [labeled]
10+
11+
permissions:
12+
contents: read
13+
14+
concurrency:
15+
group: ${{ github.ref }}-manifest-maven
16+
cancel-in-progress: true
17+
18+
jobs:
19+
maven:
20+
if: github.event.label.name == 'manifest-maven'
21+
permissions:
22+
contents: read
23+
pull-requests: write
24+
uses: ./.github/workflows/manifest-compat.yml
25+
with:
26+
tool: maven

.github/workflows/manifest-sbt.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Manifest sbt compat
2+
3+
# On-demand: add the `manifest-sbt` label to a PR to run the sbt plugin's compat
4+
# matrix across sbt versions (re-add to re-run). Its own workflow so it shows as
5+
# its own PR check group.
6+
7+
on:
8+
pull_request:
9+
types: [labeled]
10+
11+
permissions:
12+
contents: read
13+
14+
concurrency:
15+
group: ${{ github.ref }}-manifest-sbt
16+
cancel-in-progress: true
17+
18+
jobs:
19+
sbt:
20+
if: github.event.label.name == 'manifest-sbt'
21+
permissions:
22+
contents: read
23+
pull-requests: write
24+
uses: ./.github/workflows/manifest-compat.yml
25+
with:
26+
tool: sbt

.github/workflows/provenance.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,16 @@ jobs:
204204
- name: Install dependencies
205205
run: pnpm install --loglevel error
206206

207+
# Compile the Maven manifest extension jar so the dist build bundles it
208+
# into dist/manifest-scripts (the jar is never committed; it ships only in
209+
# the published package). Needs a JDK; build-jar.sh uses the Maven wrapper.
210+
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
211+
with:
212+
distribution: 'temurin'
213+
java-version: '17'
214+
- name: Build Maven manifest extension jar
215+
run: pnpm run build:maven-extension
216+
207217
- run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 pnpm run build:dist
208218
- name: Publish socket
209219
id: publish_socket

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [1.1.132](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.132) - 2026-06-30
8+
9+
### Changed
10+
- More reliable reachability for Gradle, sbt, and Maven projects with dynamic versions (git versions, CI build numbers, timestamps): the build is resolved once and its artifact paths reused, avoiding spurious "failed to install" errors.
11+
- `socket manifest` and `--auto-manifest` now prefer your project's build-tool wrapper (`./gradlew`, `./mvnw`) when present, falling back to `gradle`/`mvn` on PATH.
12+
<!-- TODO(release): set to the Coana version that ships `coana run --compute-artifacts-sidecar`, then remove this comment. -->
13+
- Updated the Coana CLI to v `TODO`.
14+
715
## [1.1.131](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.131) - 2026-06-29
816

917
### Changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "socket",
3-
"version": "1.1.131",
3+
"version": "1.1.132",
44
"description": "CLI for Socket.dev",
55
"homepage": "https://github.com/SocketDev/socket-cli",
66
"license": "MIT",
@@ -36,6 +36,7 @@
3636
"build:dist": "pnpm build:dist:src && pnpm build:dist:types",
3737
"build:dist:src": "run-p -c clean:dist clean:external && dotenvx -q run -f .env.local -- rollup -c .config/rollup.dist.config.mjs",
3838
"build:dist:types": "pnpm clean:dist:types && tsgo --project tsconfig.dts.json",
39+
"build:maven-extension": "bash src/commands/manifest/scripts/maven-extension/build-jar.sh",
3940
"build:sea": "node src/sea/build-sea.mts",
4041
"build:sea:internal:bootstrap": "rollup -c .config/rollup.sea.config.mjs",
4142
"check": "pnpm check:lint && pnpm check:tsc",

src/commands/manifest/cmd-manifest-gradle.mts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { logger } from '@socketsecurity/registry/lib/logger'
66
import { convertGradleToFacts } from './convert-gradle-to-facts.mts'
77
import { convertGradleToMaven } from './convert_gradle_to_maven.mts'
88
import { parseBuildToolOpts } from './parse-build-tool-opts.mts'
9+
import { resolveBuildToolBin } from './scripts/build-tool.mts'
910
import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts'
1011
import { commonFlags } from '../../flags.mts'
1112
import { checkCommandInput } from '../../utils/check-input.mts'
@@ -28,7 +29,8 @@ const config: CliCommandConfig = {
2829
...commonFlags,
2930
bin: {
3031
type: 'string',
31-
description: 'Location of gradlew binary to use, default: CWD/gradlew',
32+
description:
33+
'Location of the gradle binary to use, default: ./gradlew if present, else gradle on PATH',
3234
},
3335
facts: {
3436
type: 'boolean',
@@ -156,7 +158,8 @@ async function run(
156158
bin = sockJson.defaults?.manifest?.gradle?.bin
157159
logger.info(`Using default --bin from ${SOCKET_JSON}:`, bin)
158160
} else {
159-
bin = path.join(cwd, 'gradlew')
161+
// Prefer the project's ./gradlew wrapper, else `gradle` on PATH.
162+
bin = resolveBuildToolBin('gradle', cwd)
160163
}
161164
}
162165
if (!gradleOpts) {

src/commands/manifest/cmd-manifest-gradle.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('socket manifest gradle', async () => {
2323
$ socket manifest gradle [options] [CWD=.]
2424
2525
Options
26-
--bin Location of gradlew binary to use, default: CWD/gradlew
26+
--bin Location of the gradle binary to use, default: ./gradlew if present, else gradle on PATH
2727
--exclude-configs When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applied after --include-configs)
2828
--facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph. This is the default; pass \`--pom\` to generate \`pom.xml\` files instead
2929
--gradle-opts Additional options to pass on to ./gradlew, see \`./gradlew --help\`

0 commit comments

Comments
 (0)