Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
6fdcca3
canary: test Groovy 6.0.0-SNAPSHOT
jamesfredley Apr 5, 2026
1ff5858
fix: Groovy 6 VerifyError in DefaultConstraintFactory default parameter
jamesfredley Apr 5, 2026
0fb8c5c
fix: Groovy 6 compile fixes - Spock version check and CycloneDX license
jamesfredley Apr 6, 2026
52b8812
fix: Groovy 6 genericGetMethod regression breaks property access on G…
jamesfredley Apr 6, 2026
c43633c
fix: GormEntity.get(String) throws MissingPropertyException when GORM…
jamesfredley Apr 6, 2026
ae85689
fix: centralize Spock version check and add jline 4.0.7 CycloneDX ove…
jamesfredley Apr 6, 2026
0fe29be
fix: Groovy 6 remaining test fixes
jamesfredley Apr 6, 2026
a0e42c8
fix: CodeNarc UnnecessaryDotClass in DefaultConstraintFactory
jamesfredley Apr 6, 2026
8abd9ba
fix: GormEntity.get(String) delegates to staticPropertyMissing for GO…
jamesfredley Apr 6, 2026
600c950
fix: harden Groovy 6 canary against snapshot drift and add regression…
jamesfredley Apr 25, 2026
1c35dbd
fix: Groovy 6 closure dispatch regression in ControllerActionTransformer
jamesfredley Apr 25, 2026
557a28b
fix: move Groovy 6 generic-getter guard from GormEntity trait to AST
jamesfredley Apr 25, 2026
d45aa28
fix: serialise GSP compilation under Groovy 6 to dodge ListHashMap race
jamesfredley Apr 25, 2026
3180aad
fix(sbom): revert LICENSE_GROUP_MAPPING per @jdaugherty review
jamesfredley Apr 25, 2026
47bd1a5
test(forge): capture both stdout and stderr from generated-app gradle…
jamesfredley Apr 25, 2026
b214411
fix(forge): bypass Spock Groovy-version check in generated app build.…
jamesfredley Apr 25, 2026
8596914
fix: serialise GSON/views template compilation under Groovy 6
jamesfredley Apr 25, 2026
0195558
docs: drop incorrect GROOVY-11829 citation from GormEntity workaround…
jamesfredley Apr 26, 2026
112b491
Add standalone reproducer link to GormEntity get(String) AST shim
jamesfredley Apr 27, 2026
d59ab8c
drop Groovy 6 workarounds whose upstream fixes have merged
May 2, 2026
ee41092
fix: workaround Groovy 6 stub generator regression in HibernateSettings
May 2, 2026
7fdd14e
Drop two more Groovy 6 workarounds - upstream fixes merged 2026-05-02
jamesfredley May 3, 2026
1524941
Drop GORM generic-getter workaround - GROOVY-11986 fixed upstream
jamesfredley May 8, 2026
41885c4
fix(ci): resolve 3 of 4 distinct CI failure categories on grails8-gro…
jamesfredley May 22, 2026
cbd37f0
fix: drop ConfigurationBuilder @Builder-detection workaround (GROOVY-…
jamesfredley May 29, 2026
ff19a5a
fix(views-gson): work around Groovy 6 Verifier abstract-method regres…
jamesfredley May 29, 2026
496850a
fix(build): map JLine 4.1.0 transitive modules in SBOM license allowlist
jamesfredley May 29, 2026
4ed1515
fix(build): propagate Spock version-check opt-out to the forked view …
jamesfredley May 29, 2026
a2a769e
fix(testing-http-client): correct XmlUtils secure-slurper feature URI…
jamesfredley May 29, 2026
cb7ce36
fix(config): track WriteFilteringMap mutations under Groovy 6 by excl…
jamesfredley May 29, 2026
798fbaa
fix(gorm): handle specialized trait method signatures
jamesfredley Jul 4, 2026
e69d5e8
fix(build): map JLine 4.2.1 modules in SBOM allowlist
jamesfredley Jul 4, 2026
a8ffc6d
fix(hibernate): parenthesize embedded association check
jamesfredley Jul 4, 2026
436d496
fix(validation): handle interface entries in static property hierarchy
jamesfredley Jul 4, 2026
6eda49c
fix(build): preserve Mongo Spock opt-out
jamesfredley Jul 4, 2026
d1d9488
fix(config): reject malformed nested map settings
jamesfredley Jul 5, 2026
b81775f
fix(config): reject scalar nested map settings
jamesfredley Jul 5, 2026
c9be2fe
fix(ci): map Groovy 6 joint validation to master
jamesfredley Jul 5, 2026
98e0fbf
fix(examples): remove obsolete Micronaut BOM overrides
jamesfredley Jul 5, 2026
da13502
fix(web): restore web rollback candidates
jamesfredley Jul 5, 2026
d9afc27
fix(deps): align Jackson BOM with Groovy 6
jamesfredley Jul 6, 2026
75c00c0
fix(deps): constrain Jackson 2 modules directly
jamesfredley Jul 6, 2026
610fad8
fix(ci): gate Micronaut island on Groovy 5
jamesfredley Jul 6, 2026
e71f870
fix(ci): resolve Groovy 6 canary failures
jamesfredley Jul 6, 2026
dded60f
fix(ci): stabilize running process stop test
jamesfredley Jul 6, 2026
4705908
Merge branch '8.0.x' into grails8-groovy6-canary
jamesfredley Jul 6, 2026
f81b63e
Merge branch '8.0.x' into grails8-groovy6-canary
Jul 17, 2026
afc7e55
fix: Groovy 6 compatibility for the canary CI
Jul 17, 2026
aba77da
Merge remote-tracking branch 'origin/8.0.x' into pr-15558
jamesfredley Jul 21, 2026
3edcb82
Handle JLine 4 licenses in SBOM generation
jamesfredley Jul 22, 2026
59f024c
Register core plugin beans without dynamic property setters
jamesfredley Jul 22, 2026
0142661
Call PluginUtils helpers explicitly in Groovy 6 tests
jamesfredley Jul 22, 2026
ff524bd
Clear matched request attributes after URL mapping requests
jamesfredley Jul 22, 2026
c3732ff
Normalize rendered mail line endings in integration tests
jamesfredley Jul 22, 2026
07db05a
Merge remote-tracking branch 'origin/8.0.x' into grails8-groovy6-canary
jamesfredley Jul 22, 2026
6ab878d
Reject unknown nested configuration map keys
jamesfredley Jul 22, 2026
165ab45
Bump managed Jackson 2 line to 2.22.1
jamesfredley Jul 22, 2026
f8d8601
Upgrade Gradle wrapper to 9.6.1
jamesfredley Jul 22, 2026
42d150a
Align Spring Dependency Management example with Groovy 6
jamesfredley Jul 23, 2026
dd0f4a8
Merge branch '9.0.x' into grails8-groovy6-canary
jamesfredley Aug 2, 2026
f710c9f
fix: add groovy-callsite so non-indy compilation works on Groovy 6
Aug 2, 2026
fd712b1
Merge branch '9.0.x' into grails8-groovy6-canary
jamesfredley Aug 3, 2026
20e8ec7
fix: move GrailsApplicationLifeCycle default method to Java for G6 no…
jamesfredley Aug 3, 2026
2d0b73d
Merge branch '9.0.x' into grails8-groovy6-canary
jamesfredley Aug 15, 2026
8ad44f8
Merge branch '9.0.x' into grails8-groovy6-canary
jamesfredley Aug 15, 2026
44f7341
build(groovy): move the canary to Groovy 6.0.0-beta-2 and retire obso…
jamesfredley Aug 16, 2026
f5e7a91
ci: cap functional jobs at 70 minutes and disable the Gradle daemon
jamesfredley Aug 18, 2026
699f8c6
ci: give the slower Groovy 6 functional shard 90 minutes
jamesfredley Aug 18, 2026
403f43d
ci: finish functional shard 1 after Gradle worker teardown hangs
jamesfredley Aug 18, 2026
f6c942b
ci: write the functional hang sentinel when the task graph empties
jamesfredley Aug 18, 2026
2ef2a57
ci: accept the known functional hang after a green end-of-suite
jamesfredley Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/ci-check-functional-reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""Fail unless parsed JUnit XML shows a positive green suite count."""

from __future__ import annotations

import os
import sys
import xml.etree.ElementTree as ET


def main() -> int:
suites = 0
tests = 0
failures = 0
errors = 0
for root, _dirs, files in os.walk('.'):
norm = root.replace('\\', '/')
if '/build/test-results' not in norm:
continue
for name in files:
if not name.endswith('.xml'):
continue
path = os.path.join(root, name)
try:
tree = ET.parse(path)
except ET.ParseError as exc:
print(f'unparsable {path}: {exc}')
return 1
node = tree.getroot()
candidates = [node] if node.tag.endswith('testsuite') else list(node)
for suite in candidates:
if not suite.tag.endswith('testsuite'):
continue
if not all(key in suite.attrib for key in ('tests', 'failures', 'errors')):
print(f'missing attrs {path}')
return 1
suites += 1
tests += int(suite.attrib['tests'])
failures += int(suite.attrib['failures'])
errors += int(suite.attrib['errors'])
print(f'suites={suites} tests={tests} failures={failures} errors={errors}')
if suites > 0 and tests > 0 and failures == 0 and errors == 0:
return 0
return 1


if __name__ == '__main__':
sys.exit(main())
73 changes: 73 additions & 0 deletions .github/ci-exit-after-build.init.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import java.nio.file.Files
import java.nio.file.StandardCopyOption

import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.api.tasks.TaskState

// Command-line init scripts also apply to included builds. Only the
// invocation root should write the functional-suite sentinel.
if (gradle.parent != null) {
return
}

File repoDir = gradle.startParameter.currentDir
File dir = new File(repoDir, 'build')
File tmp = new File(dir, 'ci-build-finished.txt.tmp')
File marker = new File(dir, 'ci-build-finished.txt')
tmp.delete()
marker.delete()

void writeMarker(File dir, File tmp, File marker, String status) {
dir.mkdirs()
tmp.text = status
Files.move(tmp.toPath(), marker.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
}

// The hang is after the last task completes and before buildFinished
// (workers never release). Write the sentinel when the task graph empties.
gradle.taskGraph.whenReady { TaskExecutionGraph graph ->
Object lock = new Object()
Set remaining = new HashSet(graph.allTasks)
boolean failed = false
graph.afterTask { task, TaskState state ->
synchronized (lock) {
if (state.failure != null) {
failed = true
}
remaining.remove(task)
if (remaining.isEmpty()) {
String status = failed ? 'FAILURE' : 'SUCCESS'
writeMarker(dir, tmp, marker, status)
logger.lifecycle("CI_BUILD_FINISHED ${status}")
}
}
}
}

gradle.buildFinished { result ->
if (marker.exists()) {
return
}
String status = result.failure == null ? 'SUCCESS' : 'FAILURE'
writeMarker(dir, tmp, marker, status)
logger.lifecycle("CI_BUILD_FINISHED ${status}")
}
71 changes: 57 additions & 14 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,11 @@ jobs:
functional:
name: "Functional Tests (${{ matrix.job_name }})"
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
# 8.0.x history: shard 1 is 22-34 min, indy=true shard 0 is 48-55.
# On Groovy 6, shard 1 tests finish around 31 min then Gradle hangs in
# worker teardown. The init script exits after buildFinished so that hang
# cannot occupy a runner. This job cap is a last-resort backstop.
timeout-minutes: 80
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -454,20 +459,58 @@ jobs:
- name: "🏃 Run Functional Tests"
# The Micronaut island is auto-pruned on a sub-25 JDK (settings.gradle), so the
# Java 21 entries skip it and the Java 25 entries include it (see comment on `build`).
run: >
./gradlew ${{ matrix.gradle_task }}
--continue
--rerun-tasks
--stacktrace
-PgrailsIndy=${{ matrix.indy }}
-PonlyFunctionalTests
-PskipCodeStyle
-PskipHibernate5Tests
-PskipHibernate7Tests
-PskipMongodbTests
-PskipSpringSecurityTests
-PskipRedisTests
${{ matrix.shard_arguments }}
# The known hanging cell finishes tests, prints the wrapper:test /
# aggregate-report tail, then hangs before buildFinished. Timeout plus
# that end-of-walk log and green XML is treated as success.
run: |
hanging=0
if [ "${{ matrix.java }}" = "21" ] && [ "${{ matrix.indy }}" = "true" ] && [ "${{ matrix.gradle_task }}" = "testShard" ]; then
hanging=1
limit=45m
else
limit=70m
fi
rm -f build/ci-build-finished.txt build/ci-build-finished.txt.tmp
set +e
timeout --kill-after=30s "$limit" ./gradlew ${{ matrix.gradle_task }} \
--init-script .github/ci-exit-after-build.init.gradle \
--no-daemon \
--continue \
--rerun-tasks \
--stacktrace \
-PgrailsIndy=${{ matrix.indy }} \
-PonlyFunctionalTests \
-PskipCodeStyle \
-PskipHibernate5Tests \
-PskipHibernate7Tests \
-PskipMongodbTests \
-PskipSpringSecurityTests \
-PskipRedisTests \
${{ matrix.shard_arguments }} \
2>&1 | tee gradle-functional.log
code=${PIPESTATUS[0]}
set -e
if [ "$code" -eq 0 ]; then
exit 0
fi
marker="build/ci-build-finished.txt"
if [ "$hanging" -eq 1 ] && { [ "$code" -eq 124 ] || [ "$code" -eq 137 ]; }; then
if [ -f "$marker" ] && [ "$(cat "$marker")" = "SUCCESS" ]; then
echo "Known hanging cell: SUCCESS sentinel after teardown hang."
exit 0
fi
if grep -qE '> Task .+ FAILED' gradle-functional.log; then
echo "A Gradle task FAILED; not recovering from teardown hang."
exit 1
fi
if grep -q '> Task :grails-wrapper:test' gradle-functional.log \
&& grep -q 'Markdown aggregate test report:' gradle-functional.log \
&& python3 .github/ci-check-functional-reports.py; then
echo "Known hanging cell: suite walked to grails-wrapper:test with green XML; teardown hang treated as success."
exit 0
fi
fi
exit "$code"
- name: "🗄️ Save dependency jar cache"
if: ${{ success() && matrix.cache_writer && steps.dependency-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/groovy-joint-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,21 @@ jobs:
run: |
# Extract the major version of `groovy.version` declared in this branch's
# dependencies.gradle (e.g. '5.0.5' -> '5', '4.0.31' -> '4') and use it to
# pick the matching Apache Groovy development branch (GROOVY_<major>_0_X).
# pick the matching Apache Groovy development branch (GROOVY_<major>_0_X),
# or master for the current Groovy development line before a maintenance
# branch exists.
# This keeps `7.0.x` PRs validating against Groovy 4 and `8.0.x` PRs
# validating against Groovy 5 without hard-coding the branch here.
GROOVY_MAJOR=$(grep -m 1 "'groovy\.version'" dependencies.gradle | sed -E "s/.*'([0-9]+)\.[0-9]+\.[0-9]+.*/\1/" | tr -d '[:space:]')
if [ -z "$GROOVY_MAJOR" ]; then
echo "::error::Could not determine Apache Groovy major version from dependencies.gradle"
exit 1
fi
GROOVY_BRANCH="GROOVY_${GROOVY_MAJOR}_0_X"
if [ "$GROOVY_MAJOR" = "6" ]; then
GROOVY_BRANCH="master"
else
GROOVY_BRANCH="GROOVY_${GROOVY_MAJOR}_0_X"
fi
echo "Validating against Apache Groovy branch: $GROOVY_BRANCH"
echo "value=$GROOVY_BRANCH" >> $GITHUB_OUTPUT
rm dependencies.gradle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.GroovyCompile
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.api.tasks.testing.Test
import org.gradle.external.javadoc.StandardJavadocDocletOptions

import static org.apache.grails.buildsrc.GradleUtils.lookupPropertyByType
Expand Down Expand Up @@ -112,7 +113,7 @@ class CompilePlugin implements Plugin<Project> {
it.options.fork = true
// always set an isolated build to ensure grails.factories aren't accidentally merged since every project
// in this mono repo should be an isolated projected
it.options.forkOptions.jvmArgs = ['-Xms128M', '-Xmx2G', '-Dgrails.isolated.build=true']
it.options.forkOptions.jvmArgs = ['-Xms128M', '-Xmx2G', '-Dgrails.isolated.build=true', '-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
// Publish THIS project's base.dir to the forked Groovy compiler. Gradle reuses a forked
// compiler daemon for a task whose requested fork arguments the daemon already satisfies,
// so a compile that does NOT request base.dir can be handed a daemon started for another
Expand All @@ -133,6 +134,9 @@ class CompilePlugin implements Plugin<Project> {
it.groovyOptions.configurationScript =
GradleUtils.findRootGrailsCoreDir(project).file('gradle/groovy-compile-configscript.groovy').asFile
}
project.tasks.withType(Test).configureEach {
it.jvmArgs('-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true')
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class SbomPlugin implements Plugin<Project> {
'pkg:maven/com.oracle.coherence.ce/coherence-bom@25.03.1?type=pom': 'UPL-1.0', // does not have map based on license id
'pkg:maven/com.oracle.coherence.ce/coherence-bom@25.03.2?type=pom': 'UPL-1.0', // does not have map based on license id
'pkg:maven/com.oracle.coherence.ce/coherence-bom@22.06.2?type=pom': 'UPL-1.0', // does not have map based on license id
'pkg:maven/jline/jline@2.14.6?type=jar' : 'BSD-2-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
'pkg:maven/jline/jline@2.14.6?type=jar' : 'BSD-2-Clause', // legacy jline:jline group, BSD-2; maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
'pkg:maven/opensymphony/sitemesh@2.6.0?type=jar' : 'OpenSymphony', // custom license approved by legal LEGAL-707
'pkg:maven/org.antlr/antlr4-runtime@4.7.2?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
// mongo-java-server declares only "The BSD License" so it maps to BSD-4-Clause for the same
Expand All @@ -115,7 +115,8 @@ class SbomPlugin implements Plugin<Project> {
'pkg:maven/de.bwaldvogel/mongo-java-server-memory-backend@1.47.0?type=jar': 'BSD-3-Clause',
// The whole org.jline group declares "The BSD License", which maps incorrectly because of
// https://github.com/CycloneDX/cyclonedx-core-java/issues/205 - the POMs point at BSD-3-Clause.
// jline.version tracks the JLine version Groovy ships, so every module resolves to one version.
// jline.version tracks the JLine version Groovy ships for the gradle BOM (3.x); Groovy 6
// may also pull JLine 4, handled by forcedLicenseFor below.
'pkg:maven/org.jline/jansi@3.30.9?type=jar' : 'BSD-3-Clause',
'pkg:maven/org.jline/jline@3.30.9?type=jar' : 'BSD-3-Clause',
'pkg:maven/org.jline/jline-builtins@3.30.9?type=jar' : 'BSD-3-Clause',
Expand All @@ -136,6 +137,18 @@ class SbomPlugin implements Plugin<Project> {
'pkg:maven/org.bouncycastle/bcutil-jdk18on@1.84?type=jar' : 'MIT',
]

private static String forcedLicenseFor(String bomRef) {
if (LICENSE_MAPPING.containsKey(bomRef)) {
return LICENSE_MAPPING[bomRef]
}
// JLine 4 LICENSE.txt confirms BSD-3-Clause, but CycloneDX misreports it as BSD-4-Clause
// (cyclonedx-core-java#205). This is limited to the org.jline Maven jar family resolved via Groovy 6.
if (bomRef.matches('pkg:maven/org\\.jline/[^/@?]+@4\\.[^?]+\\?type=jar')) {
return 'BSD-3-Clause'
}
null
}

// we don't distribute these so these licenses are considered acceptable, but we still prefer ASF licenses.
// Require a whitelist of any case of category X licenses to prevent accidental inclusion in a distributed artifact
// this list will need to be updated anytime we change versions so we can revise the licenses
Expand Down Expand Up @@ -471,10 +484,10 @@ class SbomPlugin implements Plugin<Project> {
}

logger.info('Picking license for {} from {} choices', bomRef, licenseChoices.size())
if (LICENSE_MAPPING.containsKey(bomRef)) {
def licenseId = forcedLicenseFor(bomRef)
if (licenseId) {
// There are several reasons that cyclone will get the license wrong, usually due to upstream not publishing information or publishing it incorrectly
// see the licenseMapping map above for details
def licenseId = LICENSE_MAPPING[bomRef]
logger.info('Forcing license for {} to {}', bomRef, licenseId)

def licenseBlock = LICENSES[licenseId]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class SbomPluginSpec extends Specification {
[[license: [id: 'LGPL-2.1-only']]]
}

private static List bsd4Choice() {
[[license: [id: 'BSD-4-Clause']]]
}

static class FakeCliArtifactExtension {
final Property<String> artifactId
FakeCliArtifactExtension(Property<String> artifactId) { this.artifactId = artifactId }
Expand Down Expand Up @@ -97,4 +101,25 @@ class SbomPluginSpec extends Specification {
e.message.contains('grails-data-hibernate5-dbmigration')
e.message.contains('LGPL-2.1-only')
}

void "JLine 4 Maven jars use the BSD-3-Clause correction for new versions"() {
expect:
SbomPlugin.pickLicense(LOGGER, 'grails-console', 'grails-console',
'pkg:maven/org.jline/jansi@4.3.1?type=jar', bsd4Choice()).id == 'BSD-3-Clause'
}

void "JLine 4 license correction is limited to JLine 4 Maven jars"() {
when:
SbomPlugin.pickLicense(LOGGER, 'grails-console', 'grails-console', bomRef, bsd4Choice())

then:
GradleException e = thrown(GradleException)
e.message.contains('BSD-4-Clause')

where:
bomRef << [
'pkg:maven/com.example/jansi@4.3.1?type=jar',
'pkg:maven/org.jline/jansi@3.3.1?type=jar'
]
}
}
Loading
Loading