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 ffcb508 ci: add dedicated PR validation workflow with
unit/integration test split (#117)
ffcb508 is described below
commit ffcb5089ded8ad83a3f81d9cb83581122cb1f16a
Author: Aditya Parikh <[email protected]>
AuthorDate: Sat May 2 07:03:32 2026 -0400
ci: add dedicated PR validation workflow with unit/integration test split
(#117)
Add a focused ci.yml workflow for pull request validation against main,
separating unit tests from Testcontainers-based integration tests for
clearer feedback and faster signal on PRs.
Signed-off-by: adityamparikh <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.github/workflows/build-and-publish.yml | 13 +-
.github/workflows/ci.yml | 185 +++++++++++++++++++++
build.gradle.kts | 41 +++++
.../solr/mcp/server/McpClientIntegrationTest.java | 2 +
.../CollectionServiceIntegrationTest.java | 27 +--
.../ConferenceEndToEndIntegrationTest.java | 2 +
.../solr/mcp/server/config/SolrConfigTest.java | 2 +
.../indexing/IndexingServiceIntegrationTest.java | 2 +
.../metadata/SchemaServiceIntegrationTest.java | 2 +
.../observability/DistributedTracingTest.java | 2 +
.../observability/OtlpExportIntegrationTest.java | 2 +
.../search/SearchServiceIntegrationTest.java | 2 +
12 files changed, 261 insertions(+), 21 deletions(-)
diff --git a/.github/workflows/build-and-publish.yml
b/.github/workflows/build-and-publish.yml
index 27c3013..18d5d1c 100644
--- a/.github/workflows/build-and-publish.yml
+++ b/.github/workflows/build-and-publish.yml
@@ -23,7 +23,6 @@
# WHEN TO USE:
# -----------
# ✅ Automatic on every merge to main
-# ✅ Testing pull requests (build + test only, no publish)
# ✅ Development/testing Docker images
# ❌ DO NOT use for official ASF releases (use release-publish.yml instead)
#
@@ -31,7 +30,7 @@
# --------------------------------
# build-and-publish.yml (THIS FILE):
# - Purpose: Development CI/CD
-# - Trigger: Automatic (push/PR)
+# - Trigger: Automatic (push)
# - Docker Hub: Personal namespace
# - ASF Vote: Not required
# - Use for: Daily development work
@@ -67,8 +66,9 @@
# ------------------
# 1. Push to 'main' branch - Builds, tests, and publishes Docker images
# 2. Version tags (v*) - Builds and publishes release images with version tags
-# 3. Pull requests to 'main' - Only builds and tests (no publishing)
-# 4. Manual trigger via workflow_dispatch
+# 3. Manual trigger via workflow_dispatch
+#
+# Note: Pull request validation is handled by ci.yml
#
# Jobs:
# -----
@@ -96,17 +96,14 @@ name: Build and Publish
# Triggers for this workflow
# - push: runs on commits to main and on version tags (v*)
-# - pull_request: runs on PRs targeting main (build/test only; no publishing)
# - workflow_dispatch: allows manual execution from the Actions UI
+# Pull request validation is handled separately by ci.yml
on:
push:
branches:
- main # Build + publish dev images on main merges
tags:
- 'v*' # CAUTION (ASF): tag pushes will publish images;
prefer using release-publish.yml for post-vote releases
- pull_request:
- branches:
- - main # Build + test validation for incoming changes
workflow_dispatch: # Manual runs for maintainers
jobs:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c29eed1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,185 @@
+# 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.
+
+# ============================================================================
+# Pull Request CI Workflow
+# ============================================================================
+#
+# Validates pull requests against main with:
+# 1. build - Compile + format check (fast feedback)
+# 2. unit-tests - Pure unit tests (no Docker/Testcontainers)
+# 3. integration-tests - Testcontainers-based integration tests
+# 4. solr-compatibility - Multi-version Solr matrix (label-gated)
+#
+# The solr-compatibility job only runs when the PR has the
+# "solr-compatibility" label. Default Solr 9.9 is already covered
+# by the integration-tests job.
+# ============================================================================
+
+name: CI
+
+on:
+ pull_request:
+ branches:
+ - main
+
+concurrency:
+ group: ci-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ # ========================================================================
+ # Job 1: Build
+ # ========================================================================
+ # Compiles production and test sources and checks code formatting.
+ # Uses spotlessCheck (read-only) instead of spotlessApply so CI never
+ # modifies source files.
+ # ========================================================================
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: ./.github/actions/setup-java
+
+ - name: Compile and check formatting
+ run: ./gradlew classes testClasses spotlessCheck
+
+ # ========================================================================
+ # Job 2: Unit Tests
+ # ========================================================================
+ # Runs pure unit tests that do not require Docker or Testcontainers.
+ # Excludes tests tagged with "integration" or "docker-integration".
+ # ========================================================================
+ unit-tests:
+ name: Unit Tests
+ runs-on: ubuntu-latest
+ needs: build
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: ./.github/actions/setup-java
+
+ - name: Run unit tests
+ run: ./gradlew unitTest
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: unit-test-results
+ path: build/test-results/unitTest/
+ retention-days: 7
+
+ - name: Upload coverage report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: unit-test-coverage
+ path: build/reports/jacoco/
+ retention-days: 7
+
+ # ========================================================================
+ # Job 3: Integration Tests
+ # ========================================================================
+ # Runs Testcontainers-based integration tests against the default Solr
+ # version (9.9). These tests start real Solr containers via Docker.
+ # ========================================================================
+ integration-tests:
+ name: Integration Tests
+ runs-on: ubuntu-latest
+ needs: build
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: ./.github/actions/setup-java
+
+ - name: Run integration tests
+ run: ./gradlew integrationTest
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: integration-test-results
+ path: build/test-results/integrationTest/
+ retention-days: 7
+
+ - name: Upload coverage report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: integration-test-coverage
+ path: build/reports/jacoco/
+ retention-days: 7
+
+ # ========================================================================
+ # Job 4: Solr Version Compatibility
+ # ========================================================================
+ # Tests against multiple Solr versions to catch compatibility regressions.
+ # Only runs when the PR has the "solr-compatibility" label (opt-in).
+ # Solr 9.9 is omitted since it is already tested in integration-tests.
+ # ========================================================================
+ solr-compatibility:
+ name: Solr ${{ matrix.solr-version }} Compatibility
+ runs-on: ubuntu-latest
+ needs: integration-tests
+ if: contains(github.event.pull_request.labels.*.name,
'solr-compatibility')
+ timeout-minutes: 30
+
+ strategy:
+ fail-fast: false
+ matrix:
+ solr-version:
+ - "8.11-slim"
+ - "9.4-slim"
+ - "9.10-slim"
+ - "10-slim"
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: ./.github/actions/setup-java
+
+ - name: Run integration tests against Solr ${{
matrix.solr-version }}
+ env:
+ SOLR_VERSION: ${{ matrix.solr-version }}
+ run: ./gradlew integrationTest
"-Dsolr.test.image=solr:${SOLR_VERSION}"
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: solr-${{ matrix.solr-version }}-test-results
+ path: build/test-results/integrationTest/
+ retention-days: 7
diff --git a/build.gradle.kts b/build.gradle.kts
index 3161a54..dc878d9 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -188,6 +188,47 @@ tasks.withType<Test> {
}
}
+tasks.register<Test>("unitTest") {
+ description = "Runs unit tests only (no Testcontainers)"
+ group = "verification"
+
+ useJUnitPlatform {
+ excludeTags("integration", "docker-integration")
+ }
+
+ testClassesDirs = sourceSets["test"].output.classesDirs
+ classpath = sourceSets["test"].runtimeClasspath
+
+ finalizedBy(tasks.jacocoTestReport)
+
+ reports {
+ html.outputLocation.set(layout.buildDirectory.dir("reports/unitTest"))
+
junitXml.outputLocation.set(layout.buildDirectory.dir("test-results/unitTest"))
+ }
+}
+
+tasks.register<Test>("integrationTest") {
+ description = "Runs Testcontainers-based integration tests"
+ group = "verification"
+
+ useJUnitPlatform {
+ includeTags("integration")
+ }
+
+ testClassesDirs = sourceSets["test"].output.classesDirs
+ classpath = sourceSets["test"].runtimeClasspath
+
+ systemProperty("solr.test.image", System.getProperty("solr.test.image",
"solr:9.9-slim"))
+
+ mustRunAfter(tasks.named("unitTest"))
+ finalizedBy(tasks.jacocoTestReport)
+
+ reports {
+
html.outputLocation.set(layout.buildDirectory.dir("reports/integrationTest"))
+
junitXml.outputLocation.set(layout.buildDirectory.dir("test-results/integrationTest"))
+ }
+}
+
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
diff --git
a/src/test/java/org/apache/solr/mcp/server/McpClientIntegrationTest.java
b/src/test/java/org/apache/solr/mcp/server/McpClientIntegrationTest.java
index 3eed6f8..79c4735 100644
--- a/src/test/java/org/apache/solr/mcp/server/McpClientIntegrationTest.java
+++ b/src/test/java/org/apache/solr/mcp/server/McpClientIntegrationTest.java
@@ -32,6 +32,7 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestMethodOrder;
@@ -52,6 +53,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
"spring.docker.compose.enabled=false"})
@ActiveProfiles("http")
@Import(TestcontainersConfiguration.class)
+@Tag("integration")
@Testcontainers(disabledWithoutDocker = true)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
diff --git
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
index 21a6ef8..788b952 100644
---
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
+++
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceIntegrationTest.java
@@ -16,12 +16,23 @@
*/
package org.apache.solr.mcp.server.collection;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import org.apache.solr.mcp.server.TestcontainersConfiguration;
import org.apache.solr.mcp.server.indexing.IndexingService;
import org.apache.solr.mcp.server.search.SearchResponse;
import org.apache.solr.mcp.server.search.SearchService;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.slf4j.Logger;
@@ -31,19 +42,9 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.testcontainers.junit.jupiter.Testcontainers;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
@SpringBootTest
@Import(TestcontainersConfiguration.class)
+@Tag("integration")
@Testcontainers(disabledWithoutDocker = true)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CollectionServiceIntegrationTest {
@@ -232,8 +233,8 @@ class CollectionServiceIntegrationTest {
HandlerInfo select = handlerStats.selectHandler();
assertNotNull(select);
assertTrue(select.requests() > 0, "Select handler requests
should be positive after queries");
- assertNull(select.errors());
- assertNull(select.timeouts());
+ assertNull(select.errors());
+ assertNull(select.timeouts());
// Update handler: indexing 50 docs should have driven request
counts > 0
HandlerInfo update = handlerStats.updateHandler();
diff --git
a/src/test/java/org/apache/solr/mcp/server/collection/ConferenceEndToEndIntegrationTest.java
b/src/test/java/org/apache/solr/mcp/server/collection/ConferenceEndToEndIntegrationTest.java
index 94f17d4..baa0305 100644
---
a/src/test/java/org/apache/solr/mcp/server/collection/ConferenceEndToEndIntegrationTest.java
+++
b/src/test/java/org/apache/solr/mcp/server/collection/ConferenceEndToEndIntegrationTest.java
@@ -29,6 +29,7 @@ import org.apache.solr.mcp.server.search.SearchService;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestMethodOrder;
@@ -43,6 +44,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
*/
@SpringBootTest
@Import(TestcontainersConfiguration.class)
+@Tag("integration")
@Testcontainers(disabledWithoutDocker = true)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
diff --git
a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java
b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java
index 44247fb..31e88af 100644
--- a/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java
+++ b/src/test/java/org/apache/solr/mcp/server/config/SolrConfigTest.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.*;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.HttpJdkSolrClient;
import org.apache.solr.mcp.server.TestcontainersConfiguration;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -30,6 +31,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Import(TestcontainersConfiguration.class)
+@Tag("integration")
@Testcontainers(disabledWithoutDocker = true)
class SolrConfigTest {
diff --git
a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
index bb7e586..57a544b 100644
---
a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
+++
b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
@@ -31,6 +31,7 @@ import
org.apache.solr.mcp.server.indexing.documentcreator.XmlDocumentCreator;
import org.apache.solr.mcp.server.search.SearchResponse;
import org.apache.solr.mcp.server.search.SearchService;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
@@ -45,6 +46,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
*/
@SpringBootTest
@Import(TestcontainersConfiguration.class)
+@Tag("integration")
@Testcontainers(disabledWithoutDocker = true)
@DisabledInNativeImage
class IndexingServiceIntegrationTest {
diff --git
a/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceIntegrationTest.java
b/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceIntegrationTest.java
index 020162f..d02689c 100644
---
a/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceIntegrationTest.java
+++
b/src/test/java/org/apache/solr/mcp/server/metadata/SchemaServiceIntegrationTest.java
@@ -23,6 +23,7 @@ import
org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.response.schema.SchemaRepresentation;
import org.apache.solr.mcp.server.TestcontainersConfiguration;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -35,6 +36,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
*/
@SpringBootTest
@Import(TestcontainersConfiguration.class)
+@Tag("integration")
@Testcontainers(disabledWithoutDocker = true)
class SchemaServiceIntegrationTest {
diff --git
a/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java
b/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java
index 6732974..4521b55 100644
---
a/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java
+++
b/src/test/java/org/apache/solr/mcp/server/observability/DistributedTracingTest.java
@@ -26,6 +26,7 @@ import org.apache.solr.mcp.server.TestcontainersConfiguration;
import org.apache.solr.mcp.server.search.SearchService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -60,6 +61,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
// Enable @Observed annotation support
"management.observations.annotations.enabled=true"})
@Import({TestcontainersConfiguration.class,
OpenTelemetryTestConfiguration.class})
+@Tag("integration")
@Testcontainers(disabledWithoutDocker = true)
@ActiveProfiles("http")
class DistributedTracingTest {
diff --git
a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java
b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java
index 4adde17..c247d48 100644
---
a/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java
+++
b/src/test/java/org/apache/solr/mcp/server/observability/OtlpExportIntegrationTest.java
@@ -29,6 +29,7 @@ import org.apache.solr.mcp.server.indexing.IndexingService;
import org.apache.solr.mcp.server.search.SearchService;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -77,6 +78,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
// Ensure 100% sampling for tests
"management.tracing.sampling.probability=1.0"})
@Import(TestcontainersConfiguration.class)
+@Tag("integration")
@Testcontainers(disabledWithoutDocker = true)
@ActiveProfiles("http")
class OtlpExportIntegrationTest {
diff --git
a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java
b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java
index 59f712a..9f5ff90 100644
---
a/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java
+++
b/src/test/java/org/apache/solr/mcp/server/search/SearchServiceIntegrationTest.java
@@ -29,6 +29,7 @@ import
org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.mcp.server.TestcontainersConfiguration;
import org.apache.solr.mcp.server.indexing.IndexingService;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
@@ -42,6 +43,7 @@ import org.testcontainers.junit.jupiter.Testcontainers;
*/
@SpringBootTest
@Import(TestcontainersConfiguration.class)
+@Tag("integration")
@Testcontainers(disabledWithoutDocker = true)
@DisabledInNativeImage
class SearchServiceIntegrationTest {