diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index 907475adb0..866f684564 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -18,6 +18,8 @@ on: - "**/*.java" - "**/BUILD.bazel" - "MODULE.bazel" + - "pom.xml" + - "scripts/sync_bazel_dependencies.py" - ".bazelversion" - ".bazelrc" - ".github/workflows/bazel.yml" @@ -27,6 +29,8 @@ on: - "**/*.java" - "**/BUILD.bazel" - "MODULE.bazel" + - "pom.xml" + - "scripts/sync_bazel_dependencies.py" - ".bazelversion" - ".bazelrc" - ".github/workflows/bazel.yml" @@ -74,11 +78,14 @@ jobs: path: | ~/.cache/bazel-disk-cache ~/.cache/bazel/cache/repos/v1 - key: bazel-${{ runner.os }}-java${{ matrix.java }}-${{ hashFiles('MODULE.bazel', '.bazelversion', 'maven_install.json') }} + key: bazel-${{ runner.os }}-java${{ matrix.java }}-${{ hashFiles('MODULE.bazel', 'pom.xml', 'scripts/sync_bazel_dependencies.py', '.bazelversion', 'maven_install.json') }} restore-keys: | bazel-${{ runner.os }}-java${{ matrix.java }}- bazel-${{ runner.os }}- + - name: Verify Bazel dependency sync + run: python3 scripts/sync_bazel_dependencies.py --check + # Re-generate the Maven lock file so it is always consistent with # MODULE.bazel. The __INPUT_ARTIFACTS_HASH in the committed file is # intentionally set to -1 to signal that the file needs to be regenerated; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4274c74da3..73e36ef669 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,5 +20,15 @@ If you have any problem with the package or any suggestions, please file an [iss 3. Submit a pull request. 4. The bot will automatically assign someone to review your PR. Check the full list of bot commands [here](https://prow.k8s.io/command-help). +### Sync Bazel dependency versions + +The root `pom.xml` is the source of truth for Maven and Bazel dependency versions. +If you update a managed dependency version there, also regenerate the Bazel dependency +block and lock file before sending your PR: + +1. Run `python3 scripts/sync_bazel_dependencies.py` +2. Run `REPIN=1 bazel run @maven//:pin` +3. Commit the updated `MODULE.bazel` and `maven_install.json` + ### Contact You can reach the maintainers of this project at [SIG API Machinery](https://github.com/kubernetes/community/tree/master/sig-api-machinery) or on the [#kubernetes-client](https://kubernetes.slack.com/messages/kubernetes-client) channel on the Kubernetes slack. diff --git a/MODULE.bazel b/MODULE.bazel index 26f7670721..91728b2e60 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -25,59 +25,60 @@ bazel_dep(name = "contrib_rules_jvm", version = "0.27.0") maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") +# BEGIN: generated by scripts/sync_bazel_dependencies.py +# Generated from pom.xml by scripts/sync_bazel_dependencies.py. +# Do not edit this block by hand; update pom.xml and rerun the script instead. +# Artifact order follows pom.xml dependencyManagement order within each section. maven.install( artifacts = [ # ---- core ---- - # Versions kept in sync with the property values in the root pom.xml. - "jakarta.annotation:jakarta.annotation-api:3.0.0", - "io.swagger:swagger-annotations:1.6.16", - "com.squareup.okhttp3:okhttp:5.3.2", - "com.squareup.okhttp3:okhttp-jvm:5.3.2", - "com.squareup.okhttp3:logging-interceptor:5.3.2", - "com.squareup.okio:okio:3.16.4", - "com.squareup.okio:okio-jvm:3.16.4", - "com.google.code.gson:gson:2.14.0", - "com.fasterxml.jackson.core:jackson-databind:2.21.2", - "com.fasterxml.jackson.core:jackson-core:2.21.2", - "com.fasterxml.jackson.core:jackson-annotations:2.21", - "io.gsonfire:gson-fire:1.9.0", "org.apache.commons:commons-lang3:3.20.0", "org.apache.commons:commons-collections4:4.5.0", + "org.yaml:snakeyaml:2.6", + "commons-codec:commons-codec:1.22.0", "org.apache.commons:commons-compress:1.28.0", "commons-io:commons-io:2.22.0", - "commons-codec:commons-codec:1.22.0", - "org.yaml:snakeyaml:2.6", - "org.slf4j:slf4j-api:2.0.17", + "com.github.ben-manes.caffeine:caffeine:3.0.0", + "org.slf4j:slf4j-api:2.0.18", "org.bouncycastle:bcpkix-jdk18on:1.84", "org.bouncycastle:bcprov-jdk18on:1.84", + "software.amazon.awssdk:sts:2.44.8", + "software.amazon.awssdk:auth:2.44.8", + "software.amazon.awssdk:http-auth-aws:2.44.8", + "software.amazon.awssdk:http-auth-spi:2.44.8", + "software.amazon.awssdk:http-client-spi:2.44.8", + "software.amazon.awssdk:utils:2.44.8", "com.google.protobuf:protobuf-java:4.34.1", "org.bitbucket.b_c:jose4j:0.9.6", - "com.google.auth:google-auth-library-oauth2-http:1.46.0", - "software.amazon.awssdk:auth:2.43.0", - "software.amazon.awssdk:http-auth-aws:2.43.0", - "software.amazon.awssdk:http-auth-spi:2.43.0", - "software.amazon.awssdk:http-client-spi:2.43.0", - "software.amazon.awssdk:sts:2.43.0", - "software.amazon.awssdk:utils:2.43.0", + "com.bucket4j:bucket4j-core:8.10.1", "io.prometheus:simpleclient:0.16.0", "io.prometheus:simpleclient_httpserver:0.16.0", - "com.bucket4j:bucket4j-core:8.10.1", - "com.github.ben-manes.caffeine:caffeine:3.0.0", - "com.flipkart.zjsonpatch:zjsonpatch:0.4.16", + "com.google.code.gson:gson:2.14.0", + "com.fasterxml.jackson.core:jackson-databind:2.21.3", + "com.fasterxml.jackson.core:jackson-annotations:2.21", + "com.fasterxml.jackson.core:jackson-core:2.21.3", + "io.gsonfire:gson-fire:1.9.0", + "com.squareup.okhttp3:okhttp:5.3.2", + "com.squareup.okhttp3:logging-interceptor:5.3.2", + "io.swagger:swagger-annotations:1.6.16", + "jakarta.annotation:jakarta.annotation-api:3.0.0", + "com.google.auth:google-auth-library-oauth2-http:1.47.0", "org.jetbrains:annotations:26.1.0", "org.reflections:reflections:0.10.2", + "com.squareup.okhttp3:okhttp-jvm:5.3.2", + "com.squareup.okio:okio:3.16.4", + "com.squareup.okio:okio-jvm:3.16.4", + "com.flipkart.zjsonpatch:zjsonpatch:0.4.16", # ---- spring (Java 17+ modules) ---- - # spring-boot 4.0.6 transitively brings in spring-framework 7.0.x, but the - # root pom.xml pins spring.version=6.2.12. Explicitly declare all - # spring-framework modules at 6.2.12 and use maven.artifact() with - # exclusions on the spring-boot artifacts so Coursier does not upgrade them. - "org.springframework:spring-aop:6.2.12", - "org.springframework:spring-beans:6.2.12", - "org.springframework:spring-context:6.2.12", - "org.springframework:spring-core:6.2.12", - "org.springframework:spring-expression:6.2.12", - "org.springframework:spring-test:6.2.12", + "org.springframework:spring-core:6.2.8", + "org.springframework:spring-aop:6.2.8", + "org.springframework:spring-beans:6.2.8", + "org.springframework:spring-context:6.2.8", + "org.springframework:spring-expression:6.2.8", + "org.springframework:spring-test:6.2.8", # ---- test ---- + "ch.qos.logback:logback-classic:1.5.32", + "ch.qos.logback:logback-core:1.5.32", "org.junit.jupiter:junit-jupiter-api:5.13.4", "org.junit.jupiter:junit-jupiter-engine:5.13.4", "org.junit.jupiter:junit-jupiter-params:5.13.4", @@ -90,10 +91,8 @@ maven.install( "uk.org.webcompere:system-stubs-jupiter:2.1.8", "uk.org.webcompere:system-stubs-core:2.1.8", "org.wiremock:wiremock:3.13.2", - "org.awaitility:awaitility:4.3.0", "org.assertj:assertj-core:3.27.7", - "ch.qos.logback:logback-classic:1.5.32", - "ch.qos.logback:logback-core:1.5.32", + "org.awaitility:awaitility:4.3.0", ], repositories = [ "https://repo1.maven.org/maven2", @@ -103,14 +102,13 @@ maven.install( lock_file = "//:maven_install.json", ) -# Spring Boot 4.0.6 artifacts declared with exclusions to pin spring-framework at -# 6.2.12 (matching pom.xml spring.version) instead of the 7.0.x that spring-boot -# 4.0.x normally pulls in. +# Spring Boot 4.0.6 artifacts declared with exclusions to keep +# spring-framework pinned to 6.2.8. _SPRING_FRAMEWORK_EXCLUSIONS = [ + "org.springframework:spring-core", "org.springframework:spring-aop", "org.springframework:spring-beans", "org.springframework:spring-context", - "org.springframework:spring-core", "org.springframework:spring-expression", "org.springframework:spring-test", ] @@ -143,4 +141,6 @@ maven.artifact( exclusions = _SPRING_FRAMEWORK_EXCLUSIONS, ) +# END: generated by scripts/sync_bazel_dependencies.py + use_repo(maven, "maven") diff --git a/pom.xml b/pom.xml index ce8a92b966..9eb97e24cb 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ 3.0.0 4.34.1 5.13.4 - 5.13.4 + 1.13.4 ${junit-jupiter.version} 8.10.1 1.84 @@ -73,6 +73,18 @@ 6.2.8 0.16.0 0.10.2 + 2.44.8 + 1.47.0 + 0.9.6 + 26.1.0 + 1.5.32 + 5.23.0 + 3.16.4 + 3.27.7 + 4.3.0 + 2.1.8 + 3.13.2 + 0.4.16 org.apache.commons commons-lang3 @@ -161,10 +174,40 @@ bcpkix-jdk18on ${bouncycastle.version} + + org.bouncycastle + bcprov-jdk18on + ${bouncycastle.version} + software.amazon.awssdk sts - 2.44.8 + ${aws.sdk.version} + + + software.amazon.awssdk + auth + ${aws.sdk.version} + + + software.amazon.awssdk + http-auth-aws + ${aws.sdk.version} + + + software.amazon.awssdk + http-auth-spi + ${aws.sdk.version} + + + software.amazon.awssdk + http-client-spi + ${aws.sdk.version} + + + software.amazon.awssdk + utils + ${aws.sdk.version} com.google.protobuf @@ -174,7 +217,7 @@ org.bitbucket.b_c jose4j - 0.9.6 + ${jose4j.version} com.bucket4j @@ -196,11 +239,26 @@ spring-core ${spring.version} + + org.springframework + spring-aop + ${spring.version} + + + org.springframework + spring-beans + ${spring.version} + org.springframework spring-context ${spring.version} + + org.springframework + spring-expression + ${spring.version} + org.springframework.boot spring-boot-autoconfigure @@ -274,19 +332,57 @@ com.google.auth google-auth-library-oauth2-http - 1.47.0 + ${google.auth.version} org.jetbrains annotations - 26.1.0 + ${jetbrains.annotations.version} + + + org.reflections + reflections + ${reflections.version} + + + com.squareup.okhttp3 + okhttp-jvm + ${okhttp3.version} + + + com.squareup.okio + okio + ${okio.version} + + + com.squareup.okio + okio-jvm + ${okio.version} ch.qos.logback logback-classic - 1.5.32 + ${logback.version} + test + + + ch.qos.logback + logback-core + ${logback.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} test @@ -301,22 +397,58 @@ ${junit-jupiter.version} test + + org.junit.platform + junit-platform-launcher + ${junit-platform.version} + test + + + org.junit.platform + junit-platform-commons + ${junit-platform.version} + test + + + org.junit.platform + junit-platform-engine + ${junit-platform.version} + test + + + org.junit.platform + junit-platform-reporting + ${junit-platform.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + org.mockito mockito-junit-jupiter - 5.23.0 + ${mockito.version} test uk.org.webcompere system-stubs-jupiter - 2.1.8 + ${system-stubs.version} + test + + + uk.org.webcompere + system-stubs-core + ${system-stubs.version} test org.wiremock wiremock - 3.13.2 + ${wiremock.version} test @@ -334,7 +466,7 @@ com.flipkart.zjsonpatch zjsonpatch - 0.4.16 + ${zjsonpatch.version} com.fasterxml.jackson.core @@ -345,13 +477,13 @@ org.assertj assertj-core - 3.27.7 + ${assertj.version} test org.awaitility awaitility - 4.3.0 + ${awaitility.version} test diff --git a/scripts/sync_bazel_dependencies.py b/scripts/sync_bazel_dependencies.py new file mode 100644 index 0000000000..bbef5a7281 --- /dev/null +++ b/scripts/sync_bazel_dependencies.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + + +GENERATED_START = "# BEGIN: generated by scripts/sync_bazel_dependencies.py" +GENERATED_END = "# END: generated by scripts/sync_bazel_dependencies.py" +MAX_PROPERTY_RESOLUTION_DEPTH = 20 +SECTION_CORE = "core" +SECTION_SPRING = "spring (Java 17+ modules)" +SECTION_TEST = "test" +SCOPE_TEST = "test" +SPRING_BOOT_GROUP = "org.springframework.boot" +SPRING_FRAMEWORK_GROUP = "org.springframework" +SPRING_BOOT_COORDINATE = "org.springframework.boot:spring-boot" +SPRING_CORE_COORDINATE = "org.springframework:spring-core" +EXCLUDED_BAZEL_COORDINATES = { + "commons-cli:commons-cli", + "com.google.code.findbugs:jsr305", + "org.junit.jupiter:junit-jupiter", +} + + +@dataclass(frozen=True) +class ManagedDependency: + group_id: str + artifact_id: str + version: str + scope: str | None = None + + @property + def coordinate(self) -> str: + return f"{self.group_id}:{self.artifact_id}" + + @property + def bazel_artifact(self) -> str: + return f"{self.coordinate}:{self.version}" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Sync Bazel dependency declarations from the root pom.xml." + ) + parser.add_argument( + "--root", + type=Path, + default=Path(__file__).resolve().parents[1], + help="Repository root containing pom.xml and MODULE.bazel.", + ) + parser.add_argument( + "--check", + action="store_true", + help="Fail if MODULE.bazel is not up to date.", + ) + return parser.parse_args() + + +def local_name(tag: str) -> str: + return tag.split("}", 1)[-1] + + +def parse_managed_dependencies(pom_path: Path) -> list[ManagedDependency]: + root = ET.parse(pom_path).getroot() + namespace = {} + if root.tag.startswith("{"): + namespace["m"] = root.tag[1:].split("}", 1)[0] + prefix = "m:" + else: + prefix = "" + + properties_element = root.find(f"{prefix}properties", namespace) + properties = {} + if properties_element is not None: + for child in properties_element: + properties[local_name(child.tag)] = (child.text or "").strip() + + project_version = (root.findtext(f"{prefix}version", default="", namespaces=namespace) or "").strip() + if project_version: + properties.setdefault("project.version", project_version) + properties.setdefault("pom.version", project_version) + properties.setdefault("version", project_version) + + def resolve(value: str) -> str: + resolved = value + for _ in range(MAX_PROPERTY_RESOLUTION_DEPTH): + updated = re.sub( + r"\$\{([^}]+)\}", + lambda match: properties.get(match.group(1), match.group(0)), + resolved, + ) + if updated == resolved: + return updated + resolved = updated + raise ValueError(f"Could not fully resolve property expression: {value}") + + managed_dependencies = [] + dependency_path = f"{prefix}dependencyManagement/{prefix}dependencies/{prefix}dependency" + for dependency in root.findall(dependency_path, namespace): + group_id = dependency.findtext(f"{prefix}groupId", default="", namespaces=namespace).strip() + artifact_id = dependency.findtext( + f"{prefix}artifactId", default="", namespaces=namespace + ).strip() + version = dependency.findtext(f"{prefix}version", default="", namespaces=namespace).strip() + scope = dependency.findtext(f"{prefix}scope", default="", namespaces=namespace).strip() or None + if group_id and artifact_id and version: + managed_dependencies.append( + ManagedDependency( + group_id=group_id, + artifact_id=artifact_id, + version=resolve(version), + scope=scope, + ) + ) + return managed_dependencies + + +def classify_dependency(dependency: ManagedDependency) -> str: + # Keep all org.springframework modules together so the generated spring + # section also doubles as the exclusion list for the spring-boot artifacts. + # The group_id check intentionally runs before the scope check so spring-test + # stays in the spring section even though its Maven scope is test. + if dependency.group_id == SPRING_FRAMEWORK_GROUP: + return SECTION_SPRING + if dependency.scope == SCOPE_TEST: + return SECTION_TEST + return SECTION_CORE + + +def partition_dependencies( + managed_dependencies: list[ManagedDependency], +) -> tuple[dict[str, list[ManagedDependency]], list[ManagedDependency]]: + """Split dependencyManagement entries into Bazel install sections and spring boot artifacts.""" + install_sections = { + SECTION_CORE: [], + SECTION_SPRING: [], + SECTION_TEST: [], + } + spring_boot_dependencies = [] + + for dependency in managed_dependencies: + if dependency.group_id == SPRING_BOOT_GROUP: + spring_boot_dependencies.append(dependency) + continue + if dependency.coordinate in EXCLUDED_BAZEL_COORDINATES: + continue + install_sections[classify_dependency(dependency)].append(dependency) + + if not spring_boot_dependencies: + raise ValueError("No org.springframework.boot dependencies found in dependencyManagement.") + + return install_sections, spring_boot_dependencies + + +def find_dependency( + dependencies: list[ManagedDependency], coordinate: str +) -> ManagedDependency: + """Return the dependency matching a group/artifact coordinate or raise ValueError.""" + for dependency in dependencies: + if dependency.coordinate == coordinate: + return dependency + raise ValueError(f"Missing expected dependencyManagement entry: {coordinate}") + + +def render_generated_block(managed_dependencies: list[ManagedDependency]) -> str: + install_sections, spring_boot_dependencies = partition_dependencies(managed_dependencies) + spring_framework_dependencies = install_sections[SECTION_SPRING] + if not spring_framework_dependencies: + raise ValueError("No spring-framework dependencies found in dependencyManagement.") + spring_framework_exclusions = [ + dependency.coordinate for dependency in spring_framework_dependencies + ] + + lines = [ + GENERATED_START, + "# Generated from pom.xml by scripts/sync_bazel_dependencies.py.", + "# Do not edit this block by hand; update pom.xml and rerun the script instead.", + "# Artifact order follows pom.xml dependencyManagement order within each section.", + "maven.install(", + " artifacts = [", + ] + for section, dependencies in install_sections.items(): + lines.append(f" # ---- {section} ----") + for dependency in dependencies: + lines.append(f' "{dependency.bazel_artifact}",') + spring_boot_version = find_dependency( + spring_boot_dependencies, SPRING_BOOT_COORDINATE + ).version + spring_framework_version = find_dependency( + spring_framework_dependencies, SPRING_CORE_COORDINATE + ).version + lines.extend( + [ + " ],", + " repositories = [", + ' "https://repo1.maven.org/maven2",', + ' "https://repo.spring.io/milestone",', + " ],", + " fetch_sources = False,", + ' lock_file = "//:maven_install.json",', + ")", + "", + f"# Spring Boot {spring_boot_version} artifacts declared with exclusions to keep", + f"# spring-framework pinned to {spring_framework_version}.", + "_SPRING_FRAMEWORK_EXCLUSIONS = [", + ] + ) + for exclusion in spring_framework_exclusions: + lines.append(f' "{exclusion}",') + lines.extend(["]", ""]) + + for dependency in spring_boot_dependencies: + lines.extend( + [ + "maven.artifact(", + f' artifact = "{dependency.artifact_id}",', + ' group = "org.springframework.boot",', + f' version = "{dependency.version}",', + " exclusions = _SPRING_FRAMEWORK_EXCLUSIONS,", + ")", + "", + ] + ) + + spring_boot_versions = {dependency.version for dependency in spring_boot_dependencies} + if len(spring_boot_versions) != 1: + raise ValueError( + "Spring Boot artifact versions diverged in dependencyManagement: " + + ", ".join(sorted(spring_boot_versions)) + ) + + lines.append(GENERATED_END) + return "\n".join(lines) + "\n" + + +def sync_module(module_path: Path, generated_block: str, check_only: bool) -> bool: + module_text = module_path.read_text() + pattern = re.compile( + rf"{re.escape(GENERATED_START)}\n.*?{re.escape(GENERATED_END)}\n?", + re.DOTALL, + ) + match = pattern.search(module_text) + if not match: + raise ValueError( + f"Could not find generated block markers in {module_path}. " + f"Expected {GENERATED_START!r} and {GENERATED_END!r}." + ) + + updated_text = module_text[: match.start()] + generated_block + module_text[match.end() :] + changed = updated_text != module_text + if changed and not check_only: + module_path.write_text(updated_text) + return changed + + +def main() -> int: + args = parse_args() + root = args.root.resolve() + pom_path = root / "pom.xml" + module_path = root / "MODULE.bazel" + + managed_dependencies = parse_managed_dependencies(pom_path) + generated_block = render_generated_block(managed_dependencies) + changed = sync_module(module_path, generated_block, args.check) + + if args.check and changed: + print(f"{module_path} is out of date. Run {Path(__file__).name} to regenerate it.") + return 1 + + if not args.check and changed: + print(f"Updated {module_path}") + elif not args.check: + print(f"{module_path} is already up to date") + return 0 + + +if __name__ == "__main__": + sys.exit(main())