jamesfredley opened a new pull request, #15624:
URL: https://github.com/apache/grails-core/pull/15624
## 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 and the project's
existing reproducibility infrastructure are correct, but three independent root
causes upstream of them break reproducibility for these specific files.
This PR addresses all three.
### 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.
### 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: byte-identical between runs
✅ grails-bootstrap : byte-identical between runs
✅ grails-cache : byte-identical between runs (sbom.json AND
application.cdx.json)
✅ no build-system externalReference in any sbom.json
✅ no build-system or vcs externalReference in application.cdx.json
✅ deterministic urn:uuid serialNumber format
```
Existing `FieldDefinitionSpec` and `PropertyDefinitionSpec` test suites
continue to pass because the bytecode-level `builder()` return type is
unchanged.
End-to-end CI-vs-local validation will happen on this PR's CI build: once
merged and a release is cut, `verify.sh v<next_release>` should pass on the
previously-failing 9 jars.
## Test plan
- [x] `./gradlew :grails-bootstrap:test --tests
"grails.codegen.model.FieldDefinitionSpec" --tests
"grails.codegen.model.PropertyDefinitionSpec"` passes
- [x] `./gradlew :grails-bootstrap:codeStyle` passes
- [x] `./gradlew :build-logic:compileGroovy` (in `build-logic/`) 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.
- Build-logic does not currently have a `codeStyle` task or unit-test
infrastructure for plugins; the verification was done via behavioral
integration build. If you'd like to add `SbomPluginSpec` coverage in this PR,
happy to extend - just say the word.
- 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]