This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push:
new 56defd136b Add incremental build support to CI workflow
56defd136b is described below
commit 56defd136b5a73a64290a0687909461316601dce
Author: James Netherton <[email protected]>
AuthorDate: Wed Jun 24 14:08:46 2026 +0100
Add incremental build support to CI workflow
Integrate Maveniverse Scalpel for git-diff-based incremental builds.
When a PR changes only a subset of modules, only the affected tests run,
significantly reducing CI time. Full builds still trigger for changes to
core infrastructure (build parents, extensions-core, tooling, .mvn).
Key changes:
- Add Scalpel Maven extension (.mvn/extensions.xml) for diff analysis
- Add IncrementalBuildMojo for matrix generation and test filtering
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.github/workflows/ci-build.yaml | 376 +++++++++--
.mvn/extensions.xml | 8 +
.mvn/maven.config | 3 +
.../camel/quarkus/maven/IncrementalBuildMojo.java | 701 +++++++++++++++++++++
4 files changed, 1051 insertions(+), 37 deletions(-)
diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml
index c34db8d58a..026b5c8d8b 100644
--- a/.github/workflows/ci-build.yaml
+++ b/.github/workflows/ci-build.yaml
@@ -106,6 +106,12 @@ env:
CQ_MAVEN_ARGS: -V -ntp -e -Daether.connector.http.connectionMaxTtl=120
TESTCONTAINERS_RYUK_DISABLED: true
CHECKOUT_REF: ${{ github.event_name == 'pull_request' &&
github.event.pull_request.user.login == 'dependabot[bot]' && github.head_ref ||
'' }}
+ # Scalpel configuration (defaults for all jobs; initial-mvn-install
overrides SCALPEL_FULL_BUILD_TRIGGERS).
+ # Broad extensions-core/** trigger here is intentional:
functional-extension-tests uses trim mode,
+ # which actively excludes modules from the build, so any extensions-core
change should run full tests.
+ SCALPEL_FULL_BUILD_TRIGGERS:
"-Dscalpel.fullBuildTriggers=poms/build-parent/**,poms/build-parent-it/**,extensions-core/**,tooling/maven-plugin/**,.mvn/**"
+ SCALPEL_EXCLUDE_PATHS:
"-Dscalpel.excludePaths=**.adoc,**.md,docs/**,Jenkinsfile*,LICENSE.txt,NOTICE.txt,KEYS,camel-quarkus-sbom/**,.github/*.sh"
+ CQ_MAVEN_SKIP_CHECKS: "-Dformatter.skip -Dimpsort.skip -Denforcer.skip
-Dcamel-quarkus.update-extension-doc-page.skip"
jobs:
pre-build-checks:
@@ -179,11 +185,25 @@ jobs:
runs-on: ubuntu-latest
needs: pre-build-checks
outputs:
- matrix: ${{ steps.set-native-matrix.outputs.matrix }}
- examples-matrix: ${{ steps.set-examples-matrix.outputs.examples-matrix }}
+ # Miscellaneous job outputs. Keep sorted alphabetically.
cache-key: ${{ steps.maven-cache.outputs.cache-key }}
+ # Incremental build job outputs. Keep sorted alphabetically.
+ examples-matrix: ${{ steps.set-examples-matrix.outputs.examples-matrix }}
+ integration-tests-jvm-modules: ${{
steps.process-incremental.outputs.integration-tests-jvm-modules || '' }}
+ integration-tests-matrix: ${{
steps.process-incremental.outputs.use-incremental == 'true' &&
steps.process-incremental.outputs.integration-tests-matrix ||
steps.set-native-matrix-full.outputs.matrix || '{"include":[]}' }}
+ integration-tests-modules: ${{
steps.incremental-build.outputs.integration-tests-modules }}
+ run-catalog-tests: ${{
steps.process-incremental.outputs.run-catalog-tests }}
+ run-examples: ${{ steps.process-incremental.outputs.run-examples }}
+ run-extensions-core-tests: ${{
steps.process-incremental.outputs.run-extensions-core-tests }}
+ run-extensions-tests: ${{
steps.process-incremental.outputs.run-extensions-tests }}
+ run-integration-tests-jvm: ${{
steps.process-incremental.outputs.run-integration-tests-jvm }}
+ run-test-framework-tests: ${{
steps.process-incremental.outputs.run-test-framework-tests }}
+ run-tooling-tests: ${{
steps.process-incremental.outputs.run-tooling-tests }}
env:
MAVEN_OPTS: -Xmx4600m
+ SCALPEL_ENABLED: "-Dscalpel.enabled=true"
+ SCALPEL_FULL_BUILD_TRIGGERS:
"-Dscalpel.fullBuildTriggers=poms/build-parent/**,poms/build-parent-it/**,extensions-core/core/**,tooling/maven-plugin/**,.mvn/**"
+ SCALPEL_EXCLUDE_PATHS: "-Dscalpel.excludePaths=docs/**"
steps:
- name: Check free space on disk
run: |
@@ -221,12 +241,138 @@ jobs:
run: |
cd ../camel
./mvnw ${CQ_MAVEN_ARGS} clean install -Dquickly -T1C
+ - name: Detect PR Incremental Build
+ id: detect-incremental
+ if: github.event_name == 'pull_request' &&
!contains(github.event.pull_request.labels.*.name, 'ci/disable-incremental')
+ run: |
+ echo "incremental-build=true" >> $GITHUB_OUTPUT
- name: Update extension metadata
run: |
./mvnw -N cq:update-quarkus-metadata ${CQ_MAVEN_ARGS}
- name: mvn clean install -DskipTests
run: |
- eval ./mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} clean install
-DskipTests -Dquarkus.build.skip -Pformat
+ eval ./mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} -T1C clean install
-DskipTests -Dquarkus.build.skip -Pformat
+ - name: Scalpel Analysis
+ id: scalpel-analysis
+ if: steps.detect-incremental.outputs.incremental-build == 'true'
+ continue-on-error: true
+ run: |
+ ./mvnw validate -Dquickly \
+ ${SCALPEL_ENABLED} \
+ -Dscalpel.mode=report \
+ ${SCALPEL_FULL_BUILD_TRIGGERS} \
+ ${SCALPEL_EXCLUDE_PATHS} \
+ ${CQ_MAVEN_ARGS}
+
+ if [ -f target/scalpel-report.json ]; then
+ echo "Scalpel report generated successfully"
+ cat target/scalpel-report.json | jq '{fullBuildTriggered,
triggerFile, affectedModulesCount: (.affectedModules | length)}'
+ else
+ echo "Scalpel report not found"
+ exit 1
+ fi
+ - name: Warn on Scalpel failure
+ if: steps.scalpel-analysis.outcome == 'failure'
+ run: echo "::warning::Scalpel analysis failed, falling back to full
build"
+ - name: Detect Changed Container Properties
+ id: detect-container-props
+ if: steps.detect-incremental.outputs.incremental-build == 'true' &&
steps.scalpel-analysis.outcome != 'failure'
+ run: |
+ # NOTE: We do this manual checking since the container image
properties are not linked to any other pom.xml in the project.
+ # They are queried dynamically via Java code. This logic enables
pom.xml to be removed from Scalpel full build triggers.
+
+ CHANGED_CONTAINER_PROPS=""
+ if [ "${{ github.event_name }}" == "pull_request" ]; then
+ BASE_SHA="${{ github.event.pull_request.base.sha }}"
+ else
+ BASE_SHA="${{ github.event.before }}"
+ fi
+ if [ -n "$BASE_SHA" ] && git diff "$BASE_SHA" -- pom.xml | grep -qE
'[+-].*\.container\.image>'; then
+ CHANGED_CONTAINER_PROPS=$(git diff "$BASE_SHA" -- pom.xml \
+ | grep -E '^\+.*\.container\.image>' \
+ | sed -E 's/.*<([^<\/]+\.container\.image)>.*/\1/' \
+ | paste -sd,)
+ echo "Changed container properties: $CHANGED_CONTAINER_PROPS"
+ fi
+ echo "props=$CHANGED_CONTAINER_PROPS" >> $GITHUB_OUTPUT
+ - name: Incremental Build Analysis
+ id: incremental-build
+ if: steps.detect-incremental.outputs.incremental-build == 'true' &&
steps.scalpel-analysis.outcome != 'failure'
+ continue-on-error: true
+ run: |
+ # Run unified incremental build analysis
+ # All directory patterns are explicitly configured here for
visibility and maintainability
+ # Extract project version dynamically to avoid hardcoding
+ PROJECT_VERSION=$(./mvnw help:evaluate -Dexpression=project.version
-q -DforceStdout -N)
+ echo "Using camel-quarkus-maven-plugin version: $PROJECT_VERSION"
+
+ CONTAINER_PROPS="${{ steps.detect-container-props.outputs.props }}"
+
+ ./mvnw
org.apache.camel.quarkus:camel-quarkus-maven-plugin:${PROJECT_VERSION}:incremental-build
\
+ -Dcq.action=analyze \
+ -Dcq.useIncrementalBuild=true \
+ -Dcq.extensionDirs="extensions/,extensions-jvm/,extensions-core/" \
+
-Dcq.integrationTestDirs="integration-tests/,integration-tests-jvm/" \
+ -Dcq.nativeTestsPrefix="integration-tests/" \
+ -Dcq.jvmTestsPrefix="integration-tests-jvm/" \
+ -Dcq.integrationTestGroupsPrefix="integration-test-groups/" \
+
-Dcq.functionalScopeDirs="extensions-core/:runExtensionsCoreTests,extensions/:runExtensionsTests,test-framework/:runTestFrameworkTests,tooling/:runToolingTests,catalog/:runCatalogTests"
\
+
${CONTAINER_PROPS:+-Dcq.changedContainerProperties="$CONTAINER_PROPS"} \
+ -N ${CQ_MAVEN_ARGS}
+
+ if [ -f target/incremental-build.json ]; then
+ echo "Incremental build analysis complete"
+ cat target/incremental-build.json
+
+ # Output entire JSON as single output (compact for GitHub Actions)
+ echo "data=$(cat target/incremental-build.json | jq -c '.')" >>
$GITHUB_OUTPUT
+
+ # Only set integration-tests-modules for actual incremental builds.
+ # For full builds (incrementalBuild=false), leave it empty so
downstream jobs run all modules.
+ IS_INCREMENTAL=$(cat target/incremental-build.json | jq -r
'.incrementalBuild // false')
+ if [ "$IS_INCREMENTAL" == "true" ]; then
+ INTEGRATION_TESTS_MODULES=$(cat target/incremental-build.json |
jq -c '{modules: .affectedModules, modulesCsv: (.affectedModules | join(",")),
fullBuild: false, totalModules: .affectedModulesCount}')
+ echo "integration-tests-modules=$INTEGRATION_TESTS_MODULES" >>
$GITHUB_OUTPUT
+ fi
+ else
+ echo "Incremental build analysis failed - falling back to full
build"
+ # Output empty JSON structure for fallback
+ echo "data={\"incrementalBuild\":false}" >> $GITHUB_OUTPUT
+ fi
+ - name: Process Incremental Build Data
+ id: process-incremental
+ run: |
+ # Parse JSON and resolve all outputs.
+ # For full builds, all run-* flags resolve to true so job outputs
are simple pass-throughs.
+ INCREMENTAL=false
+ if [ -f target/incremental-build.json ]; then
+ DATA=$(cat target/incremental-build.json)
+ INCREMENTAL=$(echo "$DATA" | jq -r '.incrementalBuild // false')
+ fi
+
+ echo "use-incremental=$INCREMENTAL" >> $GITHUB_OUTPUT
+
+ if [ "$INCREMENTAL" == "true" ]; then
+ echo "Incremental build will be used"
+ echo "integration-tests-matrix=$(echo "$DATA" | jq -c
'.nativeTestMatrix')" >> $GITHUB_OUTPUT
+ echo "run-integration-tests-jvm=$(echo "$DATA" | jq -r
'.integrationTestsJvm.runTests')" >> $GITHUB_OUTPUT
+ echo "integration-tests-jvm-modules=$(echo "$DATA" | jq -r
'.integrationTestsJvm.modules')" >> $GITHUB_OUTPUT
+ echo "run-extensions-core-tests=$(echo "$DATA" | jq -r
'.functionalTestScope.runExtensionsCoreTests')" >> $GITHUB_OUTPUT
+ echo "run-extensions-tests=$(echo "$DATA" | jq -r
'.functionalTestScope.runExtensionsTests')" >> $GITHUB_OUTPUT
+ echo "run-examples=$(echo "$DATA" | jq -r '.runExamples')" >>
$GITHUB_OUTPUT
+ echo "run-test-framework-tests=$(echo "$DATA" | jq -r
'.functionalTestScope.runTestFrameworkTests')" >> $GITHUB_OUTPUT
+ echo "run-tooling-tests=$(echo "$DATA" | jq -r
'.functionalTestScope.runToolingTests')" >> $GITHUB_OUTPUT
+ echo "run-catalog-tests=$(echo "$DATA" | jq -r
'.functionalTestScope.runCatalogTests')" >> $GITHUB_OUTPUT
+ else
+ echo "Full build will be used"
+ echo "run-integration-tests-jvm=true" >> $GITHUB_OUTPUT
+ echo "run-extensions-core-tests=true" >> $GITHUB_OUTPUT
+ echo "run-extensions-tests=true" >> $GITHUB_OUTPUT
+ echo "run-examples=true" >> $GITHUB_OUTPUT
+ echo "run-test-framework-tests=true" >> $GITHUB_OUTPUT
+ echo "run-tooling-tests=true" >> $GITHUB_OUTPUT
+ echo "run-catalog-tests=true" >> $GITHUB_OUTPUT
+ fi
- name: Sync Maven properties
run: |
./mvnw cq:sync-versions ${CQ_MAVEN_ARGS} -N
@@ -252,11 +398,24 @@ jobs:
with:
action: save
cache-key: ${{ steps.maven-cache.outputs.cache-key }}
- - name: Setup Native Test Matrix
- id: set-native-matrix
+ - name: Setup Native Test Matrix (Full Build)
+ id: set-native-matrix-full
+ if: steps.process-incremental.outputs.use-incremental != 'true'
run: |
+ echo "Using all test categories (full build)"
CATEGORIES=$(yq -M -N -I 0 -o=json e 'keys'
tooling/scripts/test-categories.yaml | tr '"' "'")
- echo "matrix={'category': ${CATEGORIES}}" >> $GITHUB_OUTPUT
+ MATRIX_FULL="{'category': ${CATEGORIES}}"
+
+ # Validate matrix size (convert single quotes to double quotes for
jq)
+ MATRIX_SIZE=$(echo "$MATRIX_FULL" | sed "s/'/\"/g" | jq '.category |
length')
+ echo "Native tests matrix size: $MATRIX_SIZE jobs (full build)"
+ if [ "$MATRIX_SIZE" -gt 20 ]; then
+ echo "ERROR: Native tests matrix size ($MATRIX_SIZE) exceeds
maximum expected (20)"
+ echo "Matrix content: $MATRIX_FULL"
+ exit 1
+ fi
+
+ echo "matrix=$MATRIX_FULL" >> $GITHUB_OUTPUT
- name: Setup Examples Matrix
id: set-examples-matrix
run: |
@@ -272,13 +431,15 @@ jobs:
echo "examples-matrix=${EXAMPLES_MATRIX}" >> $GITHUB_OUTPUT
native-tests:
- name: Native Tests - ${{matrix.category}}
+ name: Native Tests - ${{ matrix.name || matrix.category }}
needs: initial-mvn-install
runs-on: ubuntu-latest
- if: github.event_name != 'pull_request' ||
!contains(github.event.pull_request.labels.*.name, 'JVM')
+ if: |
+ (github.event_name != 'pull_request' ||
!contains(github.event.pull_request.labels.*.name, 'JVM'))
+ &&
(fromJson(needs.initial-mvn-install.outputs.integration-tests-matrix).include[0]
!= null ||
fromJson(needs.initial-mvn-install.outputs.integration-tests-matrix).category[0]
!= null)
strategy:
fail-fast: false
- matrix: ${{ fromJson(needs.initial-mvn-install.outputs.matrix) }}
+ matrix: ${{
fromJson(needs.initial-mvn-install.outputs.integration-tests-matrix) }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #
v7.0.0
@@ -299,36 +460,54 @@ jobs:
download-maven-repo: 'true'
- name: Integration Tests
run: |
- for MODULE in $(yq -M -N e ".${{ matrix.category }}"
tooling/scripts/test-categories.yaml | grep -vE '^\s*#' | cut -f2 -d' '); do
- if [[ "${MODULE}" == "null" ]]; then
- continue
- fi
+ # Check if this is incremental build (modules) or full build
(category)
+ if [ -n "${{ matrix.modules }}" ]; then
+ echo "Incremental build - running modules: ${{ matrix.modules }}"
+ # Split comma-separated modules
+ IFS=',' read -ra MODULE_LIST <<< "${{ matrix.modules }}"
+ for MODULE_NAME in "${MODULE_LIST[@]}"; do
+ MODULE="integration-tests/${MODULE_NAME}"
- MODULE="integration-tests/$(echo ${MODULE} | sed 's/^[ \t]*//;s/[
\t]*$//')"
+ if [[ "x$(./mvnw
org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate
-Dexpression=ci.native.tests.skip -DforceStdout -q -f ${MODULE})" == "xtrue"
]]; then
+ JVM_MODULES+=("${MODULE}")
+ else
+ NATIVE_MODULES+=("${MODULE}")
+ fi
+ done
+ else
+ echo "Full build - running category: ${{ matrix.category }}"
+ # Original category-based logic
+ for MODULE in $(yq -M -N e ".${{ matrix.category }}"
tooling/scripts/test-categories.yaml | grep -vE '^\s*#' | cut -f2 -d' '); do
+ if [[ "${MODULE}" == "null" ]]; then
+ continue
+ fi
- if [[ "x$(./mvnw
org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate
-Dexpression=ci.native.tests.skip -DforceStdout -q -f ${MODULE})" == "xtrue"
]]; then
- JVM_MODULES+=("${MODULE}")
- else
- NATIVE_MODULES+=("${MODULE}")
- fi
- done
+ MODULE="integration-tests/$(echo ${MODULE} | sed 's/^[
\t]*//;s/[ \t]*$//')"
+
+ if [[ "x$(./mvnw
org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate
-Dexpression=ci.native.tests.skip -DforceStdout -q -f ${MODULE})" == "xtrue"
]]; then
+ JVM_MODULES+=("${MODULE}")
+ else
+ NATIVE_MODULES+=("${MODULE}")
+ fi
+ done
+ fi
if [[ ${#JVM_MODULES[@]} -eq 0 ]] && [[ ${#NATIVE_MODULES[@]} -eq 0
]]; then
- echo "No test modules were found for category ${{ matrix.category
}}"
+ echo "No test modules were found"
exit 1
fi
IFS=,
if [[ ${JVM_MODULES[@]} ]]; then
eval ./mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} clean test \
- -Dformatter.skip -Dimpsort.skip -Denforcer.skip \
+ ${CQ_MAVEN_SKIP_CHECKS} \
-Pdocker,ci \
-pl "${JVM_MODULES[*]}"
fi
if [[ ${NATIVE_MODULES[@]} ]]; then
eval ./mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} clean verify \
- -Dformatter.skip -Dimpsort.skip -Denforcer.skip \
+ ${CQ_MAVEN_SKIP_CHECKS} \
-Dquarkus.native.builder-image.pull=missing \
-Pnative,docker,ci \
--fail-at-end \
@@ -347,8 +526,16 @@ jobs:
functional-extension-tests:
runs-on: ubuntu-latest
needs: initial-mvn-install
+ if: >-
+ (github.event_name != 'pull_request' ||
!contains(github.event.pull_request.labels.*.name, 'JVM'))
+ && (needs.initial-mvn-install.outputs.run-extensions-core-tests == 'true'
+ || needs.initial-mvn-install.outputs.run-extensions-tests == 'true'
+ || needs.initial-mvn-install.outputs.run-test-framework-tests == 'true'
+ || needs.initial-mvn-install.outputs.run-tooling-tests == 'true'
+ || needs.initial-mvn-install.outputs.run-catalog-tests == 'true')
env:
MAVEN_OPTS: -Xmx3000m
+ SCALPEL_TRIM_ARGS: "-Dscalpel.enabled=true -Dscalpel.mode=trim
-Dscalpel.alsoMake=true -Dscalpel.alsoMakeDependents=false"
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #
v7.0.0
@@ -366,10 +553,14 @@ jobs:
cache-key: ${{ needs.initial-mvn-install.outputs.cache-key }}
download-maven-repo: 'true'
- name: cd extensions-core && mvn test
+ if: needs.initial-mvn-install.outputs.run-extensions-core-tests ==
'true'
run: |
cd extensions-core
../mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} \
- -Dformatter.skip -Dimpsort.skip -Denforcer.skip
-Dcamel-quarkus.update-extension-doc-page.skip \
+ ${CQ_MAVEN_SKIP_CHECKS} \
+ ${SCALPEL_TRIM_ARGS} \
+ ${SCALPEL_FULL_BUILD_TRIGGERS} \
+ ${SCALPEL_EXCLUDE_PATHS} \
--fail-at-end \
test
- name: Report test failures
@@ -378,10 +569,14 @@ jobs:
with:
test-report-xml-base-dir: extensions-core
- name: cd extensions && mvn test
+ if: needs.initial-mvn-install.outputs.run-extensions-tests == 'true'
run: |
cd extensions
../mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} \
- -Dformatter.skip -Dimpsort.skip -Denforcer.skip
-Dcamel-quarkus.update-extension-doc-page.skip \
+ ${CQ_MAVEN_SKIP_CHECKS} \
+ ${SCALPEL_TRIM_ARGS} \
+ ${SCALPEL_FULL_BUILD_TRIGGERS} \
+ ${SCALPEL_EXCLUDE_PATHS} \
--fail-at-end \
test
- name: Report test failures
@@ -390,10 +585,14 @@ jobs:
with:
test-report-xml-base-dir: extensions
- name: cd test-framework && mvn test
+ if: needs.initial-mvn-install.outputs.run-test-framework-tests ==
'true'
run: |
cd test-framework
../mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} \
- -Dformatter.skip -Dimpsort.skip -Denforcer.skip
-Dcamel-quarkus.update-extension-doc-page.skip \
+ ${CQ_MAVEN_SKIP_CHECKS} \
+ ${SCALPEL_TRIM_ARGS} \
+ ${SCALPEL_FULL_BUILD_TRIGGERS} \
+ ${SCALPEL_EXCLUDE_PATHS} \
--fail-at-end \
test
- name: Report test failures
@@ -402,10 +601,14 @@ jobs:
with:
test-report-xml-base-dir: test-framework
- name: cd tooling && mvn verify
+ if: needs.initial-mvn-install.outputs.run-tooling-tests == 'true'
run: |
cd tooling
../mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} \
- -Dformatter.skip -Dimpsort.skip -Denforcer.skip \
+ ${CQ_MAVEN_SKIP_CHECKS} \
+ ${SCALPEL_TRIM_ARGS} \
+ ${SCALPEL_FULL_BUILD_TRIGGERS} \
+ ${SCALPEL_EXCLUDE_PATHS} \
--fail-at-end \
verify
- name: Report test failures
@@ -414,10 +617,14 @@ jobs:
with:
test-report-xml-base-dir: tooling
- name: cd catalog && mvn test
+ if: needs.initial-mvn-install.outputs.run-catalog-tests == 'true'
run: |
cd catalog
../mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} \
- -Dformatter.skip -Dimpsort.skip -Denforcer.skip \
+ ${CQ_MAVEN_SKIP_CHECKS} \
+ ${SCALPEL_TRIM_ARGS} \
+ ${SCALPEL_FULL_BUILD_TRIGGERS} \
+ ${SCALPEL_EXCLUDE_PATHS} \
test
- name: Report test failures
uses: ./.github/actions/test-summary-report
@@ -428,6 +635,7 @@ jobs:
integration-tests-jvm:
runs-on: ubuntu-latest
needs: initial-mvn-install
+ if: needs.initial-mvn-install.outputs.run-integration-tests-jvm == 'true'
env:
MAVEN_OPTS: -Xmx3000m
steps:
@@ -449,8 +657,19 @@ jobs:
- name: cd integration-tests-jvm && mvn clean test
run: |
cd integration-tests-jvm
+ JVM_MODULES="${{
needs.initial-mvn-install.outputs.integration-tests-jvm-modules }}"
+
+ if [ -n "$JVM_MODULES" ]; then
+ echo "Running incremental JVM tests for: $JVM_MODULES"
+ PL_ARGS="-pl $JVM_MODULES"
+ else
+ echo "Running all JVM tests"
+ PL_ARGS=""
+ fi
+
../mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} \
- -Dformatter.skip -Dimpsort.skip -Denforcer.skip \
+ $PL_ARGS \
+ ${CQ_MAVEN_SKIP_CHECKS} \
--fail-at-end \
clean test
- name: Report test failures
@@ -466,7 +685,9 @@ jobs:
fail-fast: false
matrix:
os: [ 'windows-latest' ]
- if: github.event_name != 'pull_request' ||
!contains(github.event.pull_request.labels.*.name, 'JVM')
+ if: |
+ (github.event_name != 'pull_request' ||
!contains(github.event.pull_request.labels.*.name, 'JVM'))
+ && (needs.initial-mvn-install.outputs.integration-tests-modules == '' ||
fromJson(needs.initial-mvn-install.outputs.integration-tests-modules).totalModules
> 0)
env:
MAVEN_OPTS: -Xmx3000m -Xms3000m
steps:
@@ -489,10 +710,23 @@ jobs:
shell: bash
run: |
cd integration-tests
- ../mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} \
- -Dskip-testcontainers-tests -Dformatter.skip -Dimpsort.skip
-Denforcer.skip \
- --fail-at-end \
- clean verify
+ if [ -n "${{
needs.initial-mvn-install.outputs.integration-tests-modules }}" ]; then
+ # Incremental build - use affected modules from filtered matrix
+ AFFECTED_MODULES=$(echo '${{
needs.initial-mvn-install.outputs.integration-tests-modules }}' | jq -r
'.modulesCsv')
+ echo "Running incremental build with affected modules:
$AFFECTED_MODULES"
+ ../mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} \
+ -pl "$AFFECTED_MODULES" \
+ -Dskip-testcontainers-tests ${CQ_MAVEN_SKIP_CHECKS} \
+ --fail-at-end \
+ clean verify
+ else
+ # Full build - run all modules
+ echo "Running full build - all modules"
+ ../mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS} \
+ -Dskip-testcontainers-tests ${CQ_MAVEN_SKIP_CHECKS} \
+ --fail-at-end \
+ clean verify
+ fi
- name: Report test failures
uses: ./.github/actions/test-summary-report
if: ${{ failure() }}
@@ -503,7 +737,9 @@ jobs:
name: Examples Tests - ${{matrix.name}}
needs: initial-mvn-install
runs-on: ubuntu-latest
- if: github.event_name != 'pull_request' ||
!contains(github.event.pull_request.labels.*.name, 'JVM')
+ if: |
+ (github.event_name != 'pull_request' ||
!contains(github.event.pull_request.labels.*.name, 'JVM'))
+ && needs.initial-mvn-install.outputs.run-examples == 'true'
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.initial-mvn-install.outputs.examples-matrix)
}}
@@ -540,13 +776,79 @@ jobs:
&& echo "Current Examples commit:" $(git rev-parse HEAD) \
&& ./mvnw ${CQ_MAVEN_ARGS} ${BRANCH_OPTIONS}
org.l2x6.cq:cq-maven-plugin:2.10.0:examples-set-platform
-Dcq.camel-quarkus.version=${CQ_VERSION}
+ # Filter examples for incremental builds
+ MODULES_TO_BUILD="${EXAMPLE_MODULES//,/ }"
+
+ if [ -n "${{
needs.initial-mvn-install.outputs.integration-tests-modules }}" ]; then
+ echo "Incremental build detected - filtering examples based on
affected extensions"
+
+ # Get affected modules from filtered matrix
+ # Get affected modules as space-separated list for iteration
+ AFFECTED_MODULES=$(echo '${{
needs.initial-mvn-install.outputs.integration-tests-modules }}' | jq -r
'.modulesCsv' | tr ',' ' ')
+ echo "Affected modules: $AFFECTED_MODULES"
+
+ # Check if extensions-core/core is affected (build all examples if
so)
+ if echo "$AFFECTED_MODULES" | grep -q "^core$"; then
+ echo "Core extension affected - building all examples"
+ else
+ # Filter examples based on dependency analysis
+ echo "Affected modules for filtering: $AFFECTED_MODULES"
+ FILTERED_MODULES=()
+
+ for MODULE in ${EXAMPLE_MODULES//,/ }; do
+ echo "Analyzing dependencies for example: $MODULE"
+ cd ${MODULE}
+
+ # Use quarkus:dependency-list which works per-module (root
pom.xml is just an aggregator)
+ echo " Running: ../mvnw quarkus:dependency-list
-DoutputFile=target/deps.txt -q"
+ ../mvnw ${CQ_MAVEN_ARGS} quarkus:dependency-list
-DoutputFile=target/deps.txt -q 2>&1 || true
+
+ if [ -f target/deps.txt ]; then
+ echo " ✓ deps.txt created ($(wc -l < target/deps.txt)
lines)"
+ # Check if any affected extension appears in dependencies
+ SHOULD_BUILD=false
+ for AFFECTED in $AFFECTED_MODULES; do
+ # Convert module name to artifact ID (e.g., "box" ->
"camel-quarkus-box")
+ ARTIFACT_ID="camel-quarkus-${AFFECTED}"
+ # Use word boundary matching to avoid partial matches
(e.g., box matching dropbox)
+ if grep -qw "$ARTIFACT_ID" target/deps.txt; then
+ echo " → Depends on affected extension: $ARTIFACT_ID"
+ SHOULD_BUILD=true
+ break
+ fi
+ done
+
+ if [ "$SHOULD_BUILD" == "true" ]; then
+ FILTERED_MODULES+=("$MODULE")
+ else
+ echo " → No affected dependencies - skipping"
+ fi
+ else
+ echo " → Could not determine dependencies - including for
safety"
+ FILTERED_MODULES+=("$MODULE")
+ fi
+
+ cd -
+ done
+
+ if [ ${#FILTERED_MODULES[@]} -eq 0 ]; then
+ echo "No examples depend on affected extensions - skipping all"
+ exit 0
+ fi
+
+ MODULES_TO_BUILD="${FILTERED_MODULES[@]}"
+ echo "Filtered examples to build: $MODULES_TO_BUILD"
+ fi
+ fi
+
BUILD_FAILURES=()
- for MODULE in ${EXAMPLE_MODULES//,/ }; do
+ for MODULE in $MODULES_TO_BUILD; do
+ echo "Building example: $MODULE"
cd ${MODULE}
../mvnw ${CQ_MAVEN_ARGS} clean verify \
- -Dformatter.skip -Dimpsort.skip \
+ ${CQ_MAVEN_SKIP_CHECKS} \
-Dquarkus.native.builder-image.pull=missing \
-Pnative,docker,ci
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
new file mode 100644
index 0000000000..b70339124d
--- /dev/null
+++ b/.mvn/extensions.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<extensions>
+ <extension>
+ <groupId>eu.maveniverse.maven.scalpel</groupId>
+ <artifactId>extension</artifactId>
+ <version>0.3.5</version>
+ </extension>
+</extensions>
diff --git a/.mvn/maven.config b/.mvn/maven.config
index 7408098dbe..9b174c4c84 100644
--- a/.mvn/maven.config
+++ b/.mvn/maven.config
@@ -1,2 +1,5 @@
-Daether.remoteRepositoryFilter.groupId=true
-Daether.remoteRepositoryFilter.groupId.basedir=${session.rootDirectory}/.mvn/rrf/
+
+# Scalpel is disabled by default, only enabled in specific CI steps
+-Dscalpel.enabled=false
diff --git
a/tooling/maven-plugin/src/main/java/org/apache/camel/quarkus/maven/IncrementalBuildMojo.java
b/tooling/maven-plugin/src/main/java/org/apache/camel/quarkus/maven/IncrementalBuildMojo.java
new file mode 100644
index 0000000000..ae1d58a6a2
--- /dev/null
+++
b/tooling/maven-plugin/src/main/java/org/apache/camel/quarkus/maven/IncrementalBuildMojo.java
@@ -0,0 +1,701 @@
+/*
+ * 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.camel.quarkus.maven;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+/**
+ * Unified mojo for incremental build analysis and matrix generation.
+ * <p>
+ * Supports multiple actions via the {@code -Dcq.action} parameter:
+ * <ul>
+ * <li>{@code analyze} - Performs all analysis operations and outputs
comprehensive JSON (recommended)</li>
+ * <li>{@code filter-modules} - Extracts affected modules from Scalpel
report</li>
+ * <li>{@code native-matrix} - Generates native test matrix with balanced
distribution</li>
+ * <li>{@code alternate-jvm-matrix} - Generates alternate JVM test matrix</li>
+ * <li>{@code functional-scope} - Detects which functional test scopes are
affected</li>
+ * <li>{@code jvm-tests} - Detects affected JVM-only test modules</li>
+ * </ul>
+ * <p>
+ * Usage:
+ *
+ * <pre>
+ * mvn org.apache.camel.quarkus:camel-quarkus-maven-plugin:incremental-build \
+ * -Dcq.action=analyze \
+ * -Dcq.useIncrementalBuild=true \
+ * -N
+ * </pre>
+ */
+@Mojo(name = "incremental-build", threadSafe = true, requiresProject = false)
+public class IncrementalBuildMojo extends AbstractMojo {
+
+ private static final TypeReference<Map<String, Object>> JSON_TYPE_REF =
new TypeReference<>() {
+ };
+
+ /**
+ * Action to perform. Supported values:
+ * <ul>
+ * <li>analyze - Full analysis (all operations)</li>
+ * <li>filter-modules - Extract affected modules</li>
+ * <li>native-matrix - Generate native test matrix</li>
+ * <li>alternate-jvm-matrix - Generate alternate JVM matrix</li>
+ * <li>functional-scope - Detect functional test scope</li>
+ * <li>jvm-tests - Detect JVM-only tests</li>
+ * </ul>
+ */
+ @Parameter(property = "cq.action", required = true)
+ String action;
+
+ /**
+ * Path to Scalpel's JSON report file
+ */
+ @Parameter(defaultValue =
"${maven.multiModuleProjectDirectory}/target/scalpel-report.json", property =
"cq.scalpelReportJson")
+ Path scalpelReportJson;
+
+ /**
+ * Path to test-categories.yaml file (for full builds)
+ */
+ @Parameter(defaultValue =
"${maven.multiModuleProjectDirectory}/tooling/scripts/test-categories.yaml",
property = "cq.testCategoriesFile")
+ Path testCategoriesFile;
+
+ /**
+ * Path to write the output JSON file
+ */
+ @Parameter(defaultValue =
"${maven.multiModuleProjectDirectory}/target/incremental-build.json", property
= "cq.outputFile")
+ Path outputFile;
+
+ /**
+ * Whether to use incremental build (true) or full build (false)
+ */
+ @Parameter(property = "cq.useIncrementalBuild", defaultValue = "false")
+ boolean useIncrementalBuild;
+
+ /**
+ * Maximum number of groups for native test matrix distribution
+ */
+ @Parameter(property = "cq.maxGroups", defaultValue = "13")
+ int maxGroups;
+
+ /**
+ * Maximum allowed matrix size (validation)
+ */
+ @Parameter(property = "cq.maxMatrixSize", defaultValue = "20")
+ int maxMatrixSize;
+
+ /**
+ * Output compact JSON (single line)
+ */
+ @Parameter(property = "cq.outputCompact", defaultValue = "true")
+ boolean outputCompact;
+
+ /**
+ * Comma-separated list of extension directory prefixes to detect
extension changes.
+ * Default: extensions/,extensions-jvm/,extensions-core/
+ */
+ @Parameter(property = "cq.extensionDirs", defaultValue =
"extensions/,extensions-jvm/,extensions-core/")
+ String extensionDirs;
+
+ /**
+ * Comma-separated list of integration test directory prefixes.
+ * Default: integration-tests/,integration-tests-jvm/
+ */
+ @Parameter(property = "cq.integrationTestDirs", defaultValue =
"integration-tests/,integration-tests-jvm/")
+ String integrationTestDirs;
+
+ /**
+ * Prefix for native-supported integration tests (used for filtering).
+ * Default: integration-tests/
+ */
+ @Parameter(property = "cq.nativeTestsPrefix", defaultValue =
"integration-tests/")
+ String nativeTestsPrefix;
+
+ /**
+ * Prefix for JVM-only integration tests.
+ * Default: integration-tests-jvm/
+ */
+ @Parameter(property = "cq.jvmTestsPrefix", defaultValue =
"integration-tests-jvm/")
+ String jvmTestsPrefix;
+
+ /**
+ * Prefix for grouped integration tests.
+ * Path structure: integration-test-groups/<group>/<module>/
+ * Default: integration-test-groups/
+ */
+ @Parameter(property = "cq.integrationTestGroupsPrefix", defaultValue =
"integration-test-groups/")
+ String integrationTestGroupsPrefix;
+
+ /**
+ * Comma-separated list of directory prefixes for functional test scope
detection.
+ * Format: prefix:scopeName
+ * Default:
+ *
extensions-core/:runExtensionsCoreTests,extensions/:runExtensionsTests,test-framework/:runTestFrameworkTests,tooling/:runToolingTests,catalog/:runCatalogTests
+ */
+ @Parameter(property = "cq.functionalScopeDirs", defaultValue =
"extensions-core/:runExtensionsCoreTests,extensions/:runExtensionsTests,test-framework/:runTestFrameworkTests,tooling/:runToolingTests,catalog/:runCatalogTests")
+ String functionalScopeDirs;
+
+ /**
+ * Prefix for shared integration test support modules.
+ * Default: integration-tests-support/
+ */
+ @Parameter(property = "cq.integrationTestSupportPrefix", defaultValue =
"integration-tests-support/")
+ String integrationTestSupportPrefix;
+
+ /**
+ * Comma-separated list of changed container image property names (e.g.
+ * {@code kafka.container.image,mysql.container.image}).
+ * When set, TestResource.java files are scanned to find which test
modules reference these
+ * properties, and those modules are added to the affected set.
+ */
+ @Parameter(property = "cq.changedContainerProperties")
+ String changedContainerProperties;
+
+ /**
+ * Project root directory used for scanning TestResource files.
+ */
+ @Parameter(defaultValue = "${maven.multiModuleProjectDirectory}", property
= "cq.projectRootDir")
+ Path projectRootDir;
+
+ private final ObjectMapper jsonMapper = new ObjectMapper();
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ try {
+ Map<String, Object> result;
+
+ switch (action) {
+ case "analyze":
+ result = performFullAnalysis();
+ break;
+ case "filter-modules":
+ result = filterModules();
+ break;
+ case "native-matrix": {
+ ScalpelReport report = readScalpelReport();
+ ContainerAffectedModules containerModules =
detectContainerAffectedModules();
+ List<String> modules = (List<String>) filterModules(report,
containerModules).get("modules");
+ result = generateNativeMatrix(modules);
+ break;
+ }
+ case "functional-scope":
+ result = detectFunctionalScope(readScalpelReport());
+ break;
+ case "jvm-tests":
+ result = detectJvmTests(readScalpelReport(),
detectContainerAffectedModules());
+ break;
+ default:
+ throw new MojoExecutionException("Unknown action: " + action +
". Supported: analyze, filter-modules, "
+ + "native-matrix, alternate-jvm-matrix,
functional-scope, jvm-tests");
+ }
+
+ writeOutput(result);
+ getLog().info("Incremental build analysis complete (action=" +
action + ")");
+
+ } catch (Exception e) {
+ throw new MojoExecutionException("Failed to execute incremental
build analysis", e);
+ }
+ }
+
+ private Map<String, Object> performFullAnalysis() throws IOException,
MojoExecutionException {
+ Map<String, Object> result = new LinkedHashMap<>();
+
+ ScalpelReport report = readScalpelReport();
+ ContainerAffectedModules containerModules =
detectContainerAffectedModules();
+
+ Map<String, Object> moduleData = filterModules(report,
containerModules);
+ result.put("incrementalBuild", moduleData.get("incrementalBuild"));
+ result.put("affectedModulesCount", moduleData.get("totalModules"));
+ result.put("affectedModules", moduleData.get("modules"));
+
+ List<String> modules = (List<String>) moduleData.get("modules");
+ result.put("nativeTestMatrix", generateNativeMatrix(modules));
+ result.put("functionalTestScope", detectFunctionalScope(report));
+ result.put("integrationTestsJvm", detectJvmTests(report,
containerModules));
+ result.put("runExamples", shouldRunExamples(report));
+
+ return result;
+ }
+
+ private ScalpelReport readScalpelReport() throws IOException {
+ if (!useIncrementalBuild || !Files.exists(scalpelReportJson)) {
+ return null;
+ }
+ Map<String, Object> raw =
jsonMapper.readValue(scalpelReportJson.toFile(), JSON_TYPE_REF);
+ return new ScalpelReport(
+ Boolean.TRUE.equals(raw.get("fullBuildTriggered")),
+ (List<Map<String, Object>>) raw.get("affectedModules"));
+ }
+
+ private static class ScalpelReport {
+ final boolean fullBuildTriggered;
+ final List<Map<String, Object>> affectedModules;
+
+ ScalpelReport(boolean fullBuildTriggered, List<Map<String, Object>>
affectedModules) {
+ this.fullBuildTriggered = fullBuildTriggered;
+ this.affectedModules = affectedModules != null ? affectedModules :
List.of();
+ }
+ }
+
+ private Map<String, Object> filterModules() throws IOException {
+ return filterModules(readScalpelReport(),
detectContainerAffectedModules());
+ }
+
+ private Map<String, Object> filterModules(ScalpelReport report,
ContainerAffectedModules containerModules) {
+ Map<String, Object> result = new LinkedHashMap<>();
+
+ if (report == null) {
+ getLog().info("Full build mode (useIncrementalBuild=" +
useIncrementalBuild + ")");
+ result.put("incrementalBuild", false);
+ result.put("modules", new ArrayList<>());
+ result.put("totalModules", 0);
+ return result;
+ }
+
+ if (report.fullBuildTriggered) {
+ getLog().info("Full build triggered by Scalpel");
+ result.put("incrementalBuild", false);
+ result.put("modules", new ArrayList<>());
+ result.put("totalModules", 0);
+ return result;
+ }
+
+ if (report.affectedModules.isEmpty() &&
containerModules.nativeModules.isEmpty()
+ && containerModules.jvmModules.isEmpty()) {
+ getLog().info("No affected modules - using full build for safety");
+ result.put("incrementalBuild", false);
+ result.put("modules", new ArrayList<>());
+ result.put("totalModules", 0);
+ return result;
+ }
+
+ Set<String> affectedTests =
extractAffectedTests(report.affectedModules);
+ affectedTests.addAll(containerModules.nativeModules);
+
+ result.put("incrementalBuild", true);
+ result.put("modules", new ArrayList<>(affectedTests));
+ result.put("totalModules", affectedTests.size());
+
+ getLog().info("Incremental build: " + affectedTests.size() + "
affected modules");
+ return result;
+ }
+
+ /**
+ * Extracts affected integration test modules from Scalpel report.
+ * Includes both DIRECT and DOWNSTREAM changes - if Scalpel reports it as
affected,
+ * we should test it.
+ */
+ private Set<String> extractAffectedTests(List<Map<String, Object>>
affectedModules) {
+ Set<String> affectedTests = new LinkedHashSet<>();
+
+ for (Map<String, Object> module : affectedModules) {
+ String path = (String) module.get("path");
+ String category = (String) module.get("category");
+
+ // Handle integration-test-groups:
integration-test-groups/<group>/... -> <group>-grouped
+ if (path != null && path.startsWith(integrationTestGroupsPrefix)) {
+ // Extract group name from: integration-test-groups/<group>/...
+ String remainder =
path.substring(integrationTestGroupsPrefix.length());
+ String[] parts = remainder.split("/");
+ if (parts.length >= 1) {
+ String groupName = parts[0]; // Get the group name
+ String groupedModuleName = groupName + "-grouped";
+ affectedTests.add(groupedModuleName);
+ getLog().debug("Including grouped test: " +
groupedModuleName + " (category: " + category + ")");
+ }
+ continue;
+ }
+
+ // Handle regular integration-tests
+ if (path != null && path.startsWith(nativeTestsPrefix)) {
+ // Include both DIRECT and DOWNSTREAM - if Scalpel detected
it, test it
+ if ("DIRECT".equals(category) ||
"DOWNSTREAM".equals(category)) {
+ // Extract test name: integration-tests/box -> box
+ String testName =
path.substring(nativeTestsPrefix.length());
+ // Remove any trailing path components
+ if (testName.contains("/")) {
+ testName = testName.substring(0,
testName.indexOf("/"));
+ }
+ affectedTests.add(testName);
+ getLog().debug("Including test: " + testName + "
(category: " + category + ")");
+ }
+ }
+ }
+
+ return affectedTests;
+ }
+
+ private static class ContainerAffectedModules {
+ final Set<String> nativeModules = new LinkedHashSet<>();
+ final Set<String> jvmModules = new LinkedHashSet<>();
+ }
+
+ /**
+ * Scans TestResource.java files for references to changed container image
properties
+ * and returns the affected module names, split by test type.
+ * <p>
+ * Handles two cases:
+ * <ul>
+ * <li>Direct references in test modules (integration-tests/,
integration-tests-jvm/, integration-test-groups/)</li>
+ * <li>References in shared support modules (integration-tests-support/) —
resolved by finding which test
+ * modules depend on the affected support module via POM dependency
grep</li>
+ * </ul>
+ */
+ private ContainerAffectedModules detectContainerAffectedModules() throws
IOException {
+ ContainerAffectedModules result = new ContainerAffectedModules();
+
+ if (changedContainerProperties == null ||
changedContainerProperties.isBlank()) {
+ return result;
+ }
+
+ Set<String> changedProps = new LinkedHashSet<>();
+ for (String prop : changedContainerProperties.split(",")) {
+ String trimmed = prop.trim();
+ if (!trimmed.isEmpty()) {
+ changedProps.add(trimmed);
+ }
+ }
+
+ if (changedProps.isEmpty()) {
+ return result;
+ }
+
+ getLog().info("Scanning for changed container properties: " +
changedProps);
+
+ // Scan integration-tests/ and integration-tests-jvm/ for direct
references
+ for (String prefix : integrationTestDirs.split(",")) {
+ String trimmedPrefix = prefix.trim();
+ Path dir = projectRootDir.resolve(trimmedPrefix);
+ if (!Files.isDirectory(dir)) {
+ continue;
+ }
+
+ boolean isJvm = trimmedPrefix.equals(jvmTestsPrefix.trim());
+ scanTestResourceFiles(dir, changedProps, (moduleName) -> {
+ if (isJvm) {
+ result.jvmModules.add(moduleName);
+ } else {
+ result.nativeModules.add(moduleName);
+ }
+ });
+ }
+
+ // Scan integration-test-groups/ for direct references
+ Path groupsDir =
projectRootDir.resolve(integrationTestGroupsPrefix.trim());
+ if (Files.isDirectory(groupsDir)) {
+ scanTestResourceFiles(groupsDir, changedProps, (moduleName) -> {
+ result.nativeModules.add(moduleName + "-grouped");
+ });
+ }
+
+ // Scan integration-tests-support/ for references in shared modules
+ Path supportDir =
projectRootDir.resolve(integrationTestSupportPrefix.trim());
+ if (Files.isDirectory(supportDir)) {
+ Set<String> affectedSupportModules = new LinkedHashSet<>();
+ scanTestResourceFiles(supportDir, changedProps,
affectedSupportModules::add);
+
+ if (!affectedSupportModules.isEmpty()) {
+ getLog().info("Container properties referenced in support
modules: " + affectedSupportModules);
+ resolveSupportModuleDependents(affectedSupportModules, result);
+ }
+ }
+
+ if (!result.nativeModules.isEmpty()) {
+ getLog().info("Container property changes affect native test
modules: " + result.nativeModules);
+ }
+ if (!result.jvmModules.isEmpty()) {
+ getLog().info("Container property changes affect JVM test modules:
" + result.jvmModules);
+ }
+
+ return result;
+ }
+
+ @FunctionalInterface
+ private interface ModuleConsumer {
+ void accept(String moduleName);
+ }
+
+ /**
+ * Walks a directory for TestResource.java files containing any of the
given property names.
+ * For each match, extracts the top-level module name and passes it to the
consumer.
+ */
+ private void scanTestResourceFiles(Path dir, Set<String> changedProps,
ModuleConsumer consumer) throws IOException {
+ try (Stream<Path> paths = Files.walk(dir)) {
+ paths.filter(p ->
p.getFileName().toString().endsWith("TestResource.java"))
+ .forEach(file -> {
+ try {
+ String content = Files.readString(file,
StandardCharsets.UTF_8);
+ for (String prop : changedProps) {
+ if (content.contains("\"" + prop + "\"")) {
+ Path relative = dir.relativize(file);
+ String moduleName =
relative.getName(0).toString();
+ consumer.accept(moduleName);
+ break;
+ }
+ }
+ } catch (IOException e) {
+ getLog().warn("Failed to read " + file + ": " +
e.getMessage());
+ }
+ });
+ }
+ }
+
+ /**
+ * For each affected support module, finds test modules that depend on it
by grepping
+ * their pom.xml files for the support module's artifactId.
+ */
+ private void resolveSupportModuleDependents(Set<String> supportModules,
ContainerAffectedModules result)
+ throws IOException {
+
+ for (String supportModule : supportModules) {
+ String artifactId = "camel-quarkus-integration-tests-support-" +
supportModule;
+ getLog().info("Resolving dependents of " + artifactId);
+
+ // Search integration-tests/*/pom.xml
+ for (String prefix : integrationTestDirs.split(",")) {
+ String trimmedPrefix = prefix.trim();
+ Path dir = projectRootDir.resolve(trimmedPrefix);
+ if (!Files.isDirectory(dir)) {
+ continue;
+ }
+
+ boolean isJvm = trimmedPrefix.equals(jvmTestsPrefix.trim());
+ findDependentModules(dir, artifactId, (moduleName) -> {
+ if (isJvm) {
+ result.jvmModules.add(moduleName);
+ } else {
+ result.nativeModules.add(moduleName);
+ }
+ });
+ }
+
+ // Search integration-test-groups/*/*/pom.xml
+ Path groupsDir =
projectRootDir.resolve(integrationTestGroupsPrefix.trim());
+ if (Files.isDirectory(groupsDir)) {
+ findDependentModules(groupsDir, artifactId, (moduleName) -> {
+ result.nativeModules.add(moduleName + "-grouped");
+ });
+ }
+ }
+ }
+
+ /**
+ * Finds modules under a directory whose pom.xml contains a dependency on
the given artifactId.
+ */
+ private void findDependentModules(Path dir, String artifactId,
ModuleConsumer consumer) throws IOException {
+ try (Stream<Path> paths = Files.walk(dir)) {
+ paths.filter(p -> p.getFileName().toString().equals("pom.xml"))
+ .forEach(pomFile -> {
+ try {
+ String content = Files.readString(pomFile,
StandardCharsets.UTF_8);
+ if (content.contains(artifactId)) {
+ Path relative = dir.relativize(pomFile);
+ String moduleName =
relative.getName(0).toString();
+ consumer.accept(moduleName);
+ getLog().debug(" " + moduleName + " depends
on " + artifactId);
+ }
+ } catch (IOException e) {
+ getLog().warn("Failed to read " + pomFile + ": " +
e.getMessage());
+ }
+ });
+ }
+ }
+
+ private Map<String, Object> generateNativeMatrix(List<String> modules)
throws MojoExecutionException {
+ Map<String, Object> result = new LinkedHashMap<>();
+
+ if (modules == null || modules.isEmpty()) {
+ result.put("include", new ArrayList<>());
+ return result;
+ }
+
+ // Distribute modules across balanced groups
+ int moduleCount = modules.size();
+ int groups = Math.min(moduleCount, maxGroups);
+ int modulesPerGroup = (int) Math.ceil((double) moduleCount / groups);
+
+ List<Map<String, String>> include = new ArrayList<>();
+ for (int i = 0; i < groups; i++) {
+ int start = i * modulesPerGroup;
+ int end = Math.min(start + modulesPerGroup, moduleCount);
+
+ if (start < moduleCount) {
+ List<String> groupModules = modules.subList(start, end);
+ Map<String, String> group = new LinkedHashMap<>();
+ group.put("name", String.format("group-%02d", i + 1));
+ group.put("modules", String.join(",", groupModules));
+ include.add(group);
+ }
+ }
+
+ // Validate matrix size
+ if (include.size() > maxMatrixSize) {
+ throw new MojoExecutionException(
+ "Native test matrix size (" + include.size() + ") exceeds
maximum (" + maxMatrixSize + ")");
+ }
+
+ result.put("include", include);
+ getLog().info("Native test matrix: " + include.size() + " groups for "
+ moduleCount + " modules");
+ return result;
+ }
+
+ private Map<String, Object> detectFunctionalScope(ScalpelReport report) {
+ Map<String, String> prefixToScope = new LinkedHashMap<>();
+ Map<String, Boolean> scope = new LinkedHashMap<>();
+
+ for (String entry : functionalScopeDirs.split(",")) {
+ String[] parts = entry.trim().split(":");
+ if (parts.length == 2) {
+ String prefix = parts[0].trim();
+ String scopeName = parts[1].trim();
+ prefixToScope.put(prefix, scopeName);
+ scope.put(scopeName, false);
+ }
+ }
+
+ if (report == null) {
+ scope.replaceAll((k, v) -> true);
+ return new LinkedHashMap<>(scope);
+ }
+
+ for (Map<String, Object> module : report.affectedModules) {
+ String category = (String) module.get("category");
+ if (!"DIRECT".equals(category)) {
+ continue;
+ }
+
+ String path = (String) module.get("path");
+ if (path == null) {
+ continue;
+ }
+
+ // Check each prefix and set corresponding scope flag
+ for (Map.Entry<String, String> entry : prefixToScope.entrySet()) {
+ if (path.startsWith(entry.getKey())) {
+ scope.put(entry.getValue(), true);
+ }
+ }
+ }
+
+ getLog().info("Functional test scope: " + scope);
+
+ return new LinkedHashMap<>(scope);
+ }
+
+ private Map<String, Object> detectJvmTests(ScalpelReport report,
ContainerAffectedModules containerModules) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("runTests", false);
+ result.put("modules", "");
+
+ if (report == null) {
+ result.put("runTests", true);
+ return result;
+ }
+
+ if (report.affectedModules.isEmpty()) {
+ return result;
+ }
+
+ Set<String> jvmModules = new LinkedHashSet<>();
+ for (Map<String, Object> module : report.affectedModules) {
+ String path = (String) module.get("path");
+ if (path != null && path.startsWith(jvmTestsPrefix)) {
+ String moduleName = path.substring(jvmTestsPrefix.length());
+ if (moduleName.contains("/")) {
+ moduleName = moduleName.substring(0,
moduleName.indexOf("/"));
+ }
+ jvmModules.add(moduleName);
+ }
+ }
+
+ jvmModules.addAll(containerModules.jvmModules);
+
+ if (!jvmModules.isEmpty()) {
+ result.put("runTests", true);
+ result.put("modules", String.join(",", jvmModules));
+ getLog().info("JVM-only tests: " + jvmModules.size() + " modules
affected");
+ }
+
+ return result;
+ }
+
+ private boolean shouldRunExamples(ScalpelReport report) {
+ if (report == null || report.fullBuildTriggered ||
report.affectedModules.isEmpty()) {
+ return true;
+ }
+
+ for (Map<String, Object> module : report.affectedModules) {
+ String path = (String) module.get("path");
+ String category = (String) module.get("category");
+
+ // Only consider DIRECT changes
+ if (!"DIRECT".equals(category)) {
+ continue;
+ }
+
+ if (path != null && isExtensionPath(path)) {
+ getLog().info("Examples should run - extension affected: " +
path);
+ return true;
+ }
+ }
+
+ getLog().info("Examples will be skipped - only integration tests
affected");
+ return false;
+ }
+
+ private boolean isExtensionPath(String path) {
+ for (String prefix : extensionDirs.split(",")) {
+ if (path.startsWith(prefix.trim())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Writes output JSON to file.
+ */
+ private void writeOutput(Map<String, Object> data) throws IOException {
+ Files.createDirectories(outputFile.getParent());
+
+ String json;
+ if (outputCompact) {
+ json = jsonMapper.writeValueAsString(data);
+ } else {
+ json =
jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(data);
+ }
+
+ Files.writeString(outputFile, json);
+ getLog().debug("Written output to: " + outputFile);
+ }
+}