From f4b3cc353c913036935f948b982a557edf00a610 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 21 Aug 2026 12:24:09 +0200 Subject: [PATCH 1/8] Apply the `revision` convention to `flume-parent` Every published module inherits from `flume-parent`, not from the aggregator, so the CI-friendly properties have to be declared there as well. Without them the release automation bumps only the aggregator and the artifacts ship a stale version and build timestamp. Assisted-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011QwMh1JvFaPBgWEGrMgMj7 --- flume-ng-auth/pom.xml | 2 +- flume-ng-channels/flume-file-channel/pom.xml | 2 +- .../flume-spillable-memory-channel/pom.xml | 2 +- flume-ng-channels/pom.xml | 2 +- .../flume-ng-config-filter-api/pom.xml | 2 +- .../pom.xml | 2 +- .../flume-ng-external-process-config-filter/pom.xml | 2 +- flume-ng-configfilters/pom.xml | 2 +- flume-ng-configuration/pom.xml | 2 +- flume-ng-core/pom.xml | 2 +- flume-ng-dist/pom.xml | 2 +- .../flume-ganglia-monitor/pom.xml | 2 +- flume-ng-instrumentation/flume-http-monitor/pom.xml | 2 +- .../flume-prometheus-monitor/pom.xml | 2 +- flume-ng-instrumentation/pom.xml | 2 +- flume-ng-node/pom.xml | 2 +- flume-ng-sdk/pom.xml | 2 +- flume-ng-sources/flume-http-source/pom.xml | 2 +- flume-ng-sources/flume-netcat-source/pom.xml | 2 +- flume-ng-sources/flume-syslog-source/pom.xml | 2 +- flume-ng-sources/flume-taildir-source/pom.xml | 2 +- flume-ng-sources/pom.xml | 2 +- flume-parent/pom.xml | 13 ++++++++----- flume-tools/pom.xml | 2 +- 24 files changed, 31 insertions(+), 28 deletions(-) diff --git a/flume-ng-auth/pom.xml b/flume-ng-auth/pom.xml index e288313d1e..b13267f33e 100644 --- a/flume-ng-auth/pom.xml +++ b/flume-ng-auth/pom.xml @@ -21,7 +21,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml diff --git a/flume-ng-channels/flume-file-channel/pom.xml b/flume-ng-channels/flume-file-channel/pom.xml index 245e4d5679..6183926cb8 100644 --- a/flume-ng-channels/flume-file-channel/pom.xml +++ b/flume-ng-channels/flume-file-channel/pom.xml @@ -21,7 +21,7 @@ org.apache.flume flume-ng-channels - 2.0.0-SNAPSHOT + ${revision} org.apache.flume.flume-ng-channels diff --git a/flume-ng-channels/flume-spillable-memory-channel/pom.xml b/flume-ng-channels/flume-spillable-memory-channel/pom.xml index b891a42aae..2fc1378d11 100644 --- a/flume-ng-channels/flume-spillable-memory-channel/pom.xml +++ b/flume-ng-channels/flume-spillable-memory-channel/pom.xml @@ -21,7 +21,7 @@ org.apache.flume flume-ng-channels - 2.0.0-SNAPSHOT + ${revision} org.apache.flume.flume-ng-channels diff --git a/flume-ng-channels/pom.xml b/flume-ng-channels/pom.xml index a8aeef90ba..42343fadbe 100644 --- a/flume-ng-channels/pom.xml +++ b/flume-ng-channels/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml diff --git a/flume-ng-configfilters/flume-ng-config-filter-api/pom.xml b/flume-ng-configfilters/flume-ng-config-filter-api/pom.xml index 5f4b9cd6d9..0fa090bd43 100644 --- a/flume-ng-configfilters/flume-ng-config-filter-api/pom.xml +++ b/flume-ng-configfilters/flume-ng-config-filter-api/pom.xml @@ -20,7 +20,7 @@ org.apache.flume flume-ng-configfilters - 2.0.0-SNAPSHOT + ${revision} flume-ng-config-filter-api diff --git a/flume-ng-configfilters/flume-ng-environment-variable-config-filter/pom.xml b/flume-ng-configfilters/flume-ng-environment-variable-config-filter/pom.xml index 78c30b0c8f..f0d39f5d79 100644 --- a/flume-ng-configfilters/flume-ng-environment-variable-config-filter/pom.xml +++ b/flume-ng-configfilters/flume-ng-environment-variable-config-filter/pom.xml @@ -20,7 +20,7 @@ org.apache.flume flume-ng-configfilters - 2.0.0-SNAPSHOT + ${revision} flume-ng-environment-variable-config-filter diff --git a/flume-ng-configfilters/flume-ng-external-process-config-filter/pom.xml b/flume-ng-configfilters/flume-ng-external-process-config-filter/pom.xml index 339fdfdb21..1ae97a67e0 100644 --- a/flume-ng-configfilters/flume-ng-external-process-config-filter/pom.xml +++ b/flume-ng-configfilters/flume-ng-external-process-config-filter/pom.xml @@ -20,7 +20,7 @@ org.apache.flume flume-ng-configfilters - 2.0.0-SNAPSHOT + ${revision} flume-ng-external-process-config-filter diff --git a/flume-ng-configfilters/pom.xml b/flume-ng-configfilters/pom.xml index 3a219835d7..7c6a1c7480 100644 --- a/flume-ng-configfilters/pom.xml +++ b/flume-ng-configfilters/pom.xml @@ -20,7 +20,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml diff --git a/flume-ng-configuration/pom.xml b/flume-ng-configuration/pom.xml index 8ab25fd658..0ae2b9d21c 100644 --- a/flume-ng-configuration/pom.xml +++ b/flume-ng-configuration/pom.xml @@ -20,7 +20,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml flume-ng-configuration diff --git a/flume-ng-core/pom.xml b/flume-ng-core/pom.xml index e8b6a0d28c..3a9bbdd973 100644 --- a/flume-ng-core/pom.xml +++ b/flume-ng-core/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml diff --git a/flume-ng-dist/pom.xml b/flume-ng-dist/pom.xml index 6e92bc1d6c..2faad49d51 100644 --- a/flume-ng-dist/pom.xml +++ b/flume-ng-dist/pom.xml @@ -21,7 +21,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml diff --git a/flume-ng-instrumentation/flume-ganglia-monitor/pom.xml b/flume-ng-instrumentation/flume-ganglia-monitor/pom.xml index 1911b1209f..cb357256b1 100644 --- a/flume-ng-instrumentation/flume-ganglia-monitor/pom.xml +++ b/flume-ng-instrumentation/flume-ganglia-monitor/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-ng-instrumentation - 2.0.0-SNAPSHOT + ${revision} org.apache.flume.flume-ng-instrumentation diff --git a/flume-ng-instrumentation/flume-http-monitor/pom.xml b/flume-ng-instrumentation/flume-http-monitor/pom.xml index 6c266c6863..e1e33e695f 100644 --- a/flume-ng-instrumentation/flume-http-monitor/pom.xml +++ b/flume-ng-instrumentation/flume-http-monitor/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-ng-instrumentation - 2.0.0-SNAPSHOT + ${revision} org.apache.flume.flume-ng-instrumentation diff --git a/flume-ng-instrumentation/flume-prometheus-monitor/pom.xml b/flume-ng-instrumentation/flume-prometheus-monitor/pom.xml index 27b90fda76..6d66a863e4 100644 --- a/flume-ng-instrumentation/flume-prometheus-monitor/pom.xml +++ b/flume-ng-instrumentation/flume-prometheus-monitor/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-ng-instrumentation - 2.0.0-SNAPSHOT + ${revision} org.apache.flume.flume-ng-instrumentation diff --git a/flume-ng-instrumentation/pom.xml b/flume-ng-instrumentation/pom.xml index e664afbbb3..1a049f90d2 100644 --- a/flume-ng-instrumentation/pom.xml +++ b/flume-ng-instrumentation/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml diff --git a/flume-ng-node/pom.xml b/flume-ng-node/pom.xml index 40f6a4d090..5878f0f551 100644 --- a/flume-ng-node/pom.xml +++ b/flume-ng-node/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml diff --git a/flume-ng-sdk/pom.xml b/flume-ng-sdk/pom.xml index bbe85ee1e3..6b231c2708 100644 --- a/flume-ng-sdk/pom.xml +++ b/flume-ng-sdk/pom.xml @@ -21,7 +21,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml diff --git a/flume-ng-sources/flume-http-source/pom.xml b/flume-ng-sources/flume-http-source/pom.xml index 43770d964c..d7f2b58500 100644 --- a/flume-ng-sources/flume-http-source/pom.xml +++ b/flume-ng-sources/flume-http-source/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-ng-sources - 2.0.0-SNAPSHOT + ${revision} org.apache.flume.flume-ng-sources diff --git a/flume-ng-sources/flume-netcat-source/pom.xml b/flume-ng-sources/flume-netcat-source/pom.xml index d329266189..5c8becc1a3 100644 --- a/flume-ng-sources/flume-netcat-source/pom.xml +++ b/flume-ng-sources/flume-netcat-source/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-ng-sources - 2.0.0-SNAPSHOT + ${revision} org.apache.flume.flume-ng-sources diff --git a/flume-ng-sources/flume-syslog-source/pom.xml b/flume-ng-sources/flume-syslog-source/pom.xml index e45b597f72..1f0beb2f9a 100644 --- a/flume-ng-sources/flume-syslog-source/pom.xml +++ b/flume-ng-sources/flume-syslog-source/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-ng-sources - 2.0.0-SNAPSHOT + ${revision} org.apache.flume.flume-ng-sources diff --git a/flume-ng-sources/flume-taildir-source/pom.xml b/flume-ng-sources/flume-taildir-source/pom.xml index 89edc39556..d371c0f3b4 100644 --- a/flume-ng-sources/flume-taildir-source/pom.xml +++ b/flume-ng-sources/flume-taildir-source/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-ng-sources - 2.0.0-SNAPSHOT + ${revision} org.apache.flume.flume-ng-sources diff --git a/flume-ng-sources/pom.xml b/flume-ng-sources/pom.xml index 3318e3fd2e..5e12ea542f 100644 --- a/flume-ng-sources/pom.xml +++ b/flume-ng-sources/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml diff --git a/flume-parent/pom.xml b/flume-parent/pom.xml index f02aa175f0..335b5bbf60 100644 --- a/flume-parent/pom.xml +++ b/flume-parent/pom.xml @@ -28,7 +28,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} pom Apache Flume Parent @@ -59,7 +59,7 @@ scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git - rel/${project.version} + rel/${revision} https://gitbox.apache.org/repos/asf/logging-flume.git @@ -75,12 +75,15 @@ + 2.0.0-SNAPSHOT + - 2.0.0-SNAPSHOT + ${revision} true - - 2022-01-02T00:00:00Z + + 2026-01-01T00:00:00Z + UTF-8 diff --git a/flume-tools/pom.xml b/flume-tools/pom.xml index 7d58b3fb53..9d3d65fbcb 100644 --- a/flume-tools/pom.xml +++ b/flume-tools/pom.xml @@ -22,7 +22,7 @@ org.apache.flume flume-parent - 2.0.0-SNAPSHOT + ${revision} ../flume-parent/pom.xml From d32d9cb00b74c98b72d34737886f0adf9f1a94fe Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 21 Aug 2026 12:25:15 +0200 Subject: [PATCH 2/8] Derive the version metadata from the JAR manifest `saveVersion.sh` recorded the builder's user name, host, clock and a checksum of the working copy, so no two builds agreed and the source distribution, which carries no repository metadata, agreed with none. The same facts now come from POM-derived manifest headers, which a Git checkout and the source archive produce identically. `getUser()` and `getSrcChecksum()` are deprecated for removal, and `getRevision()` reports nothing until the build records a commit id again. Assisted-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011QwMh1JvFaPBgWEGrMgMj7 --- flume-ng-core/pom.xml | 111 --------- flume-ng-core/scripts/saveVersion.ps1 | 61 ----- flume-ng-core/scripts/saveVersion.sh | 69 ------ .../org/apache/flume/VersionAnnotation.java | 72 ------ .../org/apache/flume/tools/VersionInfo.java | 223 ++++++++++++++---- .../apache/flume/tools/TestVersionInfo.java | 138 +++++++++-- flume-ng-sdk/pom.xml | 14 ++ flume-parent/pom.xml | 10 + 8 files changed, 312 insertions(+), 386 deletions(-) delete mode 100644 flume-ng-core/scripts/saveVersion.ps1 delete mode 100755 flume-ng-core/scripts/saveVersion.sh delete mode 100644 flume-ng-core/src/main/java/org/apache/flume/VersionAnnotation.java diff --git a/flume-ng-core/pom.xml b/flume-ng-core/pom.xml index 3a9bbdd973..a26093c9d8 100644 --- a/flume-ng-core/pom.xml +++ b/flume-ng-core/pom.xml @@ -177,115 +177,4 @@ - - - not-windows - - - !Windows - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - generate-version - - run - - generate-sources - - - - - - - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-source - - add-source - - generate-sources - - - target/generated-sources/java - - - - - - - - - - - - windows - - - Windows - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - generate-version - - run - - generate-sources - - - - - - - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-source - - add-source - - generate-sources - - - target/generated-sources/java - - - - - - - - - - - - diff --git a/flume-ng-core/scripts/saveVersion.ps1 b/flume-ng-core/scripts/saveVersion.ps1 deleted file mode 100644 index dff88134ee..0000000000 --- a/flume-ng-core/scripts/saveVersion.ps1 +++ /dev/null @@ -1,61 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This script is used to generate the annotation of package info that -# records the version, revision, branch, url, user and timestamp. - -$version=$args[0] -$buildDirectory=$args[1] -$outputFile= "$buildDirectory\generated-sources\java\org\apache\flume\package-info.java" -$user = $Env:username -$date = Get-Date - -cmd /c svn info 2>&1 | Out-Null -if ( $LastExitCode -eq 0 ) { - $revision=svn info | % { if( $_ -match "Last Changed Rev: (?.*)" ) { $matches['rev'];} } - $url=svn info | % { if( $_ -match "URL: (?.*)" ) { $matches['url'];} } - $branch= if( $url -match ".*(?(branches.*)|(tags.*)|(trunk.*))" ) { $matches['branch']; } else { "Unknown"; } -} -else { - cmd /c git rev-parse HEAD 2>&1 | Out-Null - if ( $LastExitCode -eq 0 ) { - $revision=$(git log -1 --pretty=format:"%H") - $branch=$(git name-rev --name-only HEAD) - $remote=$(git config branch.$branch.remote) - $url=$(git config remote.$remote.url) - } - else { - revision="Unknown" - branch="Unknown" - url="file://$cwd" - } -} - -$srcChecksum="N/A" - - -$fileContent = @" -/* - * Generated by scripts/saveVersion.ps1 - */ -@VersionAnnotation(version="$version", revision="$revision", branch="$branch", - user="$user", date="$date", url="$url", - srcChecksum="$srcChecksum") -package org.apache.flume; -"@ - -New-Item $outputFile -value $fileContent -force -type file - diff --git a/flume-ng-core/scripts/saveVersion.sh b/flume-ng-core/scripts/saveVersion.sh deleted file mode 100755 index ad3f8b195d..0000000000 --- a/flume-ng-core/scripts/saveVersion.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/sh - -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This script is used to generate the annotation of package info that -# records the version, revision, branch, url, user and timestamp. - -unset LANG -unset LC_CTYPE -unset LC_TIME -version=$1 -buildDirectory=$2 -user=`whoami` -date=`date` -dir=`pwd` -cwd=`dirname $dir` -if [ -d ../.svn ]; then - revision=`svn info ../ | sed -n -e 's/Last Changed Rev: \(.*\)/\1/p'` - url=`svn info ../ | sed -n -e 's/URL: \(.*\)/\1/p'` - branch=`echo $url | sed -n -e 's,.*\(branches/.*\)$,\1,p' \ - -e 's,.*\(tags/.*\)$,\1,p' \ - -e 's,.*trunk$,trunk,p'` -elif git rev-parse HEAD 2>/dev/null > /dev/null ; then - revision=`git log -1 --pretty=format:"%H"` - hostname=`hostname` - branch=`git branch | sed -n -e 's/^* //p'` - url="git://${hostname}${cwd}" -else - revision="Unknown" - branch="Unknown" - url="file://$cwd" -fi - -if [ -n "$(which md5sum)" ]; then - srcChecksum=`find ../ -name '*.java' | grep -v generated-sources | LC_ALL=C sort | \ - xargs md5sum | md5sum | cut -d ' ' -f 1` -else - srcChecksum=`find ../ -name '*.java' | grep -v generated-sources | LC_ALL=C sort | \ - xargs md5 | md5 | cut -d ' ' -f 1` -fi - -mkdir -p $buildDirectory/generated-sources/java/org/apache/flume/ -cat << EOF | \ - sed -e "s/VERSION/$version/" -e "s/USER/$user/" -e "s/DATE/$date/" \ - -e "s|URL|$url|" -e "s/REV/$revision/" \ - -e "s|BRANCH|$branch|" -e "s/SRCCHECKSUM/$srcChecksum/" \ - > $buildDirectory/generated-sources/java/org/apache/flume/package-info.java -/* - * Generated by scripts/saveVersion.sh - */ -@VersionAnnotation(version="VERSION", revision="REV", branch="BRANCH", - user="USER", date="DATE", url="URL", - srcChecksum="SRCCHECKSUM") -package org.apache.flume; -EOF \ No newline at end of file diff --git a/flume-ng-core/src/main/java/org/apache/flume/VersionAnnotation.java b/flume-ng-core/src/main/java/org/apache/flume/VersionAnnotation.java deleted file mode 100644 index 06269ac350..0000000000 --- a/flume-ng-core/src/main/java/org/apache/flume/VersionAnnotation.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.flume; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * This class is about package attribute that captures - * version info of Flume that was compiled. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PACKAGE) -public @interface VersionAnnotation { - - /** - * Get the Flume version - * @return the version string "1.1" - */ - String version(); - - /** - * Get the subversion revision. - * @return the revision number as a string (eg. "100755") - */ - String revision(); - - /** - * Get the branch from which this was compiled. - * @return The branch name, e.g. "trunk" - */ - String branch(); - - /** - * Get the username that compiled Flume. - */ - String user(); - - /** - * Get the date when Flume was compiled. - * @return the date in unix 'date' format - */ - String date(); - - /** - * Get the url for the subversion repository. - */ - String url(); - - /** - * Get a checksum of the source files from which - * Flume was compiled. - * @return a string that uniquely identifies the source - **/ - String srcChecksum(); -} diff --git a/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java b/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java index f5220e4dfd..8e9dd47524 100644 --- a/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java +++ b/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java @@ -16,105 +16,228 @@ */ package org.apache.flume.tools; -import org.apache.flume.VersionAnnotation; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.Properties; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -/* - * This class provides version info of Flume NG +/** + * Provides the build metadata of the Flume artifact this class was loaded from. + * + *

The values come from the manifest of that artifact, falling back to its Maven descriptor when + * Flume has been shaded into another artifact and the manifest is no longer its own. Every accessor + * returns {@value #UNKNOWN} rather than {@code null} when the information is unavailable. */ - public class VersionInfo { - private static Package myPackage; - private static VersionAnnotation version; + private static final String UNKNOWN = "Unknown"; + + private static final String GROUP_ID = "org.apache.flume"; + private static final String ARTIFACT_ID = "flume-ng-core"; + + private static final String IMPLEMENTATION_TIMESTAMP = "Implementation-Timestamp"; + private static final String PURL = "Purl"; + private static final String BUNDLE_SCM = "Bundle-SCM"; + + private static final String PURL_PREFIX = "pkg:maven/" + GROUP_ID + "/" + ARTIFACT_ID + "@"; + private static final String CLASS_PATH = "org/apache/flume/tools/VersionInfo.class"; + private static final String MANIFEST_PATH = "META-INF/MANIFEST.MF"; + private static final String POM_PROPERTIES_PATH = + "/META-INF/maven/" + GROUP_ID + "/" + ARTIFACT_ID + "/pom.properties"; + + /** Matches one {@code name=value} pair of an OSGi header, with an optionally quoted value. */ + private static final Pattern SCM_ATTRIBUTE = + Pattern.compile("(?:^|,)\\s*([A-Za-z0-9_-]+)\\s*=\\s*(?:\"([^\"]*)\"|([^,]*))"); + + private static final Attributes MANIFEST = ownManifest(); + private static final Properties POM_PROPERTIES = pomProperties(); + + private static final String VERSION = version(MANIFEST, POM_PROPERTIES); + private static final String PURL_VALUE = purl(MANIFEST, POM_PROPERTIES); + private static final String URL_VALUE = orUnknown(scmAttribute(MANIFEST.getValue(BUNDLE_SCM), "url")); + private static final String TAG = orUnknown(scmAttribute(MANIFEST.getValue(BUNDLE_SCM), "tag")); + private static final String DATE = orUnknown(MANIFEST.getValue(IMPLEMENTATION_TIMESTAMP)); + + /** + * Reads the manifest of the artifact this class was loaded from. + * + *

Resolving it against the location of this class, instead of looking up + * {@code META-INF/MANIFEST.MF} on the class path, keeps another artifact from answering. Returns + * empty attributes when the manifest is missing or belongs to an artifact Flume was shaded into. + */ + private static Attributes ownManifest() { + URL self = VersionInfo.class.getResource("VersionInfo.class"); + if (self == null) { + return new Attributes(); + } + String location = self.toString(); + if (!location.endsWith(CLASS_PATH)) { + return new Attributes(); + } + String root = location.substring(0, location.length() - CLASS_PATH.length()); + try (InputStream stream = URI.create(root + MANIFEST_PATH).toURL().openStream()) { + Attributes attributes = new Manifest(stream).getMainAttributes(); + return isOwn(attributes) ? attributes : new Attributes(); + } catch (IOException | RuntimeException ignored) { + return new Attributes(); + } + } + + private static Properties pomProperties() { + Properties properties = new Properties(); + try (InputStream stream = VersionInfo.class.getResourceAsStream(POM_PROPERTIES_PATH)) { + if (stream != null) { + properties.load(stream); + } + } catch (IOException | RuntimeException ignored) { + // Falls through to the empty properties. + } + return properties; + } + + /** Tells whether the manifest describes this artifact rather than one Flume was shaded into. */ + static boolean isOwn(Attributes manifest) { + String purl = manifest.getValue(PURL); + return purl != null && purl.startsWith(PURL_PREFIX); + } - static { - myPackage = VersionAnnotation.class.getPackage(); - version = myPackage.getAnnotation(VersionAnnotation.class); + static String version(Attributes manifest, Properties pomProperties) { + String version = manifest.getValue(Attributes.Name.IMPLEMENTATION_VERSION); + return orUnknown(version != null ? version : pomProperties.getProperty("version")); + } + + static String purl(Attributes manifest, Properties pomProperties) { + String purl = manifest.getValue(PURL); + if (purl == null) { + String groupId = pomProperties.getProperty("groupId"); + String artifactId = pomProperties.getProperty("artifactId"); + String version = pomProperties.getProperty("version"); + if (groupId != null && artifactId != null && version != null) { + purl = "pkg:maven/" + groupId + "/" + artifactId + "@" + version; + } + } + return orUnknown(purl); } /** - * Get the meta-data for the Flume package. - * @return + * Returns one attribute of an OSGi {@code Bundle-SCM} header, or {@code null} if absent. + * + *

The header is specified by OSGi Core R8, section 3.2.1, as a comma separated list of + * {@code url}, {@code connection}, {@code developer-connection} and {@code tag} attributes. */ - static Package getPackage() { - return myPackage; + static String scmAttribute(String header, String attribute) { + if (header == null) { + return null; + } + Matcher matcher = SCM_ATTRIBUTE.matcher(header); + while (matcher.find()) { + if (attribute.equals(matcher.group(1))) { + String quoted = matcher.group(2); + return quoted != null ? quoted : matcher.group(3).trim(); + } + } + return null; + } + + private static String orUnknown(String value) { + return value != null && !value.isEmpty() ? value : UNKNOWN; } /** - * Get the Flume version. - * @return the Flume version string, eg. "1.1" + * Gets the Flume version. + * + * @return the Flume version string, eg. "2.0.0" */ public static String getVersion() { - return version != null ? version.version() : "Unknown"; + return VERSION; + } + + /** + * Gets the Package URL of the Flume artifact this class was loaded from. + * + * @return the Package URL, eg. "pkg:maven/org.apache.flume/flume-ng-core@2.0.0" + */ + public static String getPurl() { + return PURL_VALUE; } /** - * Get the subversion revision number for the root directory - * @return the revision number, eg. "100755" + * Gets the source control revision this was built from. + * + *

The build no longer records a commit id, since it has to produce the same artifact from a + * Git checkout and from the source distribution, which carries no repository metadata. + * + * @return always "Unknown" */ public static String getRevision() { - if (version != null && version.revision() != null && !version.revision().isEmpty()) { - return version.revision(); - } - return "Unknown"; + return UNKNOWN; } /** - * Get the branch on which this originated. - * @return The branch name, e.g. "trunk" or "branches/branch-1.1" + * Gets the source control tag or branch this was built from. + * + * @return the tag, eg. "rel/2.0.0" */ public static String getBranch() { - return version != null ? version.branch() : "Unknown"; + return TAG; } /** - * The date that Flume was compiled. - * @return the compilation date in unix date format + * Gets the date Flume was built. + * + * @return the build date in ISO-8601 format */ public static String getDate() { - return version != null ? version.date() : "Unknown"; + return DATE; } /** - * The user that compiled Flume. - * @return the username of the user + * Gets the user that compiled Flume. + * + * @return always "Unknown" + * @deprecated Recording the user would make the build unreproducible. */ + @Deprecated(since = "2.0.0", forRemoval = true) public static String getUser() { - return version != null ? version.user() : "Unknown"; + return UNKNOWN; } /** - * Get the subversion URL for the root Flume directory. + * Gets the source control URL of the Flume repository. + * + * @return the repository URL */ public static String getUrl() { - return version != null ? version.url() : "Unknown"; + return URL_VALUE; } /** - * Get the checksum of the source files from which Flume was - * built. - **/ + * Gets the checksum of the source files Flume was built from. + * + * @return always "Unknown" + * @deprecated Use {@link #getPurl()} to identify the artifact, and verify it against the + * checksums published with the release. + */ + @Deprecated(since = "2.0.0", forRemoval = true) public static String getSrcChecksum() { - return version != null ? version.srcChecksum() : "Unknown"; + return UNKNOWN; } - /** - * Returns the build version info which includes version, - * revision, user, date and source checksum - */ + /** Returns the build version info, which includes the version, the tag and the build date. */ public static String getBuildVersion() { - return VersionInfo.getVersion() + " from " - + VersionInfo.getRevision() + " by " - + VersionInfo.getUser() + " on " - + VersionInfo.getDate() + " source checksum " - + VersionInfo.getSrcChecksum(); + return getVersion() + " from " + getBranch() + " built on " + getDate(); } public static void main(String[] args) { System.out.println("Flume " + getVersion()); - System.out.println("Source code repository: " + "https://git.apache.org/repos/asf/flume.git"); - System.out.println("Revision: " + getRevision()); - System.out.println("Compiled by " + getUser() + " on " + getDate()); - System.out.println("From source with checksum " + getSrcChecksum()); + System.out.println("Package URL: " + getPurl()); + System.out.println("Source code repository: " + getUrl()); + System.out.println("Tag: " + getBranch()); + System.out.println("Compiled on " + getDate()); } } diff --git a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java index 59457f96e8..66d70734f7 100644 --- a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java +++ b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java @@ -16,42 +16,134 @@ */ package org.apache.flume.tools; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import java.time.Instant; +import java.util.Properties; +import java.util.jar.Attributes; import org.junit.Test; public class TestVersionInfo { - private static final Logger logger = LogManager.getLogger(); + private static final String PURL = "pkg:maven/org.apache.flume/flume-ng-core@2.0.0"; + + private static final String BUNDLE_SCM = "url=\"https://gitbox.apache.org/repos/asf/logging-flume.git\"," + + "connection=\"scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git\"," + + "developer-connection=\"scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git\"," + + "tag=\"rel/2.0.0\""; /** - * Make sure that Unknown is expected when no version info + * Checks the metadata of the artifact the test runs against. + * + *

BND writes the manifest to the output directory before the tests run, so the values are + * available whether Flume is loaded from a JAR or from the compiled classes. */ @Test - public void testVersionInfoUnknown() { + public void testMetadataOfOwnArtifact() { + assertTrue( + "getVersion returned " + VersionInfo.getVersion(), + VersionInfo.getVersion().matches("\\d+\\.\\d+.*")); + assertTrue( + "getPurl returned " + VersionInfo.getPurl(), + VersionInfo.getPurl().startsWith("pkg:maven/org.apache.flume/flume-ng-core@")); + assertTrue( + "getUrl returned " + VersionInfo.getUrl(), + VersionInfo.getUrl().startsWith("https://") + && VersionInfo.getUrl().contains("logging-flume")); + assertTrue( + "getBranch returned " + VersionInfo.getBranch(), + VersionInfo.getBranch().startsWith("rel/")); + // Throws if the timestamp is not ISO-8601. + Instant.parse(VersionInfo.getDate()); + assertTrue( + "getBuildVersion returned " + VersionInfo.getBuildVersion(), + VersionInfo.getBuildVersion().matches(".+ from .+ built on .+")); + } - logger.debug("Flume " + VersionInfo.getVersion()); - logger.debug("Subversion " + VersionInfo.getUrl() + " -r " + VersionInfo.getRevision()); - logger.debug("Compiled by " + VersionInfo.getUser() + " on " + VersionInfo.getDate()); - logger.debug("From source with checksum " + VersionInfo.getSrcChecksum()); - logger.debug("Flume " + VersionInfo.getBuildVersion()); + @Test + @SuppressWarnings({"deprecation", "removal"}) + public void testUnrecordedMetadata() { + assertEquals("Unknown", VersionInfo.getRevision()); + assertEquals("Unknown", VersionInfo.getUser()); + assertEquals("Unknown", VersionInfo.getSrcChecksum()); + } - assertTrue("getVersion returned Unknown", !VersionInfo.getVersion().equals("Unknown")); - assertTrue("getUser returned Unknown", !VersionInfo.getUser().equals("Unknown")); - assertTrue("getUrl returned Unknown", !VersionInfo.getUrl().equals("Unknown")); - assertTrue( - "getSrcChecksum returned Unknown", !VersionInfo.getSrcChecksum().equals("Unknown")); + @Test + public void testVersionPrefersTheManifest() { + Attributes manifest = new Attributes(); + manifest.putValue("Implementation-Version", "2.0.0"); + assertEquals("2.0.0", VersionInfo.version(manifest, pomProperties("1.11.0"))); + } - // check getBuildVersion() return format - assertTrue( - "getBuildVersion returned unexpected format", - VersionInfo.getBuildVersion().matches(".+from.+by.+on.+source checksum.+")); + @Test + public void testVersionFallsBackToPomProperties() { + assertEquals("1.11.0", VersionInfo.version(new Attributes(), pomProperties("1.11.0"))); + } + + @Test + public void testVersionWithoutAnySource() { + assertEquals("Unknown", VersionInfo.version(new Attributes(), new Properties())); + } + + @Test + public void testPurlPrefersTheManifest() { + Attributes manifest = new Attributes(); + manifest.putValue("Purl", PURL); + assertEquals(PURL, VersionInfo.purl(manifest, pomProperties("1.11.0"))); + } + + @Test + public void testPurlIsBuiltFromPomProperties() { + assertEquals( + "pkg:maven/org.apache.flume/flume-ng-core@1.11.0", + VersionInfo.purl(new Attributes(), pomProperties("1.11.0"))); + } + + @Test + public void testPurlWithoutAnySource() { + assertEquals("Unknown", VersionInfo.purl(new Attributes(), new Properties())); + } + + /** A manifest of an artifact Flume was shaded into must not be mistaken for our own. */ + @Test + public void testForeignManifestIsRejected() { + Attributes foreign = new Attributes(); + foreign.putValue("Purl", "pkg:maven/com.example/uber-jar@1.0.0"); + foreign.putValue("Implementation-Version", "1.0.0"); + assertFalse(VersionInfo.isOwn(foreign)); + assertFalse(VersionInfo.isOwn(new Attributes())); + + Attributes own = new Attributes(); + own.putValue("Purl", PURL); + assertTrue(VersionInfo.isOwn(own)); + } + + @Test + public void testScmAttributes() { + assertEquals("rel/2.0.0", VersionInfo.scmAttribute(BUNDLE_SCM, "tag")); + assertEquals( + "https://gitbox.apache.org/repos/asf/logging-flume.git", VersionInfo.scmAttribute(BUNDLE_SCM, "url")); + assertEquals( + "scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git", + VersionInfo.scmAttribute(BUNDLE_SCM, "developer-connection")); + assertNull(VersionInfo.scmAttribute(BUNDLE_SCM, "revision")); + assertNull(VersionInfo.scmAttribute(null, "tag")); + } + + /** OSGi only requires quoting for values with special characters. */ + @Test + public void testScmAttributeWithoutQuotes() { + assertEquals("rel/2.0.0", VersionInfo.scmAttribute("url=https://example.org,tag=rel/2.0.0", "tag")); + } - // "Unknown" when build without svn or git - assertNotNull("getRevision returned null", VersionInfo.getRevision()); - assertNotNull("getBranch returned null", VersionInfo.getBranch()); + private static Properties pomProperties(String version) { + Properties properties = new Properties(); + properties.setProperty("groupId", "org.apache.flume"); + properties.setProperty("artifactId", "flume-ng-core"); + properties.setProperty("version", version); + return properties; } } diff --git a/flume-ng-sdk/pom.xml b/flume-ng-sdk/pom.xml index 6b231c2708..c5b1d20d69 100644 --- a/flume-ng-sdk/pom.xml +++ b/flume-ng-sdk/pom.xml @@ -90,6 +90,20 @@ test-jar + + + + + true + true + + + pkg:maven/${project.groupId}/${project.artifactId}@${project.version}?classifier=tests&type=test-jar + + + diff --git a/flume-parent/pom.xml b/flume-parent/pom.xml index 335b5bbf60..8036ba0ae2 100644 --- a/flume-parent/pom.xml +++ b/flume-parent/pom.xml @@ -84,6 +84,16 @@ 2026-01-01T00:00:00Z + + -removeheaders: Bundle-DocURL,Bundle-Developers + + Implementation-Timestamp: ${project.build.outputTimestamp} + Purl: pkg:maven/${project.groupId}/${project.artifactId}@${project.version} UTF-8 From 6372bc4e11e206c5030a4f178744f86eff609828 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 25 Aug 2026 12:28:32 +0200 Subject: [PATCH 3/8] Fall back to the Maven descriptor on blank manifest headers A present but empty header would have suppressed the fallback. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zsFn1f2B32vcNoCeZSfn7 --- .../org/apache/flume/tools/VersionInfo.java | 7 ++++--- .../apache/flume/tools/TestVersionInfo.java | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java b/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java index 8e9dd47524..b8b790bcd9 100644 --- a/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java +++ b/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java @@ -25,6 +25,7 @@ import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; /** * Provides the build metadata of the Flume artifact this class was loaded from. @@ -108,12 +109,12 @@ static boolean isOwn(Attributes manifest) { static String version(Attributes manifest, Properties pomProperties) { String version = manifest.getValue(Attributes.Name.IMPLEMENTATION_VERSION); - return orUnknown(version != null ? version : pomProperties.getProperty("version")); + return orUnknown(StringUtils.isNotBlank(version) ? version : pomProperties.getProperty("version")); } static String purl(Attributes manifest, Properties pomProperties) { String purl = manifest.getValue(PURL); - if (purl == null) { + if (StringUtils.isBlank(purl)) { String groupId = pomProperties.getProperty("groupId"); String artifactId = pomProperties.getProperty("artifactId"); String version = pomProperties.getProperty("version"); @@ -145,7 +146,7 @@ static String scmAttribute(String header, String attribute) { } private static String orUnknown(String value) { - return value != null && !value.isEmpty() ? value : UNKNOWN; + return StringUtils.defaultIfBlank(value, UNKNOWN); } /** diff --git a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java index 66d70734f7..e05e5024ad 100644 --- a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java +++ b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java @@ -88,6 +88,14 @@ public void testVersionWithoutAnySource() { assertEquals("Unknown", VersionInfo.version(new Attributes(), new Properties())); } + /** A present but blank header must not shadow the Maven descriptor. */ + @Test + public void testVersionIgnoresBlankHeader() { + Attributes manifest = new Attributes(); + manifest.putValue("Implementation-Version", " "); + assertEquals("1.11.0", VersionInfo.version(manifest, pomProperties("1.11.0"))); + } + @Test public void testPurlPrefersTheManifest() { Attributes manifest = new Attributes(); @@ -107,6 +115,16 @@ public void testPurlWithoutAnySource() { assertEquals("Unknown", VersionInfo.purl(new Attributes(), new Properties())); } + /** A present but blank header must not shadow the Maven descriptor. */ + @Test + public void testPurlIgnoresBlankHeader() { + Attributes manifest = new Attributes(); + manifest.putValue("Purl", " "); + assertEquals( + "pkg:maven/org.apache.flume/flume-ng-core@1.11.0", + VersionInfo.purl(manifest, pomProperties("1.11.0"))); + } + /** A manifest of an artifact Flume was shaded into must not be mistaken for our own. */ @Test public void testForeignManifestIsRejected() { From 294813ddc9845d5821a5b57a422aec3a0c9d897b Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 25 Aug 2026 12:28:41 +0200 Subject: [PATCH 4/8] Mention the "Unknown" sentinel in the `getDate` contract The `@return` clause promised ISO-8601 unconditionally. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zsFn1f2B32vcNoCeZSfn7 --- .../src/main/java/org/apache/flume/tools/VersionInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java b/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java index b8b790bcd9..0f64693044 100644 --- a/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java +++ b/flume-ng-core/src/main/java/org/apache/flume/tools/VersionInfo.java @@ -191,7 +191,7 @@ public static String getBranch() { /** * Gets the date Flume was built. * - * @return the build date in ISO-8601 format + * @return the build date in ISO-8601 format, or "Unknown" if unavailable */ public static String getDate() { return DATE; From d2517e5210451cc18a8673b6914fdb0311802333 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 25 Aug 2026 12:29:12 +0200 Subject: [PATCH 5/8] Keep repository specifics out of the metadata test Downstream builds override ``; assert only invariants, while still rejecting tags that betray an unset ``. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zsFn1f2B32vcNoCeZSfn7 --- .../java/org/apache/flume/tools/TestVersionInfo.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java index e05e5024ad..77db7cd9fc 100644 --- a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java +++ b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java @@ -21,7 +21,9 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.net.URI; import java.time.Instant; +import java.util.List; import java.util.Properties; import java.util.jar.Attributes; import org.junit.Test; @@ -49,13 +51,14 @@ public void testMetadataOfOwnArtifact() { assertTrue( "getPurl returned " + VersionInfo.getPurl(), VersionInfo.getPurl().startsWith("pkg:maven/org.apache.flume/flume-ng-core@")); + // Downstream builds override ``, so only check invariants: + // a valid URL, and a tag that does not betray an unset ``. assertTrue( "getUrl returned " + VersionInfo.getUrl(), - VersionInfo.getUrl().startsWith("https://") - && VersionInfo.getUrl().contains("logging-flume")); - assertTrue( + URI.create(VersionInfo.getUrl()).toString().startsWith("https://")); + assertFalse( "getBranch returned " + VersionInfo.getBranch(), - VersionInfo.getBranch().startsWith("rel/")); + List.of("", "Unknown", "HEAD", "master", "main", "trunk").contains(VersionInfo.getBranch())); // Throws if the timestamp is not ISO-8601. Instant.parse(VersionInfo.getDate()); assertTrue( From 66ce59d40e63707c8c1f6f90d2bf5dc1836f9eaf Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 25 Aug 2026 12:54:48 +0200 Subject: [PATCH 6/8] Apply Spotless formatting Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zsFn1f2B32vcNoCeZSfn7 --- .../src/test/java/org/apache/flume/tools/TestVersionInfo.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java index 77db7cd9fc..b927611026 100644 --- a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java +++ b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java @@ -124,8 +124,7 @@ public void testPurlIgnoresBlankHeader() { Attributes manifest = new Attributes(); manifest.putValue("Purl", " "); assertEquals( - "pkg:maven/org.apache.flume/flume-ng-core@1.11.0", - VersionInfo.purl(manifest, pomProperties("1.11.0"))); + "pkg:maven/org.apache.flume/flume-ng-core@1.11.0", VersionInfo.purl(manifest, pomProperties("1.11.0"))); } /** A manifest of an artifact Flume was shaded into must not be mistaken for our own. */ From 9deaa619bcca8d2ad703e45434bbbb3a8bc1e7ea Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 25 Aug 2026 13:17:43 +0200 Subject: [PATCH 7/8] De-flake `TestExecSource` on Windows Fixed sleeps lose to PowerShell start-up times on loaded CI runners: await the source counter instead. `Get-Content -Wait` keeps the process alive like `tail -f`, so the batch timeout, not the EOF flush, delivers the events. Writing the input file before the source starts removes the race with the one-shot read of the old command. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zsFn1f2B32vcNoCeZSfn7 --- flume-ng-core/pom.xml | 6 ++ .../org/apache/flume/source/ExecSource.java | 6 ++ .../apache/flume/source/TestExecSource.java | 59 ++++++++++++------- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/flume-ng-core/pom.xml b/flume-ng-core/pom.xml index a26093c9d8..2f8044fe37 100644 --- a/flume-ng-core/pom.xml +++ b/flume-ng-core/pom.xml @@ -98,6 +98,12 @@ test + + org.awaitility + awaitility + test + + org.easytesting fest-reflect diff --git a/flume-ng-core/src/main/java/org/apache/flume/source/ExecSource.java b/flume-ng-core/src/main/java/org/apache/flume/source/ExecSource.java index c99e8dd5f6..17b03c5814 100644 --- a/flume-ng-core/src/main/java/org/apache/flume/source/ExecSource.java +++ b/flume-ng-core/src/main/java/org/apache/flume/source/ExecSource.java @@ -16,6 +16,7 @@ */ package org.apache.flume.source; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.io.BufferedReader; @@ -260,6 +261,11 @@ public long getBatchSize() { return bufferCount; } + @VisibleForTesting + SourceCounter getSourceCounter() { + return sourceCounter; + } + private static class ExecRunnable implements Runnable { public ExecRunnable( diff --git a/flume-ng-core/src/test/java/org/apache/flume/source/TestExecSource.java b/flume-ng-core/src/test/java/org/apache/flume/source/TestExecSource.java index d8d369f6bc..bc1d35a74f 100644 --- a/flume-ng-core/src/test/java/org/apache/flume/source/TestExecSource.java +++ b/flume-ng-core/src/test/java/org/apache/flume/source/TestExecSource.java @@ -16,6 +16,7 @@ */ package org.apache.flume.source; +import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -31,6 +32,7 @@ import java.lang.management.ManagementFactory; import java.nio.charset.Charset; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import javax.management.Attribute; import javax.management.AttributeList; @@ -58,9 +60,9 @@ public class TestExecSource { private AbstractSource source; - private Channel channel = new MemoryChannel(); - private Context context = new Context(); - private ChannelSelector rcs = new ReplicatingChannelSelector(); + private final Channel channel = new MemoryChannel(); + private final Context context = new Context(); + private final ChannelSelector rcs = new ReplicatingChannelSelector(); @Before public void setUp() { @@ -101,7 +103,7 @@ public void testProcess() throws InterruptedException, LifecycleException, Event // Generates input file with a random data set (10 lines, 200 characters each) FileOutputStream outputStream1 = new FileOutputStream(inputFile); for (int i = 0; i < 10; i++) { - outputStream1.write(RandomStringUtils.randomAlphanumeric(200).getBytes()); + outputStream1.write(RandomStringUtils.insecure().nextAlphanumeric(200).getBytes()); outputStream1.write('\n'); } outputStream1.close(); @@ -116,7 +118,7 @@ public void testProcess() throws InterruptedException, LifecycleException, Event Configurables.configure(source, context); source.start(); - Thread.sleep(2000); + awaitEventCount(10); Transaction transaction = channel.getTransaction(); transaction.begin(); @@ -286,36 +288,41 @@ public void testMonitoredCounterGroup() public void testBatchTimeout() throws InterruptedException, LifecycleException, EventDeliveryException, IOException { - String filePath = "/tmp/flume-execsource." + Thread.currentThread().getId(); + File file = File.createTempFile("flume-execsource", null); + FileUtils.forceDeleteOnExit(file); String eventBody = "TestMessage"; - FileOutputStream outputStream = new FileOutputStream(filePath); + + // Write the file up front, so the command output does not depend on + // when the process starts reading; both commands below print the last + // lines of an existing file and then follow it. + FileOutputStream outputStream = new FileOutputStream(file); + for (int lineNumber = 0; lineNumber < 3; lineNumber++) { + outputStream.write((eventBody).getBytes()); + outputStream.write(String.valueOf(lineNumber).getBytes()); + outputStream.write('\n'); + } + outputStream.close(); context.put(ExecSourceConfigurationConstants.CONFIG_BATCH_SIZE, "50000"); context.put(ExecSourceConfigurationConstants.CONFIG_BATCH_TIME_OUT, "750"); context.put( "shell", SystemUtils.IS_OS_WINDOWS ? "powershell -ExecutionPolicy Unrestricted -command" : "/bin/bash -c"); + // The process must outlive the batch timeout, so that only the timed + // flush can deliver the events. context.put( "command", SystemUtils.IS_OS_WINDOWS - ? "Get-Content " + filePath + " | Select-Object -Last 10" - : ("tail -f " + filePath)); + ? "Get-Content -Tail 10 -Wait '" + file.getAbsolutePath() + "'" + : ("tail -f " + file.getAbsolutePath())); Configurables.configure(source, context); source.start(); + awaitEventCount(3); Transaction transaction = channel.getTransaction(); transaction.begin(); - for (int lineNumber = 0; lineNumber < 3; lineNumber++) { - outputStream.write((eventBody).getBytes()); - outputStream.write(String.valueOf(lineNumber).getBytes()); - outputStream.write('\n'); - outputStream.flush(); - } - outputStream.close(); - Thread.sleep(1500); - for (int i = 0; i < 3; i++) { Event event = channel.take(); assertNotNull(event); @@ -326,19 +333,27 @@ public void testBatchTimeout() transaction.commit(); transaction.close(); source.stop(); - File file = new File(filePath); FileUtils.forceDelete(file); } + /** + * Waits until the source accepted the given number of events. + * + *

A fixed sleep is not enough on slow environments, where starting + * the child process alone can take several seconds. + */ + private void awaitEventCount(int expected) { + await().atMost(30, TimeUnit.SECONDS) + .until(() -> ((ExecSource) source).getSourceCounter().getEventAcceptedCount() >= expected); + } + private void runTestShellCmdHelper(String shell, String command, String[] expectedOutput) throws InterruptedException, LifecycleException, EventDeliveryException, IOException { context.put("shell", shell); context.put("command", command); Configurables.configure(source, context); source.start(); - // Some commands might take longer to complete, specially on Windows - // or on slow environments (e.g. Travis CI). - Thread.sleep(2500); + awaitEventCount(expectedOutput.length); Transaction transaction = channel.getTransaction(); transaction.begin(); try { From 482a1042d3a56b93d9783cf9f2338a9bcc544ba4 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 25 Aug 2026 13:19:43 +0200 Subject: [PATCH 8/8] Revert "De-flake `TestExecSource` on Windows" The change is unrelated to this PR; it moves to its own PR from trunk. This reverts commit 9deaa619830cd845e91a1855076ac53c99cbabc0. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016zsFn1f2B32vcNoCeZSfn7 --- flume-ng-core/pom.xml | 6 -- .../org/apache/flume/source/ExecSource.java | 6 -- .../apache/flume/source/TestExecSource.java | 59 +++++++------------ 3 files changed, 22 insertions(+), 49 deletions(-) diff --git a/flume-ng-core/pom.xml b/flume-ng-core/pom.xml index 2f8044fe37..a26093c9d8 100644 --- a/flume-ng-core/pom.xml +++ b/flume-ng-core/pom.xml @@ -98,12 +98,6 @@ test - - org.awaitility - awaitility - test - - org.easytesting fest-reflect diff --git a/flume-ng-core/src/main/java/org/apache/flume/source/ExecSource.java b/flume-ng-core/src/main/java/org/apache/flume/source/ExecSource.java index 17b03c5814..c99e8dd5f6 100644 --- a/flume-ng-core/src/main/java/org/apache/flume/source/ExecSource.java +++ b/flume-ng-core/src/main/java/org/apache/flume/source/ExecSource.java @@ -16,7 +16,6 @@ */ package org.apache.flume.source; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.io.BufferedReader; @@ -261,11 +260,6 @@ public long getBatchSize() { return bufferCount; } - @VisibleForTesting - SourceCounter getSourceCounter() { - return sourceCounter; - } - private static class ExecRunnable implements Runnable { public ExecRunnable( diff --git a/flume-ng-core/src/test/java/org/apache/flume/source/TestExecSource.java b/flume-ng-core/src/test/java/org/apache/flume/source/TestExecSource.java index bc1d35a74f..d8d369f6bc 100644 --- a/flume-ng-core/src/test/java/org/apache/flume/source/TestExecSource.java +++ b/flume-ng-core/src/test/java/org/apache/flume/source/TestExecSource.java @@ -16,7 +16,6 @@ */ package org.apache.flume.source; -import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -32,7 +31,6 @@ import java.lang.management.ManagementFactory; import java.nio.charset.Charset; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import javax.management.Attribute; import javax.management.AttributeList; @@ -60,9 +58,9 @@ public class TestExecSource { private AbstractSource source; - private final Channel channel = new MemoryChannel(); - private final Context context = new Context(); - private final ChannelSelector rcs = new ReplicatingChannelSelector(); + private Channel channel = new MemoryChannel(); + private Context context = new Context(); + private ChannelSelector rcs = new ReplicatingChannelSelector(); @Before public void setUp() { @@ -103,7 +101,7 @@ public void testProcess() throws InterruptedException, LifecycleException, Event // Generates input file with a random data set (10 lines, 200 characters each) FileOutputStream outputStream1 = new FileOutputStream(inputFile); for (int i = 0; i < 10; i++) { - outputStream1.write(RandomStringUtils.insecure().nextAlphanumeric(200).getBytes()); + outputStream1.write(RandomStringUtils.randomAlphanumeric(200).getBytes()); outputStream1.write('\n'); } outputStream1.close(); @@ -118,7 +116,7 @@ public void testProcess() throws InterruptedException, LifecycleException, Event Configurables.configure(source, context); source.start(); - awaitEventCount(10); + Thread.sleep(2000); Transaction transaction = channel.getTransaction(); transaction.begin(); @@ -288,41 +286,36 @@ public void testMonitoredCounterGroup() public void testBatchTimeout() throws InterruptedException, LifecycleException, EventDeliveryException, IOException { - File file = File.createTempFile("flume-execsource", null); - FileUtils.forceDeleteOnExit(file); + String filePath = "/tmp/flume-execsource." + Thread.currentThread().getId(); String eventBody = "TestMessage"; - - // Write the file up front, so the command output does not depend on - // when the process starts reading; both commands below print the last - // lines of an existing file and then follow it. - FileOutputStream outputStream = new FileOutputStream(file); - for (int lineNumber = 0; lineNumber < 3; lineNumber++) { - outputStream.write((eventBody).getBytes()); - outputStream.write(String.valueOf(lineNumber).getBytes()); - outputStream.write('\n'); - } - outputStream.close(); + FileOutputStream outputStream = new FileOutputStream(filePath); context.put(ExecSourceConfigurationConstants.CONFIG_BATCH_SIZE, "50000"); context.put(ExecSourceConfigurationConstants.CONFIG_BATCH_TIME_OUT, "750"); context.put( "shell", SystemUtils.IS_OS_WINDOWS ? "powershell -ExecutionPolicy Unrestricted -command" : "/bin/bash -c"); - // The process must outlive the batch timeout, so that only the timed - // flush can deliver the events. context.put( "command", SystemUtils.IS_OS_WINDOWS - ? "Get-Content -Tail 10 -Wait '" + file.getAbsolutePath() + "'" - : ("tail -f " + file.getAbsolutePath())); + ? "Get-Content " + filePath + " | Select-Object -Last 10" + : ("tail -f " + filePath)); Configurables.configure(source, context); source.start(); - awaitEventCount(3); Transaction transaction = channel.getTransaction(); transaction.begin(); + for (int lineNumber = 0; lineNumber < 3; lineNumber++) { + outputStream.write((eventBody).getBytes()); + outputStream.write(String.valueOf(lineNumber).getBytes()); + outputStream.write('\n'); + outputStream.flush(); + } + outputStream.close(); + Thread.sleep(1500); + for (int i = 0; i < 3; i++) { Event event = channel.take(); assertNotNull(event); @@ -333,27 +326,19 @@ public void testBatchTimeout() transaction.commit(); transaction.close(); source.stop(); + File file = new File(filePath); FileUtils.forceDelete(file); } - /** - * Waits until the source accepted the given number of events. - * - *

A fixed sleep is not enough on slow environments, where starting - * the child process alone can take several seconds. - */ - private void awaitEventCount(int expected) { - await().atMost(30, TimeUnit.SECONDS) - .until(() -> ((ExecSource) source).getSourceCounter().getEventAcceptedCount() >= expected); - } - private void runTestShellCmdHelper(String shell, String command, String[] expectedOutput) throws InterruptedException, LifecycleException, EventDeliveryException, IOException { context.put("shell", shell); context.put("command", command); Configurables.configure(source, context); source.start(); - awaitEventCount(expectedOutput.length); + // Some commands might take longer to complete, specially on Windows + // or on slow environments (e.g. Travis CI). + Thread.sleep(2500); Transaction transaction = channel.getTransaction(); transaction.begin(); try {