Let a publication describe what its SBOM lists - #769
Conversation
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>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
Only minor documentation and API-clarity nits remain; no approval-blocking issues were identified.
Review effort: Lite
Findings: 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.
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>
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>
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>

Closes #768.
Problem
PublicationSbom(#764) writes the SBOM of every JVM publication fromruntimeClasspath. 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 withartifact(...), which is how custom publications anduber-jar-modulepublish:core-jvm-gradle-pluginlackedcore-jvm-plugins, its main runtime dependency.DEPENDS_ONdependencies.uber-jar-modulehas 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:
DEPENDS_ONthe graph ofdependencies.CONTAINSfrom the artifact, and neverDEPENDS_ONfrom it.uber-jar-moduledeclaresbundled(tasks.shadowJar)for itsfatJar.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 ofcore-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 rootprojectsEvaluatedhook: each module's own SBOM task resolves its configurations.Verification
./gradlew :buildSrc:build detektpasses with 144 tests.PublicationSbomIgTestgains 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: theuber-jar-modulepattern.ArtifactComponentsSpeccovers the rewrite on hand-written SPDX documents.core-jvm-compiler, smoke-tested in a scratch clone ofpublish-sboms, with step 3 of the issue simulated by POM-mirror configurations andsbom { }:core-jvm-gradle-pluginDEPENDS_ONexactlycore-jvm-pluginsandkotlinpoet-ksp, andCONTAINSgrpc,kspandrouting.core-jvm-pluginslists 24 of its 25 POM dependencies asDEPENDS_ON, and 26 bundled components asCONTAINS.tool-base, smoke-tested in a scratch clone:
intellij-platformandintellij-platform-javaCONTAINexactly the JetBrains artifacts their Shadowincludefilter admits. Two other modules could not be built: syncing the currentbuildSrcbrings akotlinx-coroutines-bom1.11.0 vs 1.10.2 conflict, unrelated to this change.Notes for reviewers
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 ofbundled(TaskProvider<ShadowJar>)says so.core-jvm-compilerfollow-up (step 3 of the issue):compiler-gradle-plugin,compiler-gradle-api,time-gradle-plugin,validation-gradle-pluginandprotobuf-gradle-plugin. They belong inpomProvidedModules. With that, none of its POM dependencies is reported as bundled, andverifyBundledPackagesstill passes.symbol-processing-gradle-plugin, instead..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