Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d856105
docs: add Apache LICENSE and NOTICE files
claude Jun 4, 2026
c476d41
docs(spec): add SBOM generation design
adityamparikh Jun 5, 2026
ac0ca0a
docs(plan): add SBOM generation implementation plan
adityamparikh Jun 5, 2026
8e4c3c2
chore(deps): add CycloneDX Gradle plugin 1.10.0 to version catalog
adityamparikh Jun 5, 2026
1cf5f0f
feat(build): generate and embed CycloneDX SBOM
adityamparikh Jun 5, 2026
6a0d39e
feat(actuator): enable /actuator/sbom endpoint explicitly
adityamparikh Jun 5, 2026
0c0a165
docs(spec): drop integration-test scope, document plugin-version deci…
adityamparikh Jun 5, 2026
52347bc
docs(spec): drop stale 1.10.0 version reference
adityamparikh Jun 5, 2026
2cedf64
docs(spec): inline the plugin-version constraints explanation
adityamparikh Jun 5, 2026
74992d6
ci: upload CycloneDX SBOM as workflow artifact
adityamparikh Jun 5, 2026
c3b4aa8
ci(release): strict SBOM generation + upload + release attachment
adityamparikh Jun 5, 2026
9d3d70b
docs(readme): document SBOM location, retrieval, and scanning
adityamparikh Jun 5, 2026
e179ac3
refactor(build): drop unnecessary cyclonedxBom configuration
adityamparikh Jun 5, 2026
452799d
docs(agents): note SBOM generation in commands + architecture
adityamparikh Jun 5, 2026
21df7ac
style: apply spotless
adityamparikh Jun 5, 2026
8e6f187
feat(build): derive binary-release LICENSE/NOTICE from the SBOM
adityamparikh Jun 11, 2026
685ff4c
refactor(build): extract LICENSE/NOTICE generation to a buildSrc plugin
adityamparikh Jun 11, 2026
82bacc2
test(build): unit-test the LICENSE/NOTICE buildSrc tasks
adityamparikh Jun 11, 2026
da3c5d8
docs(build): explain the LICENSE/NOTICE tasks in comments and AGENTS.md
adityamparikh Jun 11, 2026
748a70b
refactor(build): drop license-policy.json; disclose SBOM licenses ver…
adityamparikh Jun 11, 2026
3ceb2a7
docs(build): point the LICENSE appendix to the bundled SBOM
adityamparikh Jun 11, 2026
d2711c9
docs: document where/when the binary LICENSE & NOTICE are available
adityamparikh Jun 11, 2026
033c03e
docs(build): explain the buildSrc LICENSE/NOTICE plugin for non-Gradl…
adityamparikh Jun 11, 2026
230b706
docs(build): comment the convention plugin body for non-Gradle readers
adityamparikh Jun 11, 2026
fd7eedf
Merge remote-tracking branch 'upstream/main' into pr/138
epugh Jun 13, 2026
1721a0f
Dont double load the root files
epugh Jun 13, 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
49 changes: 49 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,55 @@ buildpacks (`bootBuildImage -Pnative`). Key configuration:
- **CI:** Separate `native.yml` workflow; native failures do not block JVM-path merges.
- **Spec:** [docs/specs/graalvm-native-image.md](docs/specs/graalvm-native-image.md)

## Release LICENSE / NOTICE

ASF policy requires distinct LICENSE/NOTICE for the *source* form and the *binary*
form, because the binary (Spring Boot fat `bootJar`) bundles third-party bytecode.
See [infra.apache.org/licensing-howto](https://infra.apache.org/licensing-howto.html).

- **Source form** (thin `jar`, `-sources`, `-javadoc`): the base `LICENSE` (Apache-2.0)
and `NOTICE` at the repo root, bundled into `META-INF/` as-is.
- **Binary form** (`bootJar`): generated at build time and bundled into its `META-INF/`:
- `generateBinaryLicense` → `LICENSE` = base Apache-2.0 + an appendix listing every
bundled `productionRuntimeClasspath` dependency and a link to its license. Licenses
are read from the **CycloneDX SBOM** (`cyclonedxBom`, the same SBOM embedded at
`META-INF/sbom/application.cdx.json`), filtered to the shipped classpath. The SBOM
resolves a license for every bundled component — including Gradle-module-metadata
-only ASF artifacts such as `solr-solrj`/`solr-api` that POM-only scanners miss — so
no per-dependency list is hand-maintained.
- `generateBinaryNotice` → `NOTICE` = base NOTICE + the `META-INF/NOTICE` files lifted
verbatim (de-duplicated) from the bundled jars (Maven-Shade
`ApacheNoticeResourceTransformer` approach).
- **Where / when they appear:** both binary files are regenerated on every build — the
two tasks run ahead of `bootJar` (and in `check`), so any `./gradlew build` / `bootJar`
produces them. They live at `META-INF/LICENSE` and `META-INF/NOTICE` inside the fat jar
(`build/libs/solr-mcp-<v>.jar`), and therefore inside every published **Docker image**
too, since the Jib JVM image and the Paketo native images both package the bootJar
contents. Inspect a built artifact with
`unzip -p build/libs/solr-mcp-<v>.jar META-INF/LICENSE` (or `META-INF/NOTICE`); the
generator also writes them to `build/generated/license/` for local viewing. The
source-form jars (thin `jar`, `-sources`, `-javadoc`) instead carry the repo-root base
files unchanged.
- **Licenses are disclosed as the SBOM reports them** (SPDX ids where available). The
appendix is a disclosure, not a license policy: there is **no allow-list and no
corrections**, so a few imprecise-but-permissive upstream labels appear as-is (e.g.
`mcp-server-security` shows `Apache-1.0`, ANTLR shows `BSD-4-Clause`/`BSD licence`); the
appendix preamble says so and links each license. All bundled deps are ASF Category A/B.
- **Completeness gate** (`generateBinaryLicense`, run as part of `check`/`build`): the
*only* gate — fails if a bundled dependency is missing from the SBOM, so a dependency
can never be silently omitted from the LICENSE. It makes no judgement about which
licenses are acceptable. (Unlike apache/solr's `solr/licenses/` folder, which JanHoy
said not to replicate, there is no per-dependency license/checksum store here.)
- This builds on the SBOM generation (see **SBOM Architecture**); the SBOM remains the
machine-readable bill of materials, and LICENSE/NOTICE are the human-readable legal
artifacts derived from it.
- **Implementation:** the `org.apache.solr.mcp.license-notice` convention plugin in
`buildSrc/` (typed `GenerateBinaryLicense` / `GenerateBinaryNotice` tasks). The root
`build.gradle.kts` only applies the plugin. The tasks are unit-tested in
`buildSrc/src/test/kotlin/.../LicenseNoticeTasksTest.kt` (appendix listing, SBOM
name/URL handling, the completeness gate, and NOTICE de-duplication); `buildSrc`'s
`test` runs as part of `./gradlew build`.

## Testing Structure

- **Unit tests** (`*Test.java`): Mocked dependencies, fast execution. Mockito-based
Expand Down
17 changes: 9 additions & 8 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ plugins {
alias(libs.plugins.jib)
alias(libs.plugins.graalvm.native) apply false
alias(libs.plugins.cyclonedx)
// Generates ASF source/binary LICENSE + NOTICE (buildSrc convention plugin).
// Listed after spring-boot + cyclonedx so productionRuntimeClasspath and
// cyclonedxBom exist when it wires its tasks. See buildSrc/.
id("org.apache.solr.mcp.license-notice")
}

// GraalVM Native Image (Opt-In)
Expand Down Expand Up @@ -77,15 +81,12 @@ java {
}

// ASF release policy requires every distributed artifact to carry the project's
// LICENSE and NOTICE files. Bundle them into META-INF of every JAR produced by
// this build (main jar, bootJar, sources, javadoc).
// LICENSE and NOTICE files. This is handled by the `org.apache.solr.mcp.license-notice`
// convention plugin (buildSrc/): the source-form jars (thin jar, -sources, -javadoc)
// get the base Apache-2.0 LICENSE/NOTICE, while the binary fat bootJar gets generated
// files with an SBOM-derived third-party appendix. The plugin must own this wiring for
// the bootJar — bundling the base files here too would duplicate META-INF/LICENSE.
// See https://www.apache.org/legal/release-policy.html#licensing-documentation
tasks.withType<Jar>().configureEach {
metaInf {
from(rootProject.file("LICENSE"))
from(rootProject.file("NOTICE"))
}
}

// Maven Publishing Configuration
// ==============================
Expand Down
90 changes: 90 additions & 0 deletions buildSrc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<!--
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

http://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.
-->

# buildSrc — generating the binary LICENSE & NOTICE

This directory holds the build logic that assembles the **binary-release `LICENSE`
and `NOTICE`** files (the ones bundled inside the executable JAR). It is written in
Kotlin. If you don't work with Gradle day-to-day, this README explains what each piece
is and how they fit together; the end-user view of *what* these files contain lives on
the [Licensing & Notices](https://solr.apache.org/mcp/licensing.html) docs page.

## What is `buildSrc`?

`buildSrc` is a Gradle convention: **any code you put under `buildSrc/` is compiled
automatically before the main build and made available to `build.gradle.kts`.** You
don't declare a dependency on it or publish it anywhere — Gradle just picks it up. It is
the standard place to keep custom build logic so the root `build.gradle.kts` stays
small. (Think of it as a tiny library that only this project's build uses.)

## What's in here

| File | Role |
|------|------|
| `src/main/kotlin/.../GenerateBinaryLicense.kt` | A custom Gradle **task** that writes the binary `LICENSE` (Apache-2.0 text + a generated third-party dependency appendix). |
| `src/main/kotlin/.../GenerateBinaryNotice.kt` | A custom Gradle **task** that writes the binary `NOTICE` (our `NOTICE` + the `NOTICE` files of bundled dependencies). |
| `src/main/kotlin/org.apache.solr.mcp.license-notice.gradle.kts` | A **convention plugin** that creates the two tasks above and wires them into the build. |
| `src/test/kotlin/.../LicenseNoticeTasksTest.kt` | Unit tests for the two tasks. |
| `build.gradle.kts` | Builds `buildSrc` itself (enables Kotlin + the test dependencies). |

## Gradle concepts, for Java developers

A handful of Gradle terms show up in the code. Here is the minimum to read it:

- **Task** — a single unit of build work with declared *inputs* and *outputs*, a bit
like one rule in a `Makefile`. Gradle decides whether a task needs to run by comparing
its inputs/outputs to the last run. We write a task by subclassing `DefaultTask`.
- **`@TaskAction`** — the method Gradle calls to actually do the work when the task runs.
It's effectively the task's "main".
- **Input / output annotations** (`@InputFile`, `@InputFiles`, `@Input`, `@OutputFile`) —
these declare what a task reads and writes. They are not decoration: Gradle uses them
to (1) **skip** the task when nothing changed (incremental builds), and (2) **order**
tasks so a producer runs before whoever consumes its output. `@InputFile`/`@InputFiles`
are file inputs; `@Input` is a plain value (a string, list, map); `@OutputFile` is a
produced file.
- **`Property` / `Provider` types** (`RegularFileProperty`, `ListProperty`,
`MapProperty`, `ConfigurableFileCollection`) — Gradle's "lazy" typed holders for a
value. The convention plugin `.set(...)`s them while the build is being *configured*;
the task `.get()`s them later when it actually *runs*. This lazy split is why the task
declares `abstract val foo: …Property` instead of a plain field.
- **Convention plugin** — a `.gradle.kts` file under `buildSrc` that Gradle compiles into
a plugin you can apply by id. Applying it (one line in the root build) registers our
tasks and connects them to the rest of the build, so the conventions live here instead
of being copy-pasted into `build.gradle.kts`.
- **`productionRuntimeClasspath`** — the set of dependency jars that actually end up
inside the Spring Boot fat jar. It excludes test-only, compile-only, and
`developmentOnly` dependencies. "What ships" is exactly what the binary LICENSE/NOTICE
must describe, which is why both tasks are driven by it.

## How it runs

1. The root `build.gradle.kts` applies the plugin: `id("org.apache.solr.mcp.license-notice")`.
2. The plugin registers `generateBinaryLicense` and `generateBinaryNotice`, and makes the
`bootJar` task depend on them (and the `check` task depend on `generateBinaryLicense`).
3. On a build, the CycloneDX `cyclonedxBom` task produces the SBOM, then:
- `generateBinaryLicense` reads the SBOM + the list of shipped dependencies and writes
`build/generated/license/LICENSE`. It **fails the build** if a shipped dependency is
missing from the SBOM (so nothing can ship unlisted).
- `generateBinaryNotice` scans the shipped jars for their `META-INF/NOTICE` files and
writes `build/generated/license/NOTICE`.
4. `bootJar` copies those two files into the JAR's `META-INF/`. The source-form jars
(thin `jar`, `-sources`, `-javadoc`) instead carry the plain repo-root `LICENSE` /
`NOTICE`.

See the `## Release LICENSE / NOTICE` section in the repository's `AGENTS.md` for the
policy rationale, and the [Licensing & Notices](https://solr.apache.org/mcp/licensing.html)
docs page for the consumer-facing explanation.
38 changes: 38 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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
*
* http://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.
*/

// This file builds buildSrc itself (not the main project). See buildSrc/README.md.
plugins {
// Lets us write Gradle build logic — tasks and the convention plugin — in Kotlin,
// and turns the `*.gradle.kts` files under src/main/kotlin into apply-by-id plugins.
`kotlin-dsl`
}

repositories {
mavenCentral()
}

dependencies {
// Only used by the task unit tests under src/test (the main code needs no extra deps;
// the Gradle API is provided by the kotlin-dsl plugin).
testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
138 changes: 138 additions & 0 deletions buildSrc/src/main/kotlin/org.apache.solr.mcp.license-notice.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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
*
* http://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.
*/

// Convention plugin: ASF-compliant LICENSE / NOTICE for the source and binary forms.
//
// For readers new to Gradle: this `.gradle.kts` file under buildSrc is a "precompiled
// script plugin". Gradle compiles it into a plugin whose id is the file name
// (`org.apache.solr.mcp.license-notice`); the root build applies it with one line,
// `id("org.apache.solr.mcp.license-notice")`. The body below runs at *configuration*
// time: it creates the two generator tasks (defined in this same buildSrc as
// GenerateBinaryLicense / GenerateBinaryNotice), wires their inputs, and connects their
// outputs to the `bootJar` and `check` tasks. See buildSrc/README.md for the primer.
//
// ASF policy requires distinct LICENSE/NOTICE for the source form and the binary form,
// because the binary (the Spring Boot fat `bootJar`) bundles third-party bytecode. See
// https://infra.apache.org/licensing-howto.html. This plugin:
//
// - bundles the base Apache-2.0 LICENSE + NOTICE into the source-form jars as-is;
// - generates, for the bootJar, a LICENSE with a third-party appendix derived from the
// CycloneDX SBOM and a NOTICE that lifts bundled dependencies' notices, with a
// completeness gate that fails the build if a bundled dependency is missing from the
// SBOM. Licenses are disclosed as the SBOM reports them; there is no license policy.
//
// Apply this AFTER the Spring Boot and CycloneDX plugins so `productionRuntimeClasspath`
// and the `cyclonedxBom` task exist.

import org.apache.solr.mcp.build.GenerateBinaryLicense
import org.apache.solr.mcp.build.GenerateBinaryNotice
import org.gradle.api.artifacts.component.ModuleComponentIdentifier

// The project's source-form LICENSE/NOTICE at the repo root (the plain Apache-2.0 text
// and the base NOTICE). They are bundled as-is into the non-fat jars, and are also the
// base that the generated binary files are built on top of.
val licenseFile = layout.projectDirectory.file("LICENSE")
val noticeFile = layout.projectDirectory.file("NOTICE")

// A Gradle "configuration" is a named set of dependencies. `productionRuntimeClasspath`
// is the one that actually ends up inside the fat jar — it excludes test/compile-only and
// developmentOnly deps. So this is exactly "what ships", which is what the binary
// LICENSE/NOTICE must describe.
val shippedClasspath = configurations.named("productionRuntimeClasspath")

// Resolve that configuration to its actual artifacts — each is a jar file plus the module
// identity it came from. `flatMap` keeps everything lazy: nothing is resolved here while
// the build is being configured; it is computed later, when a task that needs it runs.
// The result is a Provider<Set<ResolvedArtifactResult>>.
val shippedArtifacts = shippedClasspath.flatMap { it.incoming.artifacts.resolvedArtifacts }

// Derive the shipped dependencies as sorted, de-duplicated "group:name:version" strings.
// `mapNotNull { it... as? ModuleComponentIdentifier }` keeps only normal external modules
// and drops anything that isn't one (e.g. file dependencies). This feeds the LICENSE
// task's `bundledCoordinates` input.
val shippedCoordinates =
shippedArtifacts.map { set ->
set.mapNotNull { it.id.componentIdentifier as? ModuleComponentIdentifier }
.map { "${it.group}:${it.module}:${it.version}" }
.distinct()
.sorted()
}

// Map each shipped jar's *file name* to its "group:name:version". The NOTICE task opens
// the jar files and uses this map to label each lifted notice with the module it came
// from (at that point the file is all it has to go on).
val jarNameToCoordinate =
shippedArtifacts.map { set ->
set.mapNotNull { artifact ->
(artifact.id.componentIdentifier as? ModuleComponentIdentifier)?.let { id ->
artifact.file.name to "${id.group}:${id.module}:${id.version}"
}
}.toMap()
}

// Create (register) the LICENSE task and wire its inputs/output. `register` is lazy — the
// task is configured/run only if the build needs it. `dependsOn("cyclonedxBom")` ensures
// the SBOM exists before this runs; each `.set(...)` connects one declared input.
val generateBinaryLicense =
tasks.register<GenerateBinaryLicense>("generateBinaryLicense") {
description = "Assembles the binary-release LICENSE (Apache-2.0 + SBOM-derived appendix)."
group = "documentation"
dependsOn("cyclonedxBom")
baseLicense.set(licenseFile)
sbom.set(layout.buildDirectory.file("reports/application.cdx.json"))
bundledCoordinates.set(shippedCoordinates)
outputFile.set(layout.buildDirectory.file("generated/license/LICENSE"))
}

// Same for the NOTICE task. `jars.from(shippedClasspath)` hands it the shipped jar files
// to scan for their `META-INF/NOTICE` entries.
val generateBinaryNotice =
tasks.register<GenerateBinaryNotice>("generateBinaryNotice") {
description = "Assembles the binary-release NOTICE (project NOTICE + bundled dependency notices)."
group = "documentation"
jars.from(shippedClasspath)
coordinateByJarName.set(jarNameToCoordinate)
baseNotice.set(noticeFile)
outputFile.set(layout.buildDirectory.file("generated/license/NOTICE"))
}

// `metaInf { from(file) }` adds files to a jar's `META-INF/` directory. The source-form
// artifacts — the thin `jar`, `-sources`, `-javadoc` (everything except `bootJar`) — get
// the base LICENSE/NOTICE unchanged. `configureEach` applies this to each matching jar
// task lazily.
tasks.withType<Jar>().matching { it.name != "bootJar" }.configureEach {
metaInf {
from(licenseFile)
from(noticeFile)
}
}

// The binary artifact (the Spring Boot fat `bootJar`) instead gets the *generated* files.
// `dependsOn(...)` makes the generators run first; `from(task.flatMap { it.outputFile })`
// bundles each task's output into `META-INF/` (the lazy flatMap also wires the task
// dependency automatically).
tasks.named<Jar>("bootJar") {
dependsOn(generateBinaryLicense, generateBinaryNotice)
metaInf {
from(generateBinaryLicense.flatMap { it.outputFile })
from(generateBinaryNotice.flatMap { it.outputFile })
}
}

// Run the LICENSE task — and therefore its completeness gate — as part of `check`, so a
// plain `./gradlew build` fails if a bundled dependency is missing from the SBOM.
tasks.named("check") { dependsOn(generateBinaryLicense) }
Loading
Loading