jamesfredley opened a new pull request, #15625:
URL: https://github.com/apache/grails-core/pull/15625
## Summary
The container-based release-vote verification described in `RELEASE.md`
fails on **v8.0.0-M1** because nine jars are not byte-identical between the
CI-published copies and a fresh local rebuild from the source distribution. The
SbomPlugin's existing deterministic `serialNumber` logic (PR #15614) is
correct, but three independent root causes upstream of it break reproducibility
for these specific files.
This PR addresses all three.
> Replaces #15624, which was opened against `grails8-groovy6-canary`. Same
commit, retargeted to `8.0.x` where the fix is most directly useful and where
CI is stable.
### Affected jars (verified via `verify.sh v8.0.0-M1`)
| Jar | What differs | Fix in this PR |
|---|---|---|
| `grails-async`, `grails-async-core`, `grails-async-gpars`,
`grails-async-rxjava{,2,3}`, `grails-bootstrap` | `META-INF/sbom.json` | Fix #1
|
| `grails-cache` | `META-INF/sbom.json` and
`META-INF/sbom/application.cdx.json` | Fix #1 + Fix #2 |
| `grails-bootstrap-javadoc` | 4 HTML files for
`FieldDefinition`/`PropertyDefinition` | Fix #3 |
## Root causes and fixes
### 1. `SbomPlugin` direct task: strip the auto-injected `build-system`
externalReference
`cyclonedx-gradle-plugin` v3.0.0 auto-injects a `build-system`
externalReference pointing at
`https://github.com/apache/grails-core/actions/runs/<run_id>` when GitHub
Actions env vars are present. This URL is unknowable from source, so its
presence breaks the deterministic `serialNumber` hash that `SbomPlugin` already
computes from the BOM content. Stripping this reference before the hash
recompute makes `META-INF/sbom.json` byte-identical between CI and local
rebuilds.
> **Note on relationship to #15614**: PR #15614's
`projectPath`-into-UUID-seed fix was already merged and active when v8.0.0-M1
was tagged (verified at commit `6b7c015`), yet these 9 jars still failed
reproducibility. #15614 prevents serialNumber *collisions* between distinct
modules with identical BOM content. This PR fixes the *same module* having
different BOM content between CI and local because of CI-only auto-injection.
The two fixes are orthogonal and complementary - this PR's refactored helper
preserves #15614's `projectPath` mixing.
### 2. `SbomPlugin` aggregate task: apply the same reproducibility transforms
Spring Boot 4 `CycloneDxPluginAction` wires the cyclonedx-gradle-plugin
aggregate task (`CyclonedxAggregateTask`, default name `cyclonedxBom`) into
Grails-plugin jars, packaging its output at
`META-INF/sbom/application.cdx.json`. `SbomPlugin` previously only configured
the direct task (`CyclonedxDirectTask`), so the aggregate SBOM had:
- a random `serialNumber` UUID
- an `Instant.now()` timestamp that ignored `SOURCE_DATE_EPOCH`
- CI auto-detected externalReferences (`build-system` and `vcs`)
The rewrite logic is extracted into a shared `rewriteSbomFile` helper
applied to both task types via `configureSbomTask` and a new
`configureAggregateSbomReproducibility` method. The aggregate strip set covers
both `build-system` AND `vcs` because the aggregate task has no explicit
`externalReferences` configuration to fall back on (unlike the direct task
which sets four explicit refs).
### 3. `FieldDefinition` / `PropertyDefinition`: qualify `Builder` return
types for groovydoc
Both classes declare a static inner `Builder` class with the same simple
name, and both extend `AbstractMemberDefinition`. The `static Builder
builder()` factory method previously used the unqualified return type
`Builder`. When **groovydoc** walks the class graph, it resolves that simple
name based on file system iteration order. Different filesystems produce
different (and incorrect) HTML:
- One ordering: every `Builder` reference renders as
`PropertyDefinition.Builder` (so `FieldDefinition.html` is wrong)
- Other ordering: every `Builder` reference renders as
`FieldDefinition.Builder` (so `PropertyDefinition.html` is wrong)
This is **not a Gradle 9 issue** (Gradle 9's reproducibility fixes target
`AbstractArchiveTask`, not the groovydoc tool's name resolution), and
`noTimestamp = true` is already configured in `GroovydocEnhancerPlugin`, so the
difference is content, not metadata. Qualifying the return types as `static
FieldDefinition.Builder builder()` and `static PropertyDefinition.Builder
builder()` forces groovydoc to render the correct (and consistent) types
regardless of iteration order.
**Compiled bytecode is unchanged** because the nested `Builder` classes
already resolved correctly via lexical scope at the `groovyc` stage.
## Verification
Built `grails-async-core`, `grails-bootstrap`, and `grails-cache` twice with
the same `SOURCE_DATE_EPOCH` and confirmed:
```
✅ grails-async-core sbom.json: byte-identical between runs
✅ grails-bootstrap sbom.json: byte-identical between runs
✅ grails-cache sbom.json: byte-identical between runs
✅ grails-cache application.cdx.json: byte-identical between runs
✅ no build-system externalReference in any sbom.json
✅ no build-system or vcs externalReference in application.cdx.json
✅ deterministic urn:uuid serialNumber format (with #15614's projectPath
mixing preserved)
```
Existing `FieldDefinitionSpec` and `PropertyDefinitionSpec` test suites pass.
## Test plan
- [x] `:grails-bootstrap:test --tests
"grails.codegen.model.FieldDefinitionSpec" --tests
"grails.codegen.model.PropertyDefinitionSpec"` passes
- [x] `:grails-bootstrap:codeStyle` passes
- [x] `:build-logic:compileGroovy` passes
- [x] `:grails-async-core:cyclonedxDirectBom`,
`:grails-bootstrap:cyclonedxDirectBom`, `:grails-cache:cyclonedxDirectBom`
produce byte-identical output across two consecutive runs with the same
`SOURCE_DATE_EPOCH`
- [x] `:grails-cache:cyclonedxBom` (aggregate) produces byte-identical
output across two runs
## Notes for reviewers
- The aggregate task strip set (`['build-system', 'vcs']`) is intentionally
broader than the direct task's (`['build-system']`). The direct task explicitly
sets a known-good `vcs` reference at `SbomPlugin.groovy:191-193`, so we only
need to strip the auto-injected `build-system`. The aggregate task has no such
explicit config, so any externalReferences it has come from CI/`.git`
auto-detection that won't reproduce locally.
- For the groovydoc fix, an alternative approach would be renaming one of
the inner `Builder` classes (`FieldDefinitionBuilder` /
`PropertyDefinitionBuilder`) to remove the simple-name collision entirely.
That's an API-breaking change so I went with the non-breaking qualification
approach. If you'd prefer the rename, happy to switch.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]