diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b71cf6b9e9..e47c90adce 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -299,3 +299,25 @@ jobs: with: bom-path: gapic-generator-java-bom/pom.xml + unmanaged_dependency_check: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - uses: actions/setup-java@v3 + with: + java-version: 11 + distribution: temurin + - run: mvn -version + - name: Unit Tests + run: | + mvn test --batch-mode --no-transfer-progress + working-directory: java-shared-dependencies/unmanaged-dependency-check + - name: Install Maven modules + run: | + mvn install -B -ntp -DskipTests -Dclirr.skip -Dcheckstyle.skip + - name: Unmanaged dependency check + uses: ./java-shared-dependencies/unmanaged-dependency-check + with: + bom-path: gapic-generator-java-bom/pom.xml diff --git a/.github/workflows/create_additional_release_tag.yaml b/.github/workflows/create_additional_release_tag.yaml index 419cab0ac6..0ecaf63863 100644 --- a/.github/workflows/create_additional_release_tag.yaml +++ b/.github/workflows/create_additional_release_tag.yaml @@ -14,16 +14,14 @@ jobs: uses: actions/checkout@v3 with: token: ${{ secrets.GITHUB_TOKEN }} - - name: Set up Git run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - - name: Fetch all tags run: git fetch --tags - - name: Create additional tags + bash: shell run: | ARTIFACT_IDS=('google-cloud-shared-dependencies' 'api-common' 'gax') for ARTIFACT_ID in "${ARTIFACT_IDS[@]}"; do @@ -36,3 +34,10 @@ jobs: git tag $TAG_NAME git push origin $TAG_NAME done + # Generate a tag for unmanaged dependencies check. + # Use fixed tag so that checks in handwritten libraries do not need to + # update the version. + CHECK_LATEST_TAG="unmanaged-dependencies-check-latest" + git tag ${CHECK_LATEST_TAG} + git push origin -f ${CHECK_LATEST_TAG} + diff --git a/java-shared-dependencies/unmanaged-dependency-check/action.yaml b/java-shared-dependencies/unmanaged-dependency-check/action.yaml new file mode 100644 index 0000000000..e8988d3be1 --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/action.yaml @@ -0,0 +1,43 @@ + name: "Unmanaged dependency check" + description: "Checks whether there's a dependency that is not managed by java shared dependencies." + inputs: + bom-path: + description: "The relative path from the repository root to the pom.xml file" + required: true + runs: + using: "composite" + steps: + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 11 + cache: maven + - name: Set up Maven + uses: stCarolas/setup-maven@v4.5 + with: + maven-version: 3.8.2 + - name: Install latest Java shared dependencies + shell: bash + run: | + cd ${{ github.action_path }}/.. + echo "Install Java shared dependencies" + mvn clean install -V --batch-mode --no-transfer-progress -DskipTests + - name: Install check + shell: bash + run: | + cd ${{ github.action_path }} + echo "Install Unmanaged Dependency Check in $(pwd)" + mvn clean install -V --batch-mode --no-transfer-progress -DskipTests + - name: Run unmanaged dependency check + shell: bash + run: | + bom_absolute_path=$(realpath "${{ inputs.bom-path }}") + cd ${{ github.action_path }} + echo "Running Unmanaged Dependency Check against ${bom_absolute_path}" + unmanaged_dependencies=$(mvn exec:java -Dexec.args="../pom.xml ${bom_absolute_path}" -q) + if [[ "${unmanaged_dependencies}" != "[]" ]]; then + echo "This pull request seems to add new third-party dependency, ${unmanaged_dependencies}, among the artifacts listed in ${{ inputs.bom-path }}." + echo "Please see go/cloud-sdk-java-dependency-governance." + exit 1 + fi + diff --git a/java-shared-dependencies/unmanaged-dependency-check/pom.xml b/java-shared-dependencies/unmanaged-dependency-check/pom.xml new file mode 100644 index 0000000000..c632e59920 --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + com.google.cloud + unmanaged-dependency-check + 0.0.1-SNAPSHOT + Unmanaged dependency check + + + + + + 1.8 + 1.8 + UTF-8 + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.1 + + + + java + + + + + install-test-poms + test-compile + + exec + + + bash + + local-install.sh + + src/test/resources + + + + + com.google.cloud.UnmanagedDependencyCheck + + + + + + + + com.google.cloud.tools + dependencies + 1.5.13 + + + junit + junit + 4.13.2 + test + + + com.google.truth + truth + 1.1.5 + test + + + org.slf4j + slf4j-api + 2.0.9 + + + org.slf4j + slf4j-simple + 2.0.9 + + + + \ No newline at end of file diff --git a/java-shared-dependencies/unmanaged-dependency-check/src/main/java/com/google/cloud/UnmanagedDependencyCheck.java b/java-shared-dependencies/unmanaged-dependency-check/src/main/java/com/google/cloud/UnmanagedDependencyCheck.java new file mode 100644 index 0000000000..410c5f3966 --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/src/main/java/com/google/cloud/UnmanagedDependencyCheck.java @@ -0,0 +1,82 @@ +package com.google.cloud; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.cloud.tools.opensource.classpath.ClassPathBuilder; +import com.google.cloud.tools.opensource.classpath.DependencyMediation; +import com.google.cloud.tools.opensource.dependencies.Bom; +import com.google.cloud.tools.opensource.dependencies.MavenRepositoryException; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.version.InvalidVersionSpecificationException; + +/** + * A utility class to check unmanaged dependencies in BOM. + */ +public class UnmanagedDependencyCheck { + // regex of handwritten artifacts + private final static String downstreamArtifact = "(com.google.cloud:google-cloud-.*)|(com.google.api.grpc:(grpc|proto)-google-cloud-.*)"; + + + /** + * @param args An array with two elements.

The first string is the path of Java shared + * dependencies BOM.

The second string is the path of a pom.xml contains BOM. + */ + public static void main(String[] args) + throws MavenRepositoryException, InvalidVersionSpecificationException { + checkArgument(args.length == 2, "The length of the inputs should be 2"); + System.out.println(getUnmanagedDependencies(args[0], args[1])); + } + + /** + * Returns dependency coordinates that are not managed by shared dependency BOM. + * + * @param sharedDependenciesBomPath the path of shared dependency BOM + * @param projectBomPath the path of current project BOM + * @return a list of unmanaged dependencies by the given version of shared dependency BOM + * @throws MavenRepositoryException thrown if the artifacts in Bom can't be reached in remote or + * local Maven repository + * @throws InvalidVersionSpecificationException thrown if the shared dependency version can't be + * parsed + */ + public static List getUnmanagedDependencies( + String sharedDependenciesBomPath, String projectBomPath) + throws MavenRepositoryException, InvalidVersionSpecificationException { + Set sharedDependencies = getManagedDependencies(sharedDependenciesBomPath); + Set managedDependencies = getManagedDependencies(projectBomPath); + + return managedDependencies.stream() + .filter(dependency -> !sharedDependencies.contains(dependency)) + // handwritten artifacts, e.g., com.google.cloud:google-cloud-bigtable, should be excluded. + .filter(dependency -> !dependency.matches(downstreamArtifact)) + .collect(Collectors.toList()); + } + + private static Set getManagedDependencies(String projectBomPath) + throws MavenRepositoryException, InvalidVersionSpecificationException { + return getManagedDependenciesFromBom(Bom.readBom(Paths.get(projectBomPath))); + } + + private static Set getManagedDependenciesFromBom(Bom bom) + throws InvalidVersionSpecificationException { + Set res = new HashSet<>(); + new ClassPathBuilder() + .resolve(bom.getManagedDependencies(), true, DependencyMediation.MAVEN) + .getClassPath() + .forEach( + classPath -> { + Artifact artifact = classPath.getArtifact(); + res.add(String.format("%s:%s", artifact.getGroupId(), artifact.getArtifactId())); + }); + + return res; + } + + private UnmanagedDependencyCheck() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/java-shared-dependencies/unmanaged-dependency-check/src/test/java/com/google/cloud/UnmanagedDependencyCheckTest.java b/java-shared-dependencies/unmanaged-dependency-check/src/test/java/com/google/cloud/UnmanagedDependencyCheckTest.java new file mode 100644 index 0000000000..0f1e0e85da --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/src/test/java/com/google/cloud/UnmanagedDependencyCheckTest.java @@ -0,0 +1,43 @@ +package com.google.cloud; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.tools.opensource.dependencies.MavenRepositoryException; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.eclipse.aether.version.InvalidVersionSpecificationException; +import org.junit.Test; + +public class UnmanagedDependencyCheckTest { + @Test + public void getUnmanagedDependencyFromSamePomTest() + throws MavenRepositoryException, InvalidVersionSpecificationException { + String sharedDependenciesBom = "src/test/resources/shared-dependency-3.18.0-pom.xml"; + List unManagedDependencies = + UnmanagedDependencyCheck.getUnmanagedDependencies(sharedDependenciesBom, sharedDependenciesBom); + assertTrue(unManagedDependencies.isEmpty()); + } + + @Test + public void getUnmanagedDependencyFromHWBomTest() + throws MavenRepositoryException, InvalidVersionSpecificationException { + List unManagedDependencies = + UnmanagedDependencyCheck.getUnmanagedDependencies( + "src/test/resources/shared-dependency-3.18.0-pom.xml", "src/test/resources/bigtable-pom.xml"); + assertTrue(unManagedDependencies.isEmpty()); + } + + @Test + public void getUnmanagedDependencyFromNestedPomTest() + throws MavenRepositoryException, InvalidVersionSpecificationException { + List unManagedDependencies = + UnmanagedDependencyCheck.getUnmanagedDependencies( + "src/test/resources/shared-dependency-3.18.0-pom.xml", "src/test/resources/transitive-dependency-pom.xml"); + assertThat(unManagedDependencies) + .containsAtLeastElementsIn(ImmutableList.of("com.h2database:h2")); + // test dependency should be ignored. + assertThat(unManagedDependencies) + .doesNotContain(ImmutableList.of("com.mysql:mysql-connector-j")); + } +} diff --git a/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/bigtable-pom.xml b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/bigtable-pom.xml new file mode 100644 index 0000000000..b7a896241e --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/bigtable-pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + com.google.cloud + google-cloud-bigtable-bom + 2.29.0 + pom + + com.google.cloud + google-cloud-shared-config + 1.6.0 + + + + Google Cloud Bigtable BOM + https://github.com/googleapis/java-bigtable + + BOM for Google Cloud Bigtable + + + + Google LLC + + + + + chingor13 + Jeff Ching + chingor@google.com + Google LLC + + Developer + + + + igorberstein + Igor Bernstein + igorbernstein@google.com + Google LLC + + Developer + + + + + + scm:git:https://github.com/googleapis/java-bigtable.git + scm:git:git@github.com:googleapis/java-bigtable.git + https://github.com/googleapis/java-bigtable + + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + com.google.cloud + google-cloud-bigtable + 2.29.0 + + + com.google.cloud + google-cloud-bigtable-emulator + 0.166.0 + + + com.google.cloud + google-cloud-bigtable-emulator-core + 0.166.0 + + + com.google.api.grpc + grpc-google-cloud-bigtable-admin-v2 + 2.29.0 + + + com.google.api.grpc + grpc-google-cloud-bigtable-v2 + 2.29.0 + + + com.google.api.grpc + proto-google-cloud-bigtable-admin-v2 + 2.29.0 + + + com.google.api.grpc + proto-google-cloud-bigtable-v2 + 2.29.0 + + + com.google.cloud + google-cloud-bigtable-stats + 2.29.0 + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + true + + + + + + org.apache.maven.plugins + maven-site-plugin + + + false + + + + + diff --git a/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/gax-example-pom.xml b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/gax-example-pom.xml new file mode 100644 index 0000000000..5bc943c2de --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/gax-example-pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + com.google.api + gax-grpc + 0.0.1-SNAPSHOT + jar + + + + com.h2database + h2 + 2.2.224 + + + com.mysql + mysql-connector-j + 8.2.0 + test + + + \ No newline at end of file diff --git a/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/local-install.sh b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/local-install.sh new file mode 100755 index 0000000000..f7162e9f7b --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/local-install.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +mvn install -f shared-dependency-3.18.0-pom.xml +mvn install -f gax-example-pom.xml +mvn install -f nested-dependency-pom.xml +mvn install -f transitive-dependency-pom.xml \ No newline at end of file diff --git a/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/nested-dependency-pom.xml b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/nested-dependency-pom.xml new file mode 100644 index 0000000000..d841f4daf2 --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/nested-dependency-pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + com.example + dependencies-bom + pom + 0.0.1-SNAPSHOT + Self Dependencies + + Self dependency pom used in test. + + + + + com.google.api + gax-grpc + 0.0.1-SNAPSHOT + + + + diff --git a/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/shared-dependency-3.18.0-pom.xml b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/shared-dependency-3.18.0-pom.xml new file mode 100644 index 0000000000..d716ab7738 --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/shared-dependency-3.18.0-pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + com.google.cloud + google-cloud-shared-dependencies + pom + 3.18.0 + + first-party-dependencies + third-party-dependencies + + Google Cloud Shared Dependencies + + Shared build configuration for Google Cloud Java libraries. + + + + com.google.api + gapic-generator-java-pom-parent + 2.28.0 + ../gapic-generator-java-pom-parent + + + + UTF-8 + ${project.artifactId} + + + + + + com.google.cloud + first-party-dependencies + 3.18.0 + pom + import + + + com.google.cloud + third-party-dependencies + 3.18.0 + pom + import + + + + diff --git a/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/transitive-dependency-pom.xml b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/transitive-dependency-pom.xml new file mode 100644 index 0000000000..899652963b --- /dev/null +++ b/java-shared-dependencies/unmanaged-dependency-check/src/test/resources/transitive-dependency-pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + com.example + transitive-dependencies + pom + 0.0.1 + Self Dependencies + + Self dependency pom used in test. + + + + + com.example + dependencies-bom + 0.0.1-SNAPSHOT + pom + import + + + +