From d6a80e0c9ea36c444ef6791bba71302c437822f0 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 30 Apr 2026 18:23:19 -0400 Subject: [PATCH 1/5] fix(sbom): mix projectPath into deterministic UUID seed The SbomPlugin generates the BOM serialNumber by hashing the post-processed JSON with UUID.nameUUIDFromBytes() so rebuilds produce identical SBOMs. After upgrading to Gradle 9 and CycloneDX gradle plugin 3.0.0, multiple modules now produce JSON whose post-processed body collides (notably the empty BOM platforms), causing duplicate urn:uuid serialNumbers and violating the CycloneDX 1.6 specification. Mix the captured projectPath into the hash input. Different modules now seed the hash with a different prefix while the same module + same content keeps yielding the same UUID across rebuilds, so the reproducible-build behavior is preserved. Verified locally on Gradle 9.4.1: cyclonedxDirectBom on six modules (grails-bom, grails-base-bom, grails-hibernate5-bom, grails-micronaut-bom, grails-bootstrap, grails-encoder) produces six distinct serialNumbers that are stable across rebuilds. Assisted-by: claude-code:claude-opus-4 --- .../org/apache/grails/buildsrc/SbomPlugin.groovy | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy index 7af23167462..2212d7e0511 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy @@ -261,10 +261,20 @@ class SbomPlugin implements Plugin { } } - // force the serialNumber to be reproducible by removing it & recalculating + // force the serialNumber to be reproducible by removing it & recalculating. + // Mix the projectPath into the UUID seed so two modules whose post-processed + // BOM JSON happens to be identical (for example, empty BOM platforms with no + // runtime dependencies, or modules whose metadata.component is filled in + // identically by the CycloneDX plugin) still receive distinct serialNumbers + // as required by the CycloneDX specification. Including projectPath preserves + // reproducibility because the same project path + same content always yields + // the same UUID across rebuilds. This guards against collisions introduced by + // CycloneDX 3.0.0 / Gradle 9 metadata changes. + // See: https://cyclonedx.org/docs/1.6/json/#serialNumber bom['serialNumber'] = '' def withOutSerial = JsonOutput.prettyPrint(JsonOutput.toJson(bom)) - def uuid = UUID.nameUUIDFromBytes(withOutSerial.getBytes(StandardCharsets.UTF_8.name())) + def uuidSeed = "${projectPath}\n${withOutSerial}" + def uuid = UUID.nameUUIDFromBytes(uuidSeed.getBytes(StandardCharsets.UTF_8.name())) bom['serialNumber'] = "urn:uuid:$uuid".toString() f.setText(JsonOutput.prettyPrint(JsonOutput.toJson(bom)), StandardCharsets.UTF_8.name()) From 462b2f8d45de729f345086de8ccc4115e0831665 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 30 Apr 2026 18:33:29 -0400 Subject: [PATCH 2/5] fix(sbom): address Copilot review feedback Pure cleanup of the prior commit on this branch: - Comment now says `clearing it` instead of `removing it` to match the actual behavior of `bom['serialNumber'] = ''` (the key is blanked, not deleted from the map). - Renamed local `withOutSerial` to `withoutSerial` (proper camelCase). - Use `StandardCharsets.UTF_8` directly instead of `StandardCharsets.UTF_8.name()` so Java picks the `Charset` overload of `String.getBytes` and skips the runtime charset name lookup. Re-verified locally on Gradle 9.4.1 against the same six modules: every serialNumber is byte-identical to the prior commit, confirming the behavior is unchanged. Assisted-by: claude-code:claude-opus-4 --- .../groovy/org/apache/grails/buildsrc/SbomPlugin.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy index 2212d7e0511..f12282074ec 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy @@ -261,7 +261,7 @@ class SbomPlugin implements Plugin { } } - // force the serialNumber to be reproducible by removing it & recalculating. + // force the serialNumber to be reproducible by clearing it & recalculating. // Mix the projectPath into the UUID seed so two modules whose post-processed // BOM JSON happens to be identical (for example, empty BOM platforms with no // runtime dependencies, or modules whose metadata.component is filled in @@ -272,9 +272,9 @@ class SbomPlugin implements Plugin { // CycloneDX 3.0.0 / Gradle 9 metadata changes. // See: https://cyclonedx.org/docs/1.6/json/#serialNumber bom['serialNumber'] = '' - def withOutSerial = JsonOutput.prettyPrint(JsonOutput.toJson(bom)) - def uuidSeed = "${projectPath}\n${withOutSerial}" - def uuid = UUID.nameUUIDFromBytes(uuidSeed.getBytes(StandardCharsets.UTF_8.name())) + def withoutSerial = JsonOutput.prettyPrint(JsonOutput.toJson(bom)) + def uuidSeed = "${projectPath}\n${withoutSerial}" + def uuid = UUID.nameUUIDFromBytes(uuidSeed.getBytes(StandardCharsets.UTF_8)) bom['serialNumber'] = "urn:uuid:$uuid".toString() f.setText(JsonOutput.prettyPrint(JsonOutput.toJson(bom)), StandardCharsets.UTF_8.name()) From 4d1a78f1e2b77329dc2b0b354bb5dd46d8354d99 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 30 Apr 2026 20:57:17 -0400 Subject: [PATCH 3/5] fix(sbom): also fix UUID collisions in com.gradleup.shadow fat jars Discovered while running the verify-cli/verify-wrapper smoke tests against the build outputs of :grails-cli, :grails-cli-shadow, :grails-shell-cli, :grails-forge-cli and :grails-wrapper. Both :grails-cli (-all.jar) and :grails-cli-shadow (-all.jar) bundled grails-shell-cli as a transitive dep, and com.gradleup.shadow's first-wins merge of META-INF/sbom.json ended up putting grails-shell-cli's SBOM into both fat jars. Result: two distinct fat jars shared a single serialNumber and both reported metadata.component.name="grails-shell-cli" - a CycloneDX 1.6 violation that the previous projectPath-seed fix did not catch because that fix only changed how each project's own SBOM JSON is generated, not how shadow merges other modules' SBOMs into a fat jar. This commit applies a fix symmetrical to publishSbomForJarProjects: * SbomPlugin gains publishSbomForShadowJarProjects, which, for any project that applies com.gradleup.shadow, excludes incoming META-INF/sbom.json from the shadow merge and re-introduces the project's own SBOM (whose serialNumber is now project-path-seeded and unique). Manifest gets the same Sbom-Location / Sbom-Format attributes the regular jar already has. Uses Task + cast to Jar so build-logic does not need a compile-time dependency on the shadow plugin types. * grails-forge/grails-cli-shadow does not apply org.apache.grails.buildsrc.sbom (it is an intermediate build artifact, not published), so the SbomPlugin hook does not fire for it. Its shadowJar exclude list now drops META-INF/sbom.json directly so the intermediate fat jar - which feeds :grails-cli's shadowCombined configuration - cannot smuggle a wrong SBOM back in. Verified on Gradle 9.4.1 with SOURCE_DATE_EPOCH set, --no-build-cache, --rerun-tasks (the same flags test-reproducible-builds.sh uses): jar serialNumber :grails-shell-cli (regular) 631bbcd5... :grails-wrapper (regular) 9c5f6980... :grails-cli (regular) 3e4ea827... :grails-cli (-all FAT) 3e4ea827... * :grails-cli-shadow (regular) (no SBOM - empty) :grails-cli-shadow (-all FAT) (no SBOM - excluded) :grails-forge-cli (regular) 890372ec... *) :grails-cli's regular and FAT jar share a serialNumber by design: they ship the same project's SBOM, the FAT jar just adds bundled classes. The CycloneDX uniqueness constraint applies across BOMs describing different artifacts, not across two jars whose embedded BOM happens to describe the same project. Smoke-tested both fat-jar CLIs against verify-cli-distribution.sh flow: apache-grails-8.0.0-SNAPSHOT-bin.zip extracts cleanly with LICENSE and NOTICE present, bin/grails-shell-cli --version and bin/grails-forge-cli --version both return version + JVM info, bin/grails-shell-cli create-app ShellApp generates a complete Grails app, bin/grails-forge-cli create-app -x -g mongodb -f gradle-settings-file ForgeApp generates a complete Grails app, java -jar grails-cli-8.0.0-SNAPSHOT-all.jar --version still works with the new SBOM in place. Assisted-by: claude-code:claude-opus-4 --- .../apache/grails/buildsrc/SbomPlugin.groovy | 51 +++++++++++++++++++ grails-forge/grails-cli-shadow/build.gradle | 6 +++ 2 files changed, 57 insertions(+) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy index f12282074ec..06fafa48658 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy @@ -36,6 +36,7 @@ import org.cyclonedx.model.OrganizationalEntity import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.Task import org.gradle.api.file.CopySpec import org.gradle.api.file.RegularFile import org.gradle.api.java.archives.Manifest @@ -142,6 +143,7 @@ class SbomPlugin implements Plugin { // sboms are only published to Grails jar files at this time publishSbomForJarProjects(project, sbomOutputLocation) + publishSbomForShadowJarProjects(project, sbomOutputLocation) } private static void configureSbomTask(Project project, Provider sbomOutputLocation) { @@ -394,4 +396,53 @@ class SbomPlugin implements Plugin { } } } + + /** + * Wires this project's own SBOM into the shadow jar produced by the + * com.gradleup.shadow plugin. + * + * Without this, shadow's first-wins merge picks up a META-INF/sbom.json + * from one of the bundled transitive jars (typically grails-shell-cli's), + * giving the fat jar the wrong serialNumber and metadata.component + * (it ends up describing grails-shell-cli rather than the fat jar's own + * project). Two fat jars that both bundle grails-shell-cli (e.g. + * :grails-cli and :grails-cli-shadow) then end up with byte-identical + * META-INF/sbom.json entries and identical urn:uuid serialNumbers, + * which violates the CycloneDX 1.6 specification. + * + * The fix is symmetrical with publishSbomForJarProjects: we exclude any + * META-INF/sbom.json that arrives from transitive dependencies during + * the shadow merge, then re-introduce this project's own SBOM (whose + * serialNumber is project-path-seeded and unique per fix(sbom): mix + * projectPath into deterministic UUID seed). + * + * Uses the broad Task type to avoid a compile-time dependency on + * com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar from + * build-logic; the cast is safe because ShadowJar extends Jar. + */ + private static void publishSbomForShadowJarProjects(Project project, Provider sbomOutputLocation) { + project.plugins.withId('com.gradleup.shadow') { + project.afterEvaluate { + if (!project.findProperty('skipJavaComponent')) { + project.tasks.named('shadowJar').configure { Task t -> + Jar shadowJar = (Jar) t + shadowJar.dependsOn('cyclonedxDirectBom') + // Drop any META-INF/sbom.json that comes in via transitive jars during the merge. + shadowJar.exclude('META-INF/sbom.json') + // Re-introduce this project's own SBOM after the merge. + shadowJar.from(sbomOutputLocation) { CopySpec spec -> + spec.into('META-INF') + spec.rename { + 'sbom.json' + } + } + shadowJar.manifest { Manifest manifest -> + manifest.attributes('Sbom-Location': 'META-INF/sbom.json') + manifest.attributes('Sbom-Format': 'CycloneDX') + } + } + } + } + } + } } diff --git a/grails-forge/grails-cli-shadow/build.gradle b/grails-forge/grails-cli-shadow/build.gradle index 6978124d70c..21c1ee17e80 100644 --- a/grails-forge/grails-cli-shadow/build.gradle +++ b/grails-forge/grails-cli-shadow/build.gradle @@ -77,6 +77,12 @@ shadowJarTask.configure { ShadowJar it -> it.exclude( 'META-INF/DEPENDENCIES', // until we publish our own SBOM, this won't be correct so exclude + // This module does not apply org.apache.grails.buildsrc.sbom (it's an intermediate build + // artifact, not published). Without this exclude, shadow's first-wins merge picks one of + // the bundled transitive META-INF/sbom.json files (typically grails-shell-cli's), + // producing a fat jar whose SBOM describes the wrong module and shares its serialNumber + // with whichever sibling jar happens to win the merge - violating CycloneDX 1.6. + 'META-INF/sbom.json', 'about.html' // restatement of the Eclipse Distribution License - Version 1.0 for jakarta ) } From 23f586d4f6b363ebe889ea4225d1f45df2eed861 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 30 Apr 2026 22:16:49 -0400 Subject: [PATCH 4/5] fix(sbom): move shadow jar SBOM wiring into grails-cli build.gradle Per review feedback on PR #15614, configure the fat-jar SBOM logic directly in the project that uses com.gradleup.shadow rather than adding a shadow-aware hook to the generic SbomPlugin convention plugin. * Remove publishSbomForShadowJarProjects from SbomPlugin and the raw Task -> Jar cast it required. The plugin no longer needs to know about com.gradleup.shadow at all; its only responsibility is wiring cyclonedxDirectBom into the regular jar via publishSbomForJarProjects. * Wire the same exclude / from / manifest logic into the existing shadowJarTask.configure block in grails-forge/grails-cli/build.gradle, using a typed TaskProvider and cyclonedxDirectBomTask.flatMap { it.jsonOutput } so the shadow jar picks up only the post-processed sbom.json output (and not any future task outputs). The skipJavaComponent guard is preserved to match the convention used by publishSbomForJarProjects. * :grails-cli-shadow/build.gradle is unchanged; it already excludes META-INF/sbom.json directly because that module does not apply org.apache.grails.buildsrc.sbom. Verified on Gradle 9.4.1 with --rerun-tasks: the regular and fat :grails-cli jars share a serialNumber (by design, same project), the fat jar's metadata.component.name is grails-cli (not the leaked grails-shell-cli), and :grails-forge-cli has a distinct serialNumber. :grails-cli-shadow's fat jar correctly contains no META-INF/sbom.json. Assisted-by: claude-code:claude-opus-4 --- .../apache/grails/buildsrc/SbomPlugin.groovy | 56 ++----------------- grails-forge/grails-cli/build.gradle | 30 ++++++++++ 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy index 06fafa48658..db20a07e60b 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy @@ -36,7 +36,6 @@ import org.cyclonedx.model.OrganizationalEntity import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.Task import org.gradle.api.file.CopySpec import org.gradle.api.file.RegularFile import org.gradle.api.java.archives.Manifest @@ -141,9 +140,11 @@ class SbomPlugin implements Plugin { configureNormalization(project) ensureLicensesValidated(project) - // sboms are only published to Grails jar files at this time + // sboms are only published to Grails jar files at this time. Projects that produce a fat + // jar via com.gradleup.shadow are responsible for wiring the SBOM into their shadowJar + // task in their own build.gradle (see grails-forge/grails-cli/build.gradle), since fat-jar + // packaging is a project-specific concern and shadow is not a dependency of this plugin. publishSbomForJarProjects(project, sbomOutputLocation) - publishSbomForShadowJarProjects(project, sbomOutputLocation) } private static void configureSbomTask(Project project, Provider sbomOutputLocation) { @@ -396,53 +397,4 @@ class SbomPlugin implements Plugin { } } } - - /** - * Wires this project's own SBOM into the shadow jar produced by the - * com.gradleup.shadow plugin. - * - * Without this, shadow's first-wins merge picks up a META-INF/sbom.json - * from one of the bundled transitive jars (typically grails-shell-cli's), - * giving the fat jar the wrong serialNumber and metadata.component - * (it ends up describing grails-shell-cli rather than the fat jar's own - * project). Two fat jars that both bundle grails-shell-cli (e.g. - * :grails-cli and :grails-cli-shadow) then end up with byte-identical - * META-INF/sbom.json entries and identical urn:uuid serialNumbers, - * which violates the CycloneDX 1.6 specification. - * - * The fix is symmetrical with publishSbomForJarProjects: we exclude any - * META-INF/sbom.json that arrives from transitive dependencies during - * the shadow merge, then re-introduce this project's own SBOM (whose - * serialNumber is project-path-seeded and unique per fix(sbom): mix - * projectPath into deterministic UUID seed). - * - * Uses the broad Task type to avoid a compile-time dependency on - * com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar from - * build-logic; the cast is safe because ShadowJar extends Jar. - */ - private static void publishSbomForShadowJarProjects(Project project, Provider sbomOutputLocation) { - project.plugins.withId('com.gradleup.shadow') { - project.afterEvaluate { - if (!project.findProperty('skipJavaComponent')) { - project.tasks.named('shadowJar').configure { Task t -> - Jar shadowJar = (Jar) t - shadowJar.dependsOn('cyclonedxDirectBom') - // Drop any META-INF/sbom.json that comes in via transitive jars during the merge. - shadowJar.exclude('META-INF/sbom.json') - // Re-introduce this project's own SBOM after the merge. - shadowJar.from(sbomOutputLocation) { CopySpec spec -> - spec.into('META-INF') - spec.rename { - 'sbom.json' - } - } - shadowJar.manifest { Manifest manifest -> - manifest.attributes('Sbom-Location': 'META-INF/sbom.json') - manifest.attributes('Sbom-Format': 'CycloneDX') - } - } - } - } - } - } } diff --git a/grails-forge/grails-cli/build.gradle b/grails-forge/grails-cli/build.gradle index 79b4657d98c..1f16d876cf3 100644 --- a/grails-forge/grails-cli/build.gradle +++ b/grails-forge/grails-cli/build.gradle @@ -18,6 +18,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.apache.grails.forge.buildlogic.shadowjar.GrailsGroovyExtensionTransformer import org.apache.grails.forge.buildlogic.shadowjar.GrailsShadowLicenseTransform import org.apache.grails.forge.buildlogic.shadowjar.GrailsShadowNoticeTransform +import org.cyclonedx.gradle.CyclonedxDirectTask plugins { id 'groovy' @@ -92,6 +93,16 @@ jarTask.configure { Jar it -> } } +// The shadowJar merges multiple jars (including transitive ones from grails-shell-cli, grails-forge-cli, +// etc.) into a single fat jar. Each of those source jars carries its own META-INF/sbom.json published by +// the org.apache.grails.buildsrc.sbom convention plugin, and shadow's first-wins merge would otherwise +// pick a transitive sbom.json (typically grails-shell-cli's) and produce a fat jar whose SBOM describes +// the wrong module. We exclude any incoming META-INF/sbom.json during the merge and then re-introduce +// this project's own SBOM (whose serialNumber is project-path-seeded and unique). This keeps fat-jar +// packaging concerns local to this project rather than leaking shadow knowledge into the generic +// org.apache.grails.buildsrc.sbom plugin. See: https://cyclonedx.org/docs/1.6/json/#serialNumber +TaskProvider cyclonedxDirectBomTask = tasks.named('cyclonedxDirectBom', CyclonedxDirectTask) + TaskProvider shadowJarTask = tasks.named('shadowJar', ShadowJar) shadowJarTask.configure { ShadowJar it -> it.archiveClassifier = 'all' @@ -122,8 +133,27 @@ shadowJarTask.configure { ShadowJar it -> 'META-INF/DEPENDENCIES', // until we publish our own SBOM, this won't be correct so exclude 'META-INF/grails-plugin.xml', // we do not start or compile a grails application so these files are not needed (grails-core, url mappings, etc plugins) 'META-INF/grails-plugin.xml.asc', // avoid signing artifacts + // Drop any incoming sbom.json that arrives via transitive jars during the shadow merge; + // re-introduced below from this project's own cyclonedxDirectBom output. + 'META-INF/sbom.json', 'about.html' // restatement of the Eclipse Distribution License - Version 1.0 for jakarta ) + + // Re-introduce this project's own SBOM after the merge (mirrors the regular jar wiring done by + // the org.apache.grails.buildsrc.sbom plugin). Mirrored only when skipJavaComponent is unset to + // match the convention used elsewhere in the build for projects that opt out of jar publication. + if (!project.findProperty('skipJavaComponent')) { + it.from(cyclonedxDirectBomTask.flatMap { CyclonedxDirectTask t -> t.jsonOutput }) { CopySpec spec -> + spec.into('META-INF') + spec.rename { + 'sbom.json' + } + } + it.manifest { Manifest manifest -> + manifest.attributes('Sbom-Location': 'META-INF/sbom.json') + manifest.attributes('Sbom-Format': 'CycloneDX') + } + } } // Make shadow jar a direct dependency of assemble instead of using deprecated archives configuration tasks.named('assemble').configure { From 1144b74e8acf8b0086e5241ff0ab57b8a84e055a Mon Sep 17 00:00:00 2001 From: James Fredley Date: Sat, 2 May 2026 07:15:31 -0400 Subject: [PATCH 5/5] fix(sbom): address jdaugherty review feedback - Restore the original one-line comment in SbomPlugin#apply; the shadow-jar wiring lives entirely in grails-cli/build.gradle now, so the verbose explanation in the generic plugin is unnecessary. - Drop the long block comment in grails-cli-shadow/build.gradle and inline a short note next to the META-INF/sbom.json exclude. - In grails-cli/build.gradle, move the cyclonedxDirectBomTask lookup into the shadowJar configure block (its only caller), pass the task provider directly to from(...) instead of flatMap { it.jsonOutput } (jsonOutput is the only effective output because SbomPlugin calls xmlOutput.unsetConvention()), and shorten the comment. Verified on Gradle 9.4.1 with --rerun-tasks twice: - :grails-cli (regular jar) urn:uuid:beabd2c0-... / grails-cli - :grails-cli (-all FAT) urn:uuid:beabd2c0-... / grails-cli - :grails-cli-shadow (-all) no META-INF/sbom.json (excluded) - Sbom-Location / Sbom-Format manifest entries present on the fat jar - :grails-bootstrap urn:uuid:3fe6e19d-... matches PR description, so end-to-end reproducibility across the whole build is preserved. Assisted-by: claude-code:claude-4.6-opus --- .../apache/grails/buildsrc/SbomPlugin.groovy | 5 +---- grails-forge/grails-cli-shadow/build.gradle | 7 +------ grails-forge/grails-cli/build.gradle | 21 ++++--------------- 3 files changed, 6 insertions(+), 27 deletions(-) diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy index db20a07e60b..f12282074ec 100644 --- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy +++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy @@ -140,10 +140,7 @@ class SbomPlugin implements Plugin { configureNormalization(project) ensureLicensesValidated(project) - // sboms are only published to Grails jar files at this time. Projects that produce a fat - // jar via com.gradleup.shadow are responsible for wiring the SBOM into their shadowJar - // task in their own build.gradle (see grails-forge/grails-cli/build.gradle), since fat-jar - // packaging is a project-specific concern and shadow is not a dependency of this plugin. + // sboms are only published to Grails jar files at this time publishSbomForJarProjects(project, sbomOutputLocation) } diff --git a/grails-forge/grails-cli-shadow/build.gradle b/grails-forge/grails-cli-shadow/build.gradle index 21c1ee17e80..bff0ddc3e50 100644 --- a/grails-forge/grails-cli-shadow/build.gradle +++ b/grails-forge/grails-cli-shadow/build.gradle @@ -77,12 +77,7 @@ shadowJarTask.configure { ShadowJar it -> it.exclude( 'META-INF/DEPENDENCIES', // until we publish our own SBOM, this won't be correct so exclude - // This module does not apply org.apache.grails.buildsrc.sbom (it's an intermediate build - // artifact, not published). Without this exclude, shadow's first-wins merge picks one of - // the bundled transitive META-INF/sbom.json files (typically grails-shell-cli's), - // producing a fat jar whose SBOM describes the wrong module and shares its serialNumber - // with whichever sibling jar happens to win the merge - violating CycloneDX 1.6. - 'META-INF/sbom.json', + 'META-INF/sbom.json', // intermediate build, exclude conflicting files 'about.html' // restatement of the Eclipse Distribution License - Version 1.0 for jakarta ) } diff --git a/grails-forge/grails-cli/build.gradle b/grails-forge/grails-cli/build.gradle index 1f16d876cf3..1e01cc5c9e0 100644 --- a/grails-forge/grails-cli/build.gradle +++ b/grails-forge/grails-cli/build.gradle @@ -93,16 +93,6 @@ jarTask.configure { Jar it -> } } -// The shadowJar merges multiple jars (including transitive ones from grails-shell-cli, grails-forge-cli, -// etc.) into a single fat jar. Each of those source jars carries its own META-INF/sbom.json published by -// the org.apache.grails.buildsrc.sbom convention plugin, and shadow's first-wins merge would otherwise -// pick a transitive sbom.json (typically grails-shell-cli's) and produce a fat jar whose SBOM describes -// the wrong module. We exclude any incoming META-INF/sbom.json during the merge and then re-introduce -// this project's own SBOM (whose serialNumber is project-path-seeded and unique). This keeps fat-jar -// packaging concerns local to this project rather than leaking shadow knowledge into the generic -// org.apache.grails.buildsrc.sbom plugin. See: https://cyclonedx.org/docs/1.6/json/#serialNumber -TaskProvider cyclonedxDirectBomTask = tasks.named('cyclonedxDirectBom', CyclonedxDirectTask) - TaskProvider shadowJarTask = tasks.named('shadowJar', ShadowJar) shadowJarTask.configure { ShadowJar it -> it.archiveClassifier = 'all' @@ -133,17 +123,14 @@ shadowJarTask.configure { ShadowJar it -> 'META-INF/DEPENDENCIES', // until we publish our own SBOM, this won't be correct so exclude 'META-INF/grails-plugin.xml', // we do not start or compile a grails application so these files are not needed (grails-core, url mappings, etc plugins) 'META-INF/grails-plugin.xml.asc', // avoid signing artifacts - // Drop any incoming sbom.json that arrives via transitive jars during the shadow merge; - // re-introduced below from this project's own cyclonedxDirectBom output. - 'META-INF/sbom.json', + 'META-INF/sbom.json', // re-introduced below from this project's own cyclonedxDirectBom output to avoid the wrong transitive SBOM winning the shadow merge 'about.html' // restatement of the Eclipse Distribution License - Version 1.0 for jakarta ) - // Re-introduce this project's own SBOM after the merge (mirrors the regular jar wiring done by - // the org.apache.grails.buildsrc.sbom plugin). Mirrored only when skipJavaComponent is unset to - // match the convention used elsewhere in the build for projects that opt out of jar publication. + // Re-introduce this project's own SBOM into the fat jar (mirrors org.apache.grails.buildsrc.sbom). if (!project.findProperty('skipJavaComponent')) { - it.from(cyclonedxDirectBomTask.flatMap { CyclonedxDirectTask t -> t.jsonOutput }) { CopySpec spec -> + TaskProvider cyclonedxDirectBomTask = tasks.named('cyclonedxDirectBom', CyclonedxDirectTask) + it.from(cyclonedxDirectBomTask) { CopySpec spec -> spec.into('META-INF') spec.rename { 'sbom.json'