diff --git a/java-shared-dependencies/.github/workflows/upper-bound-check.yaml b/java-shared-dependencies/.github/workflows/upper-bound-check.yaml
deleted file mode 100644
index 45cb8b9d95..0000000000
--- a/java-shared-dependencies/.github/workflows/upper-bound-check.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-on:
- push:
- branches:
- - main
- pull_request:
-name: upper-bound-check
-jobs:
- upper-bound-check:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: stCarolas/setup-maven@v4
- with:
- maven-version: 3.8.1
- - uses: actions/setup-java@v1
- with:
- java-version: 8
- - run: java -version
- - name: Install the BOM to local Maven repository
- run: .kokoro/build.sh
- - name: Check the BOM content satisfies the upper-bound-check test case
- run: mvn -B -V -ntp verify -Dcheckstyle.skip
- working-directory: upper-bound-check
diff --git a/java-shared-dependencies/.github/workflows/version-check.yaml b/java-shared-dependencies/.github/workflows/version-check.yaml
new file mode 100644
index 0000000000..b78a560fbc
--- /dev/null
+++ b/java-shared-dependencies/.github/workflows/version-check.yaml
@@ -0,0 +1,41 @@
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+name: version-check
+jobs:
+ upper-bound-check:
+ name: Upper-bound check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: stCarolas/setup-maven@v4
+ with:
+ maven-version: 3.8.1
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 8
+ - run: java -version
+ - name: Install the BOM to local Maven repository
+ run: .kokoro/build.sh
+ - name: Check the BOM content satisfies the upper-bound-check test case
+ run: mvn -B -V -ntp verify -Dcheckstyle.skip
+ working-directory: upper-bound-check
+ grpc-convergence-check:
+ name: gRPC dependency convergence check
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: stCarolas/setup-maven@v4
+ with:
+ maven-version: 3.8.1
+ - uses: actions/setup-java@v1
+ with:
+ java-version: 8
+ - run: java -version
+ - name: Install the BOM to local Maven repository
+ run: .kokoro/build.sh
+ - name: Check the BOM content satisfies the dependency-convergence-check test
+ run: mvn -B -V -ntp verify -Dcheckstyle.skip
+ working-directory: dependency-convergence-check
diff --git a/java-shared-dependencies/dependency-convergence-check/README.md b/java-shared-dependencies/dependency-convergence-check/README.md
new file mode 100644
index 0000000000..d593196ece
--- /dev/null
+++ b/java-shared-dependencies/dependency-convergence-check/README.md
@@ -0,0 +1,6 @@
+# Dependency Convergence Check
+
+This project includes a test case for dependency convergence for some (not all)
+artifacts in the Google Cloud Shared Dependencies BOM.
+
+This project is not meant to be used by end-users or published to Maven Central.
diff --git a/java-shared-dependencies/dependency-convergence-check/pom.xml b/java-shared-dependencies/dependency-convergence-check/pom.xml
new file mode 100644
index 0000000000..3e0edf56c6
--- /dev/null
+++ b/java-shared-dependencies/dependency-convergence-check/pom.xml
@@ -0,0 +1,76 @@
+
+
+ 4.0.0
+ com.google.cloud
+ shared-dependencies-dependency-convergence-test
+ 2.7.0
+ Dependency convergence test for certain artifacts in Google Cloud Shared Dependencies
+ https://github.com/googleapis/java-shared-dependencies
+
+ An dependency convergence test case for the shared dependencies BOM. A failure of this test case means
+ the shared dependencies BOM has outdated dependencies; there are newer version of artifacts
+ appearing in the dependency tree.
+
+
+
+ Google LLC
+
+
+
+ scm:git:git@github.com:googleapis/java-shared-dependencies.git
+ scm:git:git@github.com:googleapis/java-shared-dependencies.git
+ https://github.com/googleapis/java-shared-dependencies
+ HEAD
+
+
+
+ https://github.com/googleapis/java-shared-dependencies/issues
+ GitHub Issues
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+
+ com.google.cloud
+ google-cloud-shared-dependencies
+ 2.7.0
+ pom
+ import
+
+
+
+
+
+
+ com.google.guava
+ guava
+ 31.0.1-jre
+ test
+
+
+ com.google.cloud.tools
+ dependencies
+ 1.5.12
+ test
+
+
+ junit
+ junit
+ 4.13.1
+ test
+
+
+
diff --git a/java-shared-dependencies/dependency-convergence-check/src/test/java/com/google/cloud/DependencyConvergenceTest.java b/java-shared-dependencies/dependency-convergence-check/src/test/java/com/google/cloud/DependencyConvergenceTest.java
new file mode 100644
index 0000000000..6552438363
--- /dev/null
+++ b/java-shared-dependencies/dependency-convergence-check/src/test/java/com/google/cloud/DependencyConvergenceTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2022 Google LLC.
+ *
+ * Licensed 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 com.google.cloud;
+
+import static org.junit.Assert.assertTrue;
+
+import com.google.cloud.tools.opensource.classpath.ClassPathBuilder;
+import com.google.cloud.tools.opensource.classpath.ClassPathEntry;
+import com.google.cloud.tools.opensource.classpath.ClassPathResult;
+import com.google.cloud.tools.opensource.classpath.DependencyMediation;
+import com.google.cloud.tools.opensource.dependencies.Bom;
+import com.google.cloud.tools.opensource.dependencies.DependencyPath;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.nio.file.Paths;
+import java.util.HashSet;
+import java.util.Set;
+import org.eclipse.aether.artifact.Artifact;
+import org.junit.Test;
+
+/**
+ * Test to ensure that certain artifacts in the dependency tree of each entry of the shared
+ * dependencies BOM have the same version.
+ */
+public class DependencyConvergenceTest {
+
+ @Test
+ public void testGrpcConvergence() throws Exception {
+ // There were cases where the gRPC version set in the shared dependencies BOM and the gRPC
+ // version declared in gax-grpc's dependency broke the build of downstream projects. Two
+ // artifacts were using version range to specify exact gRPC versions.
+ // https://github.com/googleapis/java-shared-dependencies/pull/595
+ Bom bom = Bom.readBom(Paths.get("../pom.xml"));
+ assertConvergence(
+ bom,
+ "io.grpc",
+ "grpc-netty-shaded",
+ ImmutableSet.of(
+ // Because OpenCensus's gRPC version does not use version range notation, it does not
+ // break downstream build
+ "opencensus-exporter-trace-stackdriver",
+ "opencensus-exporter-stats-stackdriver",
+ // Because grpc-gcp's gRPC version does not use version range notation, it does not
+ // break downstream build
+ "grpc-gcp"));
+ }
+
+ /**
+ * Asserts the artifact specified at {@code groupId} and {@code artifactId} have the same version
+ * across the dependency trees of the managed dependencies of {@code bom}.
+ *
+ *
Use {@code excludingArtifactIds} to ignore certain artifacts.
+ */
+ private void assertConvergence(
+ Bom bom, String groupId, String artifactId, Set excludingArtifactIds)
+ throws Exception {
+ ImmutableList managedDependencies = bom.getManagedDependencies();
+
+ Set foundPaths = new HashSet<>();
+ Set foundVersions = new HashSet<>();
+ for (Artifact managedDependency : managedDependencies) {
+ if (excludingArtifactIds.contains(managedDependency.getArtifactId())) {
+ continue;
+ }
+
+ ClassPathBuilder classPathBuilder = new ClassPathBuilder();
+ ClassPathResult result =
+ classPathBuilder.resolve(
+ ImmutableList.of(managedDependency), false, DependencyMediation.MAVEN);
+ ImmutableList classPath = result.getClassPath();
+ for (ClassPathEntry entry : classPath) {
+ Artifact artifact = entry.getArtifact();
+ if (artifactId.equals(artifact.getArtifactId()) && groupId.equals(artifact.getGroupId())) {
+ ImmutableList dependencyPaths = result.getDependencyPaths(entry);
+ foundPaths.add(dependencyPaths.get(0));
+ foundVersions.add(artifact.getVersion());
+ }
+ }
+ }
+
+ assertTrue(
+ "There should be at least one version in the graph but empty; "
+ + "please check the assertion is using correct groupID and artifactID.",
+ foundVersions.size() >= 1);
+
+ // Duplicate versions found in the dependency trees
+ assertTrue(
+ "The version for "
+ + groupId
+ + ":"
+ + artifactId
+ + " should be one but there are multiple of them: "
+ + Joiner.on(", ").join(foundPaths),
+ foundVersions.size() <= 1);
+ }
+}