This is an automated email from the ASF dual-hosted git repository.

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-mcp.git


The following commit(s) were added to refs/heads/main by this push:
     new 518bc17  feat(build): RAT license-header enforcement as a buildSrc 
convention plugin (stacked on #138) (#150)
518bc17 is described below

commit 518bc17a8a1f4c1c6a3f1881901d8d8892b6a86c
Author: Aditya Parikh <[email protected]>
AuthorDate: Mon Jun 15 12:19:20 2026 -0400

    feat(build): RAT license-header enforcement as a buildSrc convention plugin 
(stacked on #138) (#150)
    
    * docs: add Apache LICENSE and NOTICE files
    
    Add the top-level Apache License 2.0 text and NOTICE file required by
    ASF release policy, and bundle them into the META-INF directory of every
    JAR produced by the build (main, bootJar, sources, javadoc).
    
    See https://www.apache.org/legal/release-policy.html#licensing-documentation
    
    * docs(spec): add SBOM generation design
    
    Captures decisions made during brainstorming: CycloneDX over SPDX,
    embed-in-bootJar via Spring Boot's native CycloneDX integration, full
    build + Docker + Release coverage, no cosign attestation in this PR.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(plan): add SBOM generation implementation plan
    
    Step-by-step bite-sized tasks covering: version catalog, Gradle plugin
    wiring, actuator endpoint enablement, focused HTTP integration test,
    CI workflow uploads, README + CLAUDE.md docs, final verification.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * chore(deps): add CycloneDX Gradle plugin 1.10.0 to version catalog
    
    Plugin will be applied in the next commit. Adding the catalog entry
    first keeps build.gradle.kts changes reviewable in isolation.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * feat(build): generate and embed CycloneDX SBOM
    
    Apply org.cyclonedx.bom Gradle plugin 2.4.1. Spring Boot 3.5's
    CycloneDxPluginAction auto-wires bootJar to embed the generated SBOM at
    META-INF/sbom/application.cdx.json, so every distribution (JAR, Jib JVM
    image, both Paketo native images) ships the embedded SBOM via bootJar
    packaging — no per-image wiring.
    
    Plugin version note: 1.10.0 breaks against Gradle 9.4 with
    UnsupportedOperationException (ImmutableCollection.removeAll). 2.4.1 is
    the latest v1.x-compatible class layout (CycloneDxPlugin /
    CycloneDxTask) that Spring Boot's auto-integration recognizes; v3.x
    renamed the classes (CyclonedxPlugin) and is incompatible until Spring
    Boot adopts the new shape.
    
    projectType is set explicitly to Component.Type.APPLICATION because
    v2.4.1 changed the property from Property<String> to
    Property<Component.Type>; Spring Boot's `.convention("application")`
    would store a raw String and break the task at execution time.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * feat(actuator): enable /actuator/sbom endpoint explicitly
    
    `sbom` was already in management.endpoints.web.exposure.include; this
    makes the endpoint enablement explicit so the file conveys intent
    without relying on Spring Boot defaults.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(spec): drop integration-test scope, document plugin-version decisions
    
    - Drop the planned SbomEndpointIntegrationTest: /actuator/sbom is stock
      Spring Boot functionality; our only project-specific addition is two
      property lines. The build itself fails if cyclonedxBom breaks
      (Spring Boot's bootJar auto-depends on it).
    - Update plugin version note to 2.4.1 and explain why both 1.10.0 (Gradle
      9.4 bug) and 3.x (Spring Boot class-name change) are unsuitable.
    - CycloneDX schema 1.6 (plugin default) replaces the originally-noted 1.5.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(spec): drop stale 1.10.0 version reference
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(spec): inline the plugin-version constraints explanation
    
    Earlier edit lost the detail by accident. Restored as part of the Tool
    choice section so the spec stands on its own.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * ci: upload CycloneDX SBOM as workflow artifact
    
    Mirrors the existing JAR/test-results/coverage upload pattern. Retains
    the SBOM for 30 days (vs the standard 7) since supply-chain
    investigations often happen well after a build.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * ci(release): strict SBOM generation + upload + release attachment
    
    The existing Generate SBOM step swallowed errors with `|| echo "..."`,
    masking failures now that the plugin is wired. Removes the fallback,
    uploads the SBOM as a 90-day workflow artifact, and attaches it to the
    v<version> GitHub Release when one exists (graceful fallback otherwise
    since the source release of record lives at dist.apache.org, not GitHub).
    
    RELEASE_VERSION is already validated by validate-release; routing it
    through an env var instead of inline ${{ }} interpolation is
    defence-in-depth against actions-injection.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(readme): document SBOM location, retrieval, and scanning
    
    New 'Supply chain & SBOM' section covers all four distribution
    channels (embedded in JAR/image, /actuator/sbom endpoint, GitHub
    Release asset, CI workflow artifact) and shows trivy/grype usage.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * refactor(build): drop unnecessary cyclonedxBom configuration
    
    Spring Boot 3.5.14's CycloneDxPluginAction already sets outputName,
    outputFormat, projectType, and wires bootJar embedding — matching what
    Spring Initializr generates for the same dependency set. Verified that
    applying the plugin alone produces a valid CycloneDX 1.6 SBOM at
    META-INF/sbom/application.cdx.json inside the bootJar with
    component type=application.
    
    The earlier projectType override + includeConfigs/skipConfigs were
    defensive but unnecessary; let the framework defaults work.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(agents): note SBOM generation in commands + architecture
    
    CLAUDE.md symlinks to AGENTS.md; edit lands on the real file.
    
    Records the cyclonedxBom command and how the SBOM flows through
    bootJar → actuator → Docker images, so future agents have the
    mental model when working on related code.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * style: apply spotless
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * feat(build): derive binary-release LICENSE/NOTICE from the SBOM
    
    The base LICENSE/NOTICE are correct for the source release, but the binary
    release (the Spring Boot fat bootJar) bundles third-party bytecode and so 
per
    https://infra.apache.org/licensing-howto.html must additionally enumerate 
each
    bundled dependency's license and lift bundled ASF dependencies' NOTICE 
snippets.
    
    Stacks on the CycloneDX SBOM work and reuses it as the source of dependency
    license data:
    
    - generateBinaryLicense: base Apache-2.0 + an appendix listing every
      productionRuntimeClasspath dependency with a link to its license, read 
from the
      bundled SBOM (META-INF/sbom/application.cdx.json). The SBOM resolves a 
license
      for every component, including Gradle-module-metadata-only ASF artifacts
      (solr-solrj/solr-api) that POM-only scanners miss, so no per-dependency 
list is
      hand-maintained. It also gates the build: a bundled module missing from 
the SBOM,
      or carrying a license not in config/license-policy.json, fails the build.
    - generateBinaryNotice: base NOTICE + the META-INF/NOTICE files lifted 
verbatim and
      de-duplicated from the bundled jars (the Shade 
ApacheNoticeResourceTransformer
      approach), so ASF dependency notices stay current automatically.
    
    config/license-policy.json holds the allowedLicenses set plus overrides
    (group:name -> SPDX id) correcting the few components CycloneDX mislabels
    (mcp-server-security -> Apache-2.0; ANTLR ST4/antlr-runtime -> 
BSD-3-Clause).
    Source-form jars keep the base LICENSE/NOTICE.
    
    Verified: ./gradlew build green; fat jar META-INF/LICENSE lists 158 deps
    (incl. SolrJ) and META-INF/NOTICE aggregates 21 upstream notices.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * refactor(build): extract LICENSE/NOTICE generation to a buildSrc plugin
    
    Move the inline LICENSE/NOTICE logic out of the root build.gradle.kts into a
    buildSrc convention plugin (org.apache.solr.mcp.license-notice) backed by 
two
    typed tasks:
    
    - GenerateBinaryLicense / GenerateBinaryNotice are proper DefaultTask types 
with
      @InputFile/@InputFiles/@OutputFile, so they're incremental and (being 
real .kt
      files) free of the kts-script-compiler limitations that forced the 
previous
      Pair-based workarounds — the logic now reads as plain Kotlin with data 
classes.
    - The root build.gradle.kts drops ~250 lines and three imports, and just 
applies
      `id("org.apache.solr.mcp.license-notice")`.
    
    Behaviour is unchanged: the bootJar still bundles a LICENSE with the 
SBOM-derived
    158-dependency appendix (incl. SolrJ) and a NOTICE aggregating 21 upstream
    notices; source-form jars keep the base files; `check` still runs the gate.
    The tasks now live in buildSrc, so they can be unit-tested with Gradle 
TestKit.
    
    Verified: ./gradlew build green; fat-jar META-INF/LICENSE and NOTICE 
identical
    to the pre-refactor output.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * test(build): unit-test the LICENSE/NOTICE buildSrc tasks
    
    Add ProjectBuilder-based tests for the two convention-plugin tasks (now 
possible
    since they live in buildSrc as typed tasks). Covers the correctness-critical
    behaviour without needing the full spring-boot + cyclonedx stack:
    
    - generateBinaryLicense: appendix lists bundled deps with SPDX links, 
applies a
      policy override to correct a mislabelled SBOM license, and preserves the 
base
      LICENSE text; the gate fails on a disallowed license and on a bundled 
coordinate
      absent from the SBOM.
    - generateBinaryNotice: aggregates bundled META-INF/NOTICE files verbatim,
      de-duplicates identical notices, attributes each to its module, and emits 
just
      the project NOTICE when no dependency notices exist.
    
    buildSrc's test task runs as part of `./gradlew build`, so these are 
enforced on
    every build.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(build): explain the LICENSE/NOTICE tasks in comments and AGENTS.md
    
    Add step-by-step comments to GenerateBinaryLicense/GenerateBinaryNotice 
walking
    through what each phase does (load policy, index the SBOM, resolve+gate each
    shipped dependency, write the file; and notice matching/de-dup/attribution).
    
    Expand the AGENTS.md "Release LICENSE / NOTICE" section with where the 
tasks are
    unit-tested and a short runbook for what to do when the license gate fails
    (add an override for an SBOM mislabel, or allow a genuinely new license) 
instead
    of silencing it.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * refactor(build): drop license-policy.json; disclose SBOM licenses verbatim
    
    apache/solr has no license allow-list (it uses a per-dependency licenses/ 
folder,
    which JanHoy said not to replicate), and the binary LICENSE is a 
disclosure, not a
    license policy. Remove config/license-policy.json and the allow-list gate +
    override corrections it powered.
    
    generateBinaryLicense now lists each shipped dependency with the license the
    CycloneDX SBOM reports, verbatim — so a few imprecise-but-permissive 
upstream
    labels appear as-is (mcp-server-security: Apache-1.0; ANTLR: BSD-4-Clause / 
BSD
    licence). The appendix preamble says licenses are as-reported and links 
each one.
    
    The remaining gate is completeness only: fail if a bundled dependency is 
absent
    from the SBOM, so nothing is silently omitted from the LICENSE. Tests 
updated to
    assert verbatim SBOM labels and SBOM name/URL handling.
    
    Verified: ./gradlew build green; fat-jar LICENSE still lists 158 deps and 
NOTICE
    aggregates 21 upstream notices.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(build): point the LICENSE appendix to the bundled SBOM
    
    Add a line to the appendix preamble noting the machine-readable bill of
    materials (component versions, hashes, licenses) is bundled at
    META-INF/sbom/application.cdx.json — the inline appendix stays the
    human-readable disclosure, with the SBOM offered for tooling.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs: document where/when the binary LICENSE & NOTICE are available
    
    Add a 'where / when they appear' note to the Release LICENSE / NOTICE 
section:
    both binary files are regenerated on every build (tasks run ahead of 
bootJar and
    in check), land at META-INF/LICENSE and META-INF/NOTICE in the fat jar and 
thus
    in every published Docker image, and are also written to 
build/generated/license/
    for local viewing; source-form jars carry the repo-root base files.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(build): explain the buildSrc LICENSE/NOTICE plugin for non-Gradle 
readers
    
    Reviewers who don't work with Gradle had no easy way into buildSrc. Add:
    
    - buildSrc/README.md: what buildSrc is, a short glossary of the Gradle 
concepts
      the code uses (Task, @TaskAction, the input/output annotations, 
Property/Provider
      types, convention plugin, productionRuntimeClasspath), and the end-to-end 
flow.
    - KDoc on GenerateBinaryLicense / GenerateBinaryNotice: a "for readers new 
to
      Gradle" orientation on each class plus a note on every annotated property
      explaining what the input/output annotation does (up-to-date checking, 
ordering).
    - A note on the convention plugin header explaining precompiled script 
plugins,
      and a comment on buildSrc/build.gradle.kts explaining what it builds.
    
    Documentation only; no behaviour change. ./gradlew :buildSrc:test green.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * docs(build): comment the convention plugin body for non-Gradle readers
    
    Add plain-language inline comments through the plugin body explaining the 
parts
    that are opaque without Gradle background: what a 'configuration' is and why
    productionRuntimeClasspath equals 'what ships', how the lazy provider chains
    (flatMap/map over resolvedArtifacts) derive the coordinate list and the
    jar-name->coordinate map, what tasks.register/.set wiring does, and how 
metaInf
    from(...) plus dependsOn bundle the generated files into the bootJar while 
the
    source-form jars keep the base files. Comments only; code unchanged.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    * Dont double load the root files
    
    * feat(build): enforce Apache license headers via RAT convention plugin
    
    Add Apache RAT (Release Audit Tool) header enforcement as an 
`org.apache.solr.mcp.rat` buildSrc convention plugin, stacked on the 
license-notice plugin from #138. RAT is wired into `check`, so `./gradlew 
build` audits that every scanned file carries an ASF header (report at 
build/reports/rat/index.html).
    
    The .gitignore-to-RAT-glob translation lives in a pure, unit-tested 
`RatExcludes` helper rather than inline in build.gradle.kts. Moving it to 
buildSrc fixes two gitignore-semantics gaps from the inline approach: 
interior-slash patterns (e.g. src/generated) are now root-anchored instead of 
matched at any depth, and the negation/anchoring rules are documented and 
tested.
    
    Local developer-tooling dirs (.claude worktrees, .kotlin caches) are 
excluded so contributors don't hit spurious audit failures. ASF headers are 
added to the three application*.properties and libs.versions.toml so they pass 
the audit.
    
    Supersedes the inline approach in #149. Stacked on #138. Fixes #141.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    
    ---------
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Signed-off-by: adityamparikh <[email protected]>
    Co-authored-by: Claude <[email protected]>
    Co-authored-by: Eric Pugh <[email protected]>
---
 build.gradle.kts                                   |  3 +
 buildSrc/README.md                                 | 17 +++-
 buildSrc/build.gradle.kts                          |  9 +++
 .../main/kotlin/org.apache.solr.mcp.rat.gradle.kts | 93 ++++++++++++++++++++++
 .../org/apache/solr/mcp/build/RatExcludes.kt       | 81 +++++++++++++++++++
 .../org/apache/solr/mcp/build/RatExcludesTest.kt   | 65 +++++++++++++++
 gradle/libs.versions.toml                          | 16 ++++
 src/main/resources/application-http.properties     | 16 ++++
 src/main/resources/application-stdio.properties    | 16 ++++
 src/main/resources/application.properties          | 16 ++++
 10 files changed, 331 insertions(+), 1 deletion(-)

diff --git a/build.gradle.kts b/build.gradle.kts
index 572bc76..c9052fb 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -32,6 +32,9 @@ plugins {
     // Listed after spring-boot + cyclonedx so productionRuntimeClasspath and
     // cyclonedxBom exist when it wires its tasks. See buildSrc/.
     id("org.apache.solr.mcp.license-notice")
+    // Enforces Apache license headers via Apache RAT (buildSrc convention 
plugin).
+    // Wires `rat` into `check`, so `./gradlew build` audits headers. See 
buildSrc/.
+    id("org.apache.solr.mcp.rat")
 }
 
 // GraalVM Native Image (Opt-In)
diff --git a/buildSrc/README.md b/buildSrc/README.md
index 4d9bdff..f05cc5a 100644
--- a/buildSrc/README.md
+++ b/buildSrc/README.md
@@ -15,6 +15,18 @@
  limitations under the License.
 -->
 
+# buildSrc — project build logic
+
+This directory holds the project's custom build logic, written in Kotlin. 
Today that is
+two ASF-compliance concerns:
+
+- assembling the **binary-release `LICENSE` and `NOTICE`** files bundled 
inside the
+  executable JAR (the end-user view of *what* these contain lives on the
+  [Licensing & Notices](https://solr.apache.org/mcp/licensing.html) docs 
page); and
+- enforcing **Apache license headers** on source files via Apache RAT.
+
+If you don't work with Gradle day-to-day, this README explains what each piece 
is and how
+they fit together.
 # buildSrc — generating the binary LICENSE & NOTICE
 
 This directory holds the build logic that assembles the **binary-release 
`LICENSE`
@@ -39,7 +51,10 @@ small. (Think of it as a tiny library that only this 
project's build uses.)
 | `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). |
+| `src/main/kotlin/.../RatExcludes.kt` | Pure helper that translates 
`.gitignore` entries into Apache RAT (Ant-style) exclude globs. |
+| `src/main/kotlin/org.apache.solr.mcp.rat.gradle.kts` | A **convention 
plugin** that applies Apache RAT and configures its excludes 
(`.gitignore`-derived + an explicit list). |
+| `src/test/kotlin/.../RatExcludesTest.kt` | Unit tests for the gitignore→glob 
translation. |
+| `build.gradle.kts` | Builds `buildSrc` itself (enables Kotlin + the RAT 
plugin + the test dependencies). |
 
 ## Gradle concepts, for Java developers
 
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 40fb7c8..d1296d7 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -24,9 +24,18 @@ plugins {
 
 repositories {
     mavenCentral()
+    // Hosts the Apache RAT plugin marker below — it is not published to Maven 
Central.
+    gradlePluginPortal()
 }
 
 dependencies {
+    // Makes the Apache RAT plugin (id `org.nosphere.apache.rat`) available to 
the
+    // `org.apache.solr.mcp.rat` convention plugin's `plugins {}` block. The 
version lives
+    // here (mirroring how the junit dep below is pinned) since buildSrc does 
not read the
+    // root project's version catalog. Latest release as of writing; bump in 
lockstep with
+    // the plugin's upstream releases.
+    
implementation("org.nosphere.apache.rat:org.nosphere.apache.rat.gradle.plugin:0.8.1")
+
     // 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")
diff --git a/buildSrc/src/main/kotlin/org.apache.solr.mcp.rat.gradle.kts 
b/buildSrc/src/main/kotlin/org.apache.solr.mcp.rat.gradle.kts
new file mode 100644
index 0000000..e9ea949
--- /dev/null
+++ b/buildSrc/src/main/kotlin/org.apache.solr.mcp.rat.gradle.kts
@@ -0,0 +1,93 @@
+/*
+ * 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: Apache RAT (Release Audit Tool) license-header 
enforcement.
+//
+// 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.rat`); the root build applies it with one line,
+// `id("org.apache.solr.mcp.rat")`. The `plugins {}` block below applies the 
third-party
+// RAT plugin (made available to buildSrc by the `org.nosphere.apache.rat...` 
dependency
+// in buildSrc/build.gradle.kts), and the body configures its `rat` task.
+//
+// RAT verifies that every scanned file carries an Apache license header. The 
plugin wires
+// its `rat` task into `check`, so a plain `./gradlew build` runs it; the 
report lands at
+// build/reports/rat/index.html.
+//
+// Exclusions come from two sources so patterns are not duplicated:
+//   1. .gitignore — reused as the single source of truth for everything git 
already
+//      ignores (build output, .gradle, IDE dirs, *.iml, out/, bin/, .vscode, 
etc.).
+//      RatExcludes (in this same buildSrc) translates each entry into a RAT 
(Ant) glob;
+//      see that class for the gitignore→glob mapping rules.
+//   2. The explicit list below — only *tracked* files that RAT would scan but 
that
+//      legitimately carry no Apache header (binaries, data without a comment 
syntax,
+//      docs, tool/infra config, and LICENSE/NOTICE themselves).
+
+import org.apache.solr.mcp.build.RatExcludes
+import org.nosphere.apache.rat.RatTask
+
+plugins {
+    id("org.nosphere.apache.rat")
+}
+
+tasks.withType<RatTask>().configureEach {
+    val gitignore = rootProject.file(".gitignore")
+    if (gitignore.exists()) {
+        excludes.addAll(RatExcludes.fromGitignore(gitignore.readLines()))
+    }
+
+    excludes.addAll(
+        listOf(
+            // Gradle wrapper (ships under its own license) + on-disk OS cruft
+            "gradlew",
+            "gradlew.bat",
+            "gradle/wrapper/**",
+            "**/.DS_Store",
+            // Tracked dotfiles that take no header
+            ".run/**",
+            ".gitignore",
+            ".gitattributes",
+            ".editorconfig",
+            ".tool-versions",
+            ".env.example",
+            // License/notice files themselves (no header by definition)
+            "LICENSE",
+            "NOTICE",
+            // ASF infra metadata and tool config (no header by convention)
+            ".asf.yaml",
+            "config/**",
+            // Local developer tooling not tracked by git — Claude Code 
worktrees/settings
+            // and the Kotlin compiler cache (analogous to the gitignored 
.idea/.gradle).
+            // Present only on some local checkouts, never in CI.
+            ".claude/**",
+            "**/.kotlin/**",
+            // Tabular data — no comment syntax to hold a header
+            "**/*.csv",
+            // Documentation (markdown carries no license header)
+            "**/*.md",
+            "docs/**",
+            "dev-docs/**",
+            "security-docs/**",
+            // Binary assets
+            "images/**",
+            "**/*.png",
+            // Data / generated content (JSON cannot hold comments)
+            "mydata/**",
+            "**/*.json",
+        ),
+    )
+}
diff --git a/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/RatExcludes.kt 
b/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/RatExcludes.kt
new file mode 100644
index 0000000..f23d948
--- /dev/null
+++ b/buildSrc/src/main/kotlin/org/apache/solr/mcp/build/RatExcludes.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+package org.apache.solr.mcp.build
+
+/**
+ * Translates `.gitignore` lines into Apache RAT (Ant-style) exclude globs.
+ *
+ * RAT scans every file under the project directory; we reuse `.gitignore` as 
the single
+ * source of truth for build output, IDE folders, and other untracked cruft so 
those
+ * patterns are not duplicated in the build script. The RAT plugin's own 
`excludeFile`
+ * does **not** interpret `.gitignore` path semantics, so we translate each 
entry here.
+ *
+ * This is pure, Gradle-free logic precisely so it can be unit-tested
+ * ([RatExcludesTest]); the gitignore-to-Ant-glob mapping has enough edge cases
+ * (anchoring, directory markers, negation) to be worth pinning down with 
tests.
+ *
+ * ### Mapping rules (a practical subset of gitignore semantics)
+ *
+ * Throughout, "globstar" means the two-asterisk Ant wildcard that matches any 
number of
+ * path segments (written here without the trailing slash to keep this comment 
valid).
+ *
+ * - Blank lines and `#` comments are skipped.
+ * - **Negation (`!foo`) entries are skipped.** Git uses them to *re-include* 
a path, but
+ *   RAT excludes have no re-inclusion mechanism. Skipping (rather than 
excluding) is the
+ *   safe choice: at worst RAT still scans a file git would ignore. The 
opposite — a
+ *   bare `build/` excluding a re-included `!build/keep.txt` — over-excludes 
that one file
+ *   from header checking, which we accept as a rare, low-risk gap.
+ * - A **trailing slash** (directory marker) is stripped; the entry is still 
emitted in
+ *   both bare and directory-contents (globstar-suffixed) forms.
+ * - **Anchoring** follows git: an entry with a leading slash, or with an 
interior slash
+ *   (a separator that is not just the trailing one), is anchored to the repo 
root and is
+ *   emitted as-is (leading slash removed). An entry with no separator — or 
only a
+ *   trailing slash — matches at any depth and is prefixed with a leading 
globstar segment.
+ *   An entry that already begins with a globstar segment is left untouched. 
(The original
+ *   inline implementation prefixed *every* non-leading-slash entry with a 
globstar, which
+ *   wrongly turned a root-anchored `foo/bar` into an any-depth match; this 
distinguishes
+ *   the two.)
+ *
+ * Each surviving entry yields two globs — the path itself and its 
directory-contents form
+ * — so an ignored directory and everything under it are both pruned.
+ */
+object RatExcludes {
+
+    fun fromGitignore(lines: List<String>): List<String> =
+        lines.asSequence()
+            .map { it.trim() }
+            .filter { it.isNotEmpty() }
+            .filterNot { it.startsWith("#") }
+            .filterNot { it.startsWith("!") }
+            .map { it.trimEnd('/') }
+            .filter { it.isNotEmpty() }
+            .map(::toGlob)
+            .flatMap { sequenceOf(it, "$it/**") }
+            .distinct()
+            .toList()
+
+    /** Maps one normalized (trimmed, no trailing slash) gitignore entry to 
one Ant glob. */
+    private fun toGlob(entry: String): String {
+        val anchored = entry.startsWith("/")
+        val body = entry.removePrefix("/")
+        return when {
+            body.startsWith("**/") -> body // already any-depth
+            anchored || body.contains("/") -> body // root-anchored (leading 
or interior slash)
+            else -> "**/$body" // no separator → match at any depth
+        }
+    }
+}
diff --git 
a/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/RatExcludesTest.kt 
b/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/RatExcludesTest.kt
new file mode 100644
index 0000000..92b747c
--- /dev/null
+++ b/buildSrc/src/test/kotlin/org/apache/solr/mcp/build/RatExcludesTest.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.solr.mcp.build
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Test
+
+class RatExcludesTest {
+
+    @Test
+    fun `each entry yields both the path and its directory-contents glob`() {
+        assertEquals(listOf("**/build", "**/build/**"), 
RatExcludes.fromGitignore(listOf("build/")))
+    }
+
+    @Test
+    fun `a no-separator entry matches at any depth`() {
+        // ".gradle" can appear at any level, so it must be prefixed with **/.
+        
assertTrue(RatExcludes.fromGitignore(listOf(".gradle")).contains("**/.gradle"))
+        
assertTrue(RatExcludes.fromGitignore(listOf("*.iml")).contains("**/*.iml"))
+    }
+
+    @Test
+    fun `a leading-slash entry is anchored to the repo root`() {
+        // "/build" means the root build dir only — no **/ prefix, leading 
slash stripped.
+        assertEquals(listOf("build", "build/**"), 
RatExcludes.fromGitignore(listOf("/build")))
+    }
+
+    @Test
+    fun `an interior-slash entry is root-anchored, not any-depth`() {
+        // git anchors "src/generated" to the root; it must NOT become 
**/src/generated.
+        val globs = RatExcludes.fromGitignore(listOf("src/generated"))
+        assertEquals(listOf("src/generated", "src/generated/**"), globs)
+    }
+
+    @Test
+    fun `an entry already starting with double-star is left untouched`() {
+        assertEquals(listOf("**/*.log", "**/*.log/**"), 
RatExcludes.fromGitignore(listOf("**/*.log")))
+    }
+
+    @Test
+    fun `blank lines, comments, and negations are skipped`() {
+        val globs = RatExcludes.fromGitignore(listOf("", "   ", "# a comment", 
"!keep.txt"))
+        assertTrue(globs.isEmpty(), "expected no globs but got $globs")
+    }
+
+    @Test
+    fun `duplicate entries are collapsed`() {
+        assertEquals(listOf("**/target", "**/target/**"), 
RatExcludes.fromGitignore(listOf("target", "target/")))
+    }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6fc2187..473ff9f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,3 +1,19 @@
+#
+# 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.
+#
 [versions]
 # Build plugins
 spring-boot = "3.5.14"
diff --git a/src/main/resources/application-http.properties 
b/src/main/resources/application-http.properties
index c1b9759..77578a3 100644
--- a/src/main/resources/application-http.properties
+++ b/src/main/resources/application-http.properties
@@ -1,3 +1,19 @@
+#
+# 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.
+#
 spring.main.web-application-type=servlet
 spring.ai.mcp.server.type=sync
 spring.ai.mcp.server.protocol=stateless
diff --git a/src/main/resources/application-stdio.properties 
b/src/main/resources/application-stdio.properties
index b8864df..37f848b 100644
--- a/src/main/resources/application-stdio.properties
+++ b/src/main/resources/application-stdio.properties
@@ -1,3 +1,19 @@
+#
+# 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.
+#
 spring.main.web-application-type=none
 # NOTE: You must disable the banner and the console logging
 # to allow the STDIO transport to work !!!
diff --git a/src/main/resources/application.properties 
b/src/main/resources/application.properties
index 1592a77..c2038f5 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,3 +1,19 @@
+#
+# 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.
+#
 spring.application.name=solr-mcp
 spring.profiles.active=${PROFILES:stdio}
 spring.ai.mcp.server.instructions=This server provides tools to interact with 
Apache Solr using Model Context Protocol (MCP) over STDIO and/or HTTP. 


Reply via email to