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

FrankChen021 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 46d2de3df9f build: only warn about stale licenses.yaml versions in PR 
CI (#20774)
46d2de3df9f is described below

commit 46d2de3df9f8b0757336be0f59428bbab624b99f
Author: Andreas Maechler <[email protected]>
AuthorDate: Sat Oct 31 20:49:31 2026 -0600

    build: only warn about stale licenses.yaml versions in PR CI (#20774)
    
    * build: only warn about stale licenses.yaml versions in PR CI
    
    check-licenses.py gets --stale-versions=fail|warn, defaulting to fail. With
    warn, a reported dependency becomes a warning when its groupId and 
artifactId
    are registered under other versions, all with the reported license, and one
    of those versions is no longer in any report. The warning gives the commands
    to refresh licenses.yaml. Unregistered dependencies, changed or unknown
    licenses, license_file_path problems and unknown module tags still fail, and
    so does a module that starts bundling another version of a library while the
    registered version still ships. If the dependency ships in the release,
    registrations under excluded modules do not count.
    
    distribution/pom.xml passes druid.distribution.licenses.staleVersions 
(default
    fail) to the check, and packaging-check.sh sets it to warn, so release 
builds
    stay strict.
    
    * build: report stale licenses.yaml versions in strict mode too
    
    check-licenses.py now separates stale versions from missing licenses in both
    modes, so the release build lists them with the refresh hint instead of as
    missing licenses. --stale-versions only decides whether they fail the check.
    
    Drop the --stale-versions flag from license_checks_script.sh: nothing calls
    that script.
    
    * build: keep a shipped version registered only under excluded modules 
missing
    
    split_stale_versions treated a reported version that is registered only
    under an excluded module, with an older version registered under a shipped
    module, as stale. When an extension ships that version, the release has no
    registration for the jar and refresh-license-versions.py neither updates nor
    flags the entry. Only treat a missing version as stale when it is in no
    licenses.yaml entry at all.
---
 .github/scripts/packaging-check.sh      |   4 +-
 dev/license.md                          |   3 +
 distribution/bin/check-licenses.py      |  57 ++++++++++++++-
 distribution/bin/test_check_licenses.py | 125 +++++++++++++++++++++++++++++++-
 distribution/pom.xml                    |   3 +
 5 files changed, 185 insertions(+), 7 deletions(-)

diff --git a/.github/scripts/packaging-check.sh 
b/.github/scripts/packaging-check.sh
index 34584f57c8b..26dfda081e5 100755
--- a/.github/scripts/packaging-check.sh
+++ b/.github/scripts/packaging-check.sh
@@ -23,6 +23,8 @@ CODE_CHECKS_SKIP="-Dcheckstyle.skip -Dpmd.skip 
-Dforbiddenapis.skip -Danimal.sni
 ./mvnw -B install -Papache-release -Prat --fail-at-end \
   -pl '!benchmarks, !distribution' -P skip-tests -Dweb.console.skip=false \
   -Dgpg.skip -Ddependency-check.skip $CODE_CHECKS_SKIP
+# Stale licenses.yaml versions only warn here; release builds fail on them.
 ./mvnw -B install -Papache-release -Prat -Pdist -Pbundle-contrib-exts 
--fail-at-end \
   -pl 'distribution' -P skip-tests -Dweb.console.skip=false \
-  -Dgpg.skip -Ddependency-check.skip $CODE_CHECKS_SKIP
+  -Dgpg.skip -Ddependency-check.skip $CODE_CHECKS_SKIP \
+  -Ddruid.distribution.licenses.staleVersions=warn
diff --git a/dev/license.md b/dev/license.md
index 6ec63aa580c..c9907d345a6 100644
--- a/dev/license.md
+++ b/dev/license.md
@@ -134,3 +134,6 @@ it cannot update on its own, such as an entry whose 
libraries moved to different
 distribution/bin/generate-license-dependency-reports.py . target 
--clean-maven-artifact-transfer --parallel 2
 distribution/bin/refresh-license-versions.py licenses.yaml 
target/license-reports
 ```
+
+If you leave a version as it is, PR CI only prints a warning, provided the 
version it replaced is registered with the
+same license. Release builds still fail on these. A dependency that is new, or 
whose license changed, fails PR CI too.
diff --git a/distribution/bin/check-licenses.py 
b/distribution/bin/check-licenses.py
index 8051266b40c..564a48bac40 100755
--- a/distribution/bin/check-licenses.py
+++ b/distribution/bin/check-licenses.py
@@ -413,6 +413,29 @@ def find_missing_licenses(reported_dep_to_licenses, 
reported_modules, registered
         missing_licenses.append((", ".join(modules),) + key + 
(reported_license_druid_module[0],))
     return missing_licenses
 
+def split_stale_versions(missing_licenses, reported_modules, 
registered_dep_to_licenses, registered_modules, exclude_module_prefixes):
+    # A missing dependency only has a stale version when its own version is in 
no licenses.yaml entry, its groupId and
+    # artifactId are registered under other versions, all with the reported 
license, and one of those versions is no
+    # longer reported. If it ships in the release, only registrations outside 
the excluded modules count.
+    # Returns the stale ones, each with its registered versions appended, and 
the rest.
+    stale_licenses = []
+    remaining_licenses = []
+    for missing_license in missing_licenses:
+        key, reported_license = missing_license[1:4], missing_license[4]
+        others = [k for k in registered_dep_to_licenses if k[:2] == key[:2]]
+        if any(not is_excluded_module(m, exclude_module_prefixes) for m in 
reported_modules[key]):
+            others = [k for k in others if any(not is_excluded_module(m, 
exclude_module_prefixes) for m in registered_modules[k])]
+        if (
+            key not in registered_dep_to_licenses
+            and others
+            and all(registered_dep_to_licenses[k] == reported_license for k in 
others)
+            and any(k not in reported_modules for k in others)
+        ):
+            stale_licenses.append(missing_license + (", ".join(sorted(k[2] for 
k in others)),))
+        else:
+            remaining_licenses.append(missing_license)
+    return stale_licenses, remaining_licenses
+
 def find_unknown_licenses(reported_dep_to_licenses, reported_modules, 
reported_license_names, skipping_licenses, exclude_module_prefixes):
     # A dependency that ships in the release must report at least one license 
in compatible_license_names.
     unknown_licenses = []
@@ -449,7 +472,7 @@ def read_dependency_reports(dependency_reports_root, 
compatible_license_names):
         raise Exception("No dependency reports are found")
     return reported_dep_to_licenses, reported_modules, reported_license_names
 
-def check_licenses(license_yaml, dependency_reports_root, 
exclude_module_prefixes):
+def check_licenses(license_yaml, dependency_reports_root, 
exclude_module_prefixes, stale_versions):
     # Build a dictionary to facilitate comparing reported licenses and 
registered ones.
     # These dictionaries are the mapping of (group_id, artifact_id, version) 
to license_name.
 
@@ -568,6 +591,23 @@ def check_licenses(license_yaml, dependency_reports_root, 
exclude_module_prefixe
         reported_dep_to_licenses, reported_modules, registered_modules, 
skipping_licenses, exclude_module_prefixes
     )
 
+    stale_licenses, missing_licenses = split_stale_versions(
+        missing_licenses, reported_modules, registered_dep_to_licenses, 
registered_modules, exclude_module_prefixes
+    )
+
+    if len(stale_licenses) > 0:
+        print_log_to_stderr("{}: found {} dependencies whose previous versions 
are registered with the same license. Release builds fail on these.".format(
+            "Warn" if stale_versions == "warn" else "Error", 
len(stale_licenses)
+        ))
+        for stale_license in stale_licenses:
+            print_log_to_stderr("druid_module: {}, groupId: {}, artifactId: 
{}, version: {}, license: {}, registered_versions: {}".format(*stale_license))
+        print_log_to_stderr(
+            "To update licenses.yaml, run 
distribution/bin/generate-license-dependency-reports.py . target "
+            "--clean-maven-artifact-transfer --parallel 2 and then 
distribution/bin/refresh-license-versions.py "
+            "licenses.yaml target/license-reports, which lists the entries it 
cannot update."
+        )
+        print_log_to_stderr("")
+
     if len(missing_licenses) > 0:
         print_log_to_stderr("Error: found {} missing licenses. These licenses 
are reported, but missing in the registry".format(len(missing_licenses)))
         for missing_license in missing_licenses:
@@ -604,10 +644,14 @@ def check_licenses(license_yaml, dependency_reports_root, 
exclude_module_prefixe
             print_log_to_stderr("groupId: {}, artifactId: {}, version: {}, 
reported_license: {}".format(unchecked_license[0], unchecked_license[1], 
unchecked_license[2], unchecked_license[3]))
     print_log_to_stderr("")
 
-    if len(mismatched_licenses) > 0 or len(missing_licenses) > 0 or 
len(unknown_licenses) > 0:
+    if (
+        len(mismatched_licenses) > 0 or len(missing_licenses) > 0 or 
len(unknown_licenses) > 0
+        or (len(stale_licenses) > 0 and stale_versions == "fail")
+    ):
         print_log_to_stderr("Mismatched licenses: 
{}".format(mismatched_licenses))
         print_log_to_stderr("Missing licenses: {}".format(missing_licenses))
         print_log_to_stderr("Unknown licenses: {}".format(unknown_licenses))
+        print_log_to_stderr("Stale versions: {}".format(stale_licenses))
         sys.exit(1)
 
 
@@ -623,11 +667,18 @@ if __name__ == "__main__":
             help='Module prefix whose licenses.yaml entries the release 
LICENSE omits. Repeatable. A dependency '
                  'that ships in the release must be registered under at least 
one module not matching these.'
         )
+        parser.add_argument(
+            '--stale-versions',
+            choices=['fail', 'warn'],
+            default='fail',
+            help='Whether a dependency whose previous version is registered 
with the same license fails the check '
+                 '(default) or only prints a warning.'
+        )
         args = parser.parse_args()
 
         license_yaml = args.license_yaml
         dependency_reports_root = args.dependency_reports_root
-        check_licenses(license_yaml, dependency_reports_root, 
args.exclude_module_prefix)
+        check_licenses(license_yaml, dependency_reports_root, 
args.exclude_module_prefix, args.stale_versions)
 
     except KeyboardInterrupt:
         print('Interrupted, closing.')
diff --git a/distribution/bin/test_check_licenses.py 
b/distribution/bin/test_check_licenses.py
index 4eee01a8bee..1859b13acb5 100755
--- a/distribution/bin/test_check_licenses.py
+++ b/distribution/bin/test_check_licenses.py
@@ -15,9 +15,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import contextlib
 import importlib.util
 import io
 import os
+import tempfile
 import unittest
 
 spec = importlib.util.spec_from_file_location(
@@ -58,6 +60,77 @@ class FindMissingLicensesTest(unittest.TestCase):
         self.assertEqual([], find_missing([AZURE], [], skipping={GAV: {}}))
 
 
+class SplitStaleVersionsTest(unittest.TestCase):
+    MIT = "MIT License"
+    NEW = check_licenses.get_dep_key("org.example", "a", "1.1")
+
+    def split(self, registered, reporting_modules=(AZURE,), 
reported_license=APACHE, also_reported=()):
+        # registered: (version, license, modules) of each org.example:a 
registration;
+        # also_reported: "g:a:v" and module pairs reported next to 
org.example:a:1.1.
+        registered_dep_to_licenses = {}
+        registered_modules = {}
+        for version, license, modules in registered:
+            key = check_licenses.get_dep_key("org.example", "a", version)
+            registered_dep_to_licenses[key] = license
+            registered_modules[key] = set(modules)
+        reported_modules = reports(*also_reported)
+        reported_modules[self.NEW] = set(reporting_modules)
+        missing = [(", ".join(reporting_modules),) + self.NEW + 
(reported_license,)]
+        return check_licenses.split_stale_versions(
+            missing, reported_modules, registered_dep_to_licenses, 
registered_modules, EXCLUDED
+        )
+
+    def assertStale(self, registered_versions, split, module=AZURE):
+        self.assertEqual(([(module,) + self.NEW + (APACHE, 
registered_versions)], []), split)
+
+    def assertMissing(self, split, reported_license=APACHE):
+        self.assertEqual(([], [(AZURE,) + self.NEW + (reported_license,)]), 
split)
+
+    def 
test_registered_under_another_version_with_the_same_license_is_stale(self):
+        self.assertStale("1.0", self.split([("1.0", APACHE, [AZURE])]))
+
+    def test_registered_under_another_version_of_another_module_is_stale(self):
+        self.assertStale("1.0", self.split([("1.0", APACHE, ["java-core"])]))
+
+    def test_unregistered_is_missing(self):
+        self.assertMissing(self.split([]))
+
+    def 
test_registered_under_another_version_with_another_license_is_missing(self):
+        self.assertMissing(self.split([("1.0", self.MIT, [AZURE])]))
+
+    def 
test_registered_under_versions_with_different_licenses_is_missing(self):
+        self.assertMissing(self.split([("1.0", APACHE, [AZURE]), ("0.9", 
self.MIT, ["java-core"])]))
+
+    def 
test_registered_under_several_versions_with_the_same_license_is_stale(self):
+        self.assertStale("0.9, 1.0", self.split([("1.0", APACHE, [AZURE]), 
("0.9", APACHE, ["java-core"])]))
+
+    def 
test_registered_only_under_excluded_module_is_missing_when_shipped(self):
+        self.assertMissing(self.split([("1.0", APACHE, [RANGER])]))
+
+    def 
test_current_version_registered_only_under_excluded_module_is_missing_when_shipped(self):
+        self.assertMissing(self.split([("1.1", APACHE, [RANGER]), ("1.0", 
APACHE, ["java-core"])]))
+
+    def 
test_registrations_under_excluded_modules_are_ignored_when_shipped(self):
+        self.assertStale("1.0", self.split([("1.0", APACHE, [AZURE]), ("0.9", 
self.MIT, [RANGER])]))
+
+    def test_registered_under_excluded_module_is_stale_when_not_shipped(self):
+        self.assertStale(
+            "1.0", self.split([("1.0", APACHE, [TESTCONTAINERS])], 
reporting_modules=(TESTCONTAINERS,)), module=TESTCONTAINERS
+        )
+
+    def test_registered_version_still_reported_is_missing(self):
+        # A module that starts bundling another version of a library is not a 
version bump.
+        self.assertMissing(self.split([("1.0", APACHE, ["java-core"])], 
also_reported=[("org.example:a:1.0", "java-core")]))
+
+    def test_one_registered_version_no_longer_reported_is_stale(self):
+        self.assertStale("0.9, 1.0", self.split(
+            [("1.0", APACHE, [AZURE]), ("0.9", APACHE, ["java-core"])], 
also_reported=[("org.example:a:0.9", "java-core")]
+        ))
+
+    def test_unknown_reported_license_is_missing(self):
+        self.assertMissing(self.split([("1.0", APACHE, [AZURE])], 
reported_license=None), reported_license=None)
+
+
 def find_unknown(license, reporting_modules, skipping=None):
     return check_licenses.find_unknown_licenses(
         {GAV: (license, reporting_modules[0])}, {GAV: set(reporting_modules)}, 
{GAV: ["Some New License"]}, skipping or {}, EXCLUDED
@@ -238,16 +311,19 @@ class RefreshVersionsTest(unittest.TestCase):
         self.assertEqual((text, [], []), self.refresh(text, 
reports(("org.s:a:1.1", "java-core"), ("org.c:a:1.1", "java-core"))))
 
 
-def parse_report(*licenses):
+def report_html(*licenses):
     row = ", ".join("<a>{}</a>".format(license) for license in licenses)
-    report = (
+    return (
         '<a id="Project_Dependencies_compile"></a><table>'
         
"<tr><th>GroupId</th><th>ArtifactId</th><th>Version</th><th>Type</th><th>Licenses</th></tr>"
         
"<tr><td>org.tukaani</td><td>xz</td><td>1.10</td><td>jar</td><td>{}</td></tr>"
         "</table>".format(row)
     )
+
+
+def parse_report(*licenses):
     parser = check_licenses.DependencyReportParser("java-core", 
check_licenses.build_compatible_license_names())
-    dep_to_license = parser.parse(io.StringIO(report))
+    dep_to_license = parser.parse(io.StringIO(report_html(*licenses)))
     return dep_to_license, parser.dep_to_license_names
 
 
@@ -291,6 +367,49 @@ class CompatibleLicenseNamesTest(unittest.TestCase):
         self.assertEqual([], sorted(set(compatible_license_names.values()) - 
set(compatible_license_names)))
 
 
+class CheckLicensesTest(unittest.TestCase):
+    # The report lists org.tukaani:xz:1.10 under the Apache license.
+
+    def check(self, registered_entry, stale_versions):
+        # Returns the stderr output and the exit code, None when the check 
passes.
+        with tempfile.TemporaryDirectory() as root:
+            os.makedirs(os.path.join(root, "licenses", "bin"))
+            os.makedirs(os.path.join(root, "reports", "core"))
+            with open(os.path.join(root, "licenses.yaml"), "w", 
encoding="utf-8") as f:
+                f.write(licenses_yaml(registered_entry))
+            with open(os.path.join(root, "reports", "core", 
"dependencies.html"), "w", encoding="utf-8") as f:
+                f.write(report_html("Apache-2.0"))
+            stderr = io.StringIO()
+            with contextlib.redirect_stderr(stderr):
+                try:
+                    check_licenses.check_licenses(
+                        os.path.join(root, "licenses.yaml"), 
os.path.join(root, "reports"), EXCLUDED, stale_versions
+                    )
+                except SystemExit as e:
+                    return stderr.getvalue(), e.code
+            return stderr.getvalue(), None
+
+    def assertMissing(self, registered_entry, stale_versions):
+        output, code = self.check(registered_entry, stale_versions)
+        self.assertEqual(1, code)
+        self.assertIn("Error: found 1 missing licenses", output)
+
+    def test_stale_version_fails_in_fail_mode(self):
+        output, code = self.check(entry("XZ", "java-core", "1.9", 
["org.tukaani: xz"]), "fail")
+        self.assertEqual(1, code)
+        self.assertIn("Error: found 1 dependencies whose previous versions are 
registered", output)
+        self.assertIn("refresh-license-versions.py licenses.yaml 
target/license-reports", output)
+
+    def test_stale_version_only_warns_in_warn_mode(self):
+        output, code = self.check(entry("XZ", "java-core", "1.9", 
["org.tukaani: xz"]), "warn")
+        self.assertIsNone(code)
+        self.assertIn("version: 1.10, license: Apache License version 2.0, 
registered_versions: 1.9", output)
+        self.assertIn("refresh-license-versions.py licenses.yaml 
target/license-reports", output)
+
+    def test_unregistered_dependency_fails_in_warn_mode(self):
+        self.assertMissing(entry("Other", "java-core", "1.9", ["org.example: 
other"]), "warn")
+
+
 class LoadLicensesYamlTest(unittest.TestCase):
     def test_versions_that_look_like_numbers_are_loaded_as_strings(self):
         docs = check_licenses.licenses_yaml.load_all("version: 
1.10\n---\nversion: 2\n---\nversion: 1.5.7-11\n")
diff --git a/distribution/pom.xml b/distribution/pom.xml
index ff0ead00c6a..bff170aeadc 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -50,6 +50,8 @@
     <properties>
         <!-- the default value is a repeated flag from the command line, since 
blank value is not allowed -->
         
<druid.distribution.pulldeps.opts>--clean</druid.distribution.pulldeps.opts>
+        <!-- PR CI sets this to warn; release builds must fail on stale 
licenses.yaml versions -->
+        
<druid.distribution.licenses.staleVersions>fail</druid.distribution.licenses.staleVersions>
     </properties>
 
     <build>
@@ -343,6 +345,7 @@
                                              LICENSE, which leaves out contrib 
entries. -->
                                         
<argument>--exclude-module-prefix=extensions-contrib/</argument>
                                         
<argument>--exclude-module-prefix=extensions/druid-testcontainers</argument>
+                                        
<argument>--stale-versions=${druid.distribution.licenses.staleVersions}</argument>
                                     </arguments>
                                 </configuration>
                             </execution>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to