Skip to content

Let a publication describe what its SBOM lists - #769

Merged
alexander-yevsyukov merged 3 commits into
masterfrom
improve-sbom-generation
Sep 25, 2026
Merged

alexander-yevsyukov merged 3 commits into
masterfrom
improve-sbom-generation

Conversation

@alexander-yevsyukov

Copy link
Copy Markdown
Contributor

Closes #768.

Problem

PublicationSbom (#764) writes the SBOM of every JVM publication from runtimeClasspath. That is right for a publication made from a software component, whose POM Gradle derives from the same declarations. It is wrong for a publication made with artifact(...), which is how custom publications and uber-jar-module publish:

  • a POM written by hand declares dependencies that no Gradle-based tool sees, so the SBOM missed them. For example, core-jvm-gradle-plugin lacked core-jvm-plugins, its main runtime dependency.
  • modules and libraries packed into the JAR were listed as DEPENDS_ON dependencies. uber-jar-module has an empty POM, yet its SBOM listed every bundled library.

A module could not correct this itself.

What changes

A publication can now describe what its SBOM lists:

create<MavenPublication>("fatJar") {
    artifact(tasks.shadowJar)
    sbom {
        dependencies(fatJarDependencies) // the configuration the POM declares; default: `runtimeClasspath`
        bundled(tasks.shadowJar)         // or `bundled(configuration)`, for other packing
    }
}
  • The artifact DEPENDS_ON the graph of dependencies.
  • A bundled component is CONTAINS from the artifact, and never DEPENDS_ON from it.
  • A component resolved only to find the bundled ones, and not bundled itself, is left out, with its relationships and license texts.
  • The plugin walks the configurations of the SBOM one after another, through the packages it already knows. So its walk of the bundled content can hang a component under a dependency whose POM excludes it. Such edges are dropped.
  • uber-jar-module declares bundled(tasks.shadowJar) for its fatJar.
  • Publications without sbom { } get the same SBOM as before, with no new task input. This was verified byte for byte against Publish an SPDX SBOM with each Maven publication #764's SBOMs of core-jvm-compiler.

The pieces:

  • SbomContent.kt: the public DSL.
  • ArtifactComponents.kt: the rewrite of the SPDX document.
  • ComponentKey.kt: the key of a component, alike from Gradle's graph and from an SPDX package.
  • SpdxField.kt: the SPDX field names.
  • PublicationSbom.kt: choosing the SPDX target, and building lazy providers. A publication gets an SPDX target of its own only when its configurations differ from those of its module. Nothing is resolved in the root projectsEvaluated hook: each module's own SBOM task resolves its configurations.

Verification

  • ./gradlew :buildSrc:build detekt passes with 144 tests. PublicationSbomIgTest gains three fixture modules:

    • thin: a thin JAR with a hand-written POM that packs a sibling.
    • fat: a Shadow fat JAR with a dependency filter and a hand-written POM, including the edge-leak case.
    • uber: the uber-jar-module pattern.

    ArtifactComponentsSpec covers the rewrite on hand-written SPDX documents.

  • core-jvm-compiler, smoke-tested in a scratch clone of publish-sboms, with step 3 of the issue simulated by POM-mirror configurations and sbom { }:

    • core-jvm-gradle-plugin DEPENDS_ON exactly core-jvm-plugins and kotlinpoet-ksp, and CONTAINS grpc, ksp and routing.
    • core-jvm-plugins lists 24 of its 25 POM dependencies as DEPENDS_ON, and 26 bundled components as CONTAINS.
  • tool-base, smoke-tested in a scratch clone: intellij-platform and intellij-platform-java CONTAIN exactly the JetBrains artifacts their Shadow include filter admits. Two other modules could not be built: syncing the current buildSrc brings a kotlinx-coroutines-bom 1.11.0 vs 1.10.2 conflict, unrelated to this change.

Notes for reviewers

  • The bundled content comes from what Shadow is given, not from the JAR's entries. ShadowJar.includedDependencies, the files its dependency filter leaves in, is Shadow's only public view of what it packs. So a module whose classes are stripped by path still counts as bundled. A module that the POM declares must be excluded with the dependency filter; the KDoc of bundled(TaskProvider<ShadowJar>) says so.
  • core-jvm-compiler follow-up (step 3 of the issue):
    • Its fat JAR is given the files of five Gradle plugins its POM declares: compiler-gradle-plugin, compiler-gradle-api, time-gradle-plugin, validation-gradle-plugin and protobuf-gradle-plugin. They belong in pomProvidedModules. With that, none of its POM dependencies is reported as bundled, and verifyBundledPackages still passes.
    • The KSP Gradle plugin marker in its POM is a POM-only artifact, so the SPDX plugin lists no package for it. It lists the marker's content, symbol-processing-gradle-plugin, instead.
  • The SPDX plugin records only the first path to each package. So some dependencies a POM declares hang under the package through which they were first found, not directly under the artifact. Every SBOM the plugin writes has that shape; this PR does not change it.
  • The .agents/tasks/ plan is in the branch, to be deleted once this merges. The second commit adds a team memory about choosing file boundaries by design rather than by detekt's size thresholds.

🤖 Generated with Claude Code

alexander-yevsyukov and others added 2 commits September 25, 2026 18:28
A publication made with `artifact(...)` publishes what its module makes
of it: a POM written by hand, other modules and libraries packed into
a JAR. Its SBOM was written from `runtimeClasspath` all the same, so it
missed dependencies the POM declares, and listed bundled content as
dependencies.

`MavenPublication.sbom { }` now names the configuration holding the
dependencies of the artifact, and what the artifact bundles: each
component of a configuration, or what a Shadow task packs. The SBOM
relates the artifact to its dependencies by `DEPENDS_ON`, to its bundled
content by `CONTAINS`, and leaves out what it neither depends on nor
bundles. `uber-jar-module` describes its fat JAR this way. The SBOMs of
publications made from software components stay as they are.

Closes #768.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plan for #768 proposed a file that existed only to keep the count of
`TooManyFunctions` down. The review asked to keep cohesive code together
and to suppress the rule with a reason instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T17:39:03.115674Z d4eae1c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

Only minor documentation and API-clarity nits remain; no approval-blocking issues were identified.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Adds per-publication SBOM configuration to distinguish declared dependencies from bundled JAR contents, with SPDX rewriting and comprehensive tests.

Changes:

  • Adds sbom { dependencies(...) / bundled(...) } DSL.
  • Updates publication and uber-JAR SBOM generation.
  • Adds unit, integration, and supporting documentation coverage.
  • Two minor documentation/API clarity nits remain.
File Summary
buildSrc/​src/​test/​kotlin/​io/​spine/​gradle/​publish/​PublicationSbomIgTest.kt Adds integration coverage for custom and bundled publications.
buildSrc/​src/​test/​kotlin/​io/​spine/​gradle/​publish/​ArtifactComponentsSpec.kt Tests SPDX relationship rewriting.
buildSrc/​src/​main/​kotlin/​uber-jar-module.gradle.kts Marks uber-JAR contents as bundled.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​SpinePublishing.kt Documents custom SBOM configuration.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​SpdxField.kt Centralizes SPDX field names.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​SbomContent.kt Defines the public SBOM DSL.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​PublicationSbomTask.kt Applies publication-specific SBOM metadata.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​PublicationSbom.kt Registers publication-specific SBOM inputs and targets.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​ComponentKey.kt Matches Gradle components to SPDX packages.
buildSrc/​src/​main/​kotlin/​io/​spine/​gradle/​publish/​ArtifactComponents.kt Rewrites dependency and containment relationships.
.agents/​tasks/​sbom-custom-publications.md Records implementation design and verification.
.agents/​memory/​MEMORY.md Registers design guidance.
.agents/​memory/​feedback/​design-over-detekt-thresholds.md Documents file-boundary guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread buildSrc/src/main/kotlin/io/spine/gradle/publish/SbomContent.kt Outdated
The KDoc of `sbom { }` said that a later call adds to what the earlier
ones described, while `dependencies(...)` replaces the configuration an
earlier call gave. Now it says so, and each `bundled(...)` states that
it adds to the bundled content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexander-yevsyukov
alexander-yevsyukov merged commit ca7def7 into master Sep 25, 2026
2 checks passed
@alexander-yevsyukov
alexander-yevsyukov deleted the improve-sbom-generation branch September 25, 2026 18:04
alexander-yevsyukov added a commit to SpineEventEngine/core-jvm-compiler that referenced this pull request Sep 25, 2026
Brings in:
- SpineEventEngine/config#767: update the `agents` submodule.
- SpineEventEngine/config#769: let a publication describe what its SBOM
  lists, with `sbom { dependencies(...); bundled(...) }`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alexander-yevsyukov added a commit to SpineEventEngine/core-jvm-compiler that referenced this pull request Sep 25, 2026
With SpineEventEngine/config#769, a publication can tell its SBOM what
it depends on and what it bundles. Both publications of this build now
do so:

- `core-jvm-gradle-plugin` depends on what its POM declares, held by
  the new `pluginJarPom` configuration: the fat JAR, non-transitively,
  through its shadowed variant, and KotlinPoet. It bundles
  `bundledModules`: `grpc`, `ksp`, and `routing`.
- `core-jvm-plugins` depends on `fatJarPom`, which holds
  `pomDependencies`, and bundles what `shadowJar` packs.

The SBOM learns what `shadowJar` packs from its dependency filter only.
So `pomProvidedModules` now covers every module the POM declares, which
adds the Gradle plugins of the Compiler, Validation, Time, and Protobuf.
Their classes were stripped by path before. Now the fat JAR also loses
six stale entries that came with them: the plugin descriptors and
`.meta` files of the Time and Validation Gradle plugins, and the
`kotlin_module` files of the Compiler Gradle plugin and API. Consumers
get these with the plugins that the POM declares.

Both POMs, the plugin JAR, and the rest of the fat JAR stay byte for
byte the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Make SBOMs of custom publications match what they publish

3 participants