diff --git a/README.md b/README.md index 26b6aee..0224037 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ status. If the dependency is not available it will also list available versions. Given a GitHub Pull Request URL, the tool will attempt to use the [`gh`](https://github.com/cli/cli) tool to get the Pull Request title. If this meets the expected Dependabot title format, the tool will extract the GAV from the title and act as if that GAV were specified directly. +Given a GitHub repository URL, the tool will attempt to use `gh` to get the repository's `pom.xml` from its default branch. Then it will use `mvn` to resolve all of the +project dependencies, and report on this list of GAVs. + ## Building `./mvnw clean package` diff --git a/src/main/java/com/github/andrewazores/GitHubRepositoryIntegration.java b/src/main/java/com/github/andrewazores/GitHubRepositoryIntegration.java new file mode 100644 index 0000000..557690b --- /dev/null +++ b/src/main/java/com/github/andrewazores/GitHubRepositoryIntegration.java @@ -0,0 +1,137 @@ +/* + * Copyright Andrew Azores. + * + * 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.github.andrewazores; + +import java.io.BufferedInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.util.List; +import java.util.regex.Pattern; + +import io.quarkus.logging.Log; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +class GitHubRepositoryIntegration implements SourceIntegration { + private static final Pattern GH_REPO_PATTERN = + Pattern.compile( + "^https?://(?:www.)?github.com/(?[\\w.-]+)/(?[\\w.-]+)/?$", + Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); + private static final Pattern DEP_PATTERN = + Pattern.compile( + "^[\\s]*(?[a-z0-9._-]+):(?[a-z0-9._-]+):(?[a-z0-9._-]+):(?[a-z0-9._-]+).*", + Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); + + @Inject CliSupport cli; + + @ConfigProperty(name = "maven-gav-checker.integration.github-repository.transitive-deps") + boolean enableTransitiveDeps; + + @Override + public boolean test(URL url) { + Log.debugv("Testing {0} on {1}", GH_REPO_PATTERN.pattern(), url.toString()); + var m = GH_REPO_PATTERN.matcher(url.toString()); + return m.matches(); + } + + @Override + public List apply(URL url) throws IOException, InterruptedException { + Log.debugv("Processing GitHub repository: {0}", url); + var m = GH_REPO_PATTERN.matcher(url.toString()); + if (!m.matches()) throw new IllegalStateException(); + var owner = m.group("owner"); + var repo = m.group("repo"); + var repoId = String.format("%s/%s", owner, repo); + var checkoutRef = getDefaultBranchRef(repoId); + var pomUrl = getPomUrl(repoId, checkoutRef); + var workDir = Files.createTempDirectory(getClass().getSimpleName()); + var pom = workDir.resolve("pom.xml"); + var depsFile = workDir.resolve("deps.txt"); + + try (BufferedInputStream in = new BufferedInputStream(pomUrl.openStream()); + FileOutputStream fileOutputStream = new FileOutputStream(pom.toFile())) { + var dataBuffer = new byte[8 * 1024]; + int bytesRead; + while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) { + fileOutputStream.write(dataBuffer, 0, bytesRead); + } + + Log.debug(Files.readString(pom)); + + var proc = + cli.script( + "mvn", + "-B", + "-q", + "-Dsilent", + "-DincludeScope=compile", + String.format("-DexcludeTransitive=%b", !enableTransitiveDeps), + "-DincludeParents", + "-Dmdep.outputScope=false", + "-DoutputFile=" + depsFile.toAbsolutePath().toString(), + "-f", + pom.toAbsolutePath().toString(), + "dependency:list"); + proc.assertOk(); + return Files.readAllLines(depsFile).stream() + .peek(l -> Log.tracev("dependency: {0}", l)) + .filter(s -> DEP_PATTERN.matcher(s).matches()) + .map( + s -> { + var m2 = DEP_PATTERN.matcher(s); + if (!m2.matches()) throw new IllegalStateException(); + return new GroupArtifactVersion( + m2.group("group"), + m2.group("artifact"), + m2.group("version")); + }) + .toList(); + } finally { + Files.deleteIfExists(pom); + Files.deleteIfExists(depsFile); + } + } + + private String getDefaultBranchRef(String repo) throws IOException, InterruptedException { + var proc = + cli.script( + "gh", + "repo", + "view", + "--json", + "defaultBranchRef", + "--jq", + ".defaultBranchRef.name", + repo); + proc.assertOk(); + return proc.out().get(0); + } + + private URL getPomUrl(String repo, String checkoutRef) { + try { + return new URL( + String.format( + "https://raw.githubusercontent.com/%s/%s/pom.xml", repo, checkoutRef)); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/github/andrewazores/Main.java b/src/main/java/com/github/andrewazores/Main.java index 7c4c495..66260ae 100644 --- a/src/main/java/com/github/andrewazores/Main.java +++ b/src/main/java/com/github/andrewazores/Main.java @@ -32,6 +32,7 @@ import javax.net.ssl.X509TrustManager; import io.quarkus.arc.All; +import io.quarkus.logging.Log; import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; import picocli.CommandLine; @@ -67,9 +68,9 @@ public class Main implements Callable { description = "The Maven repository root URL to search, ex." + " https://repo.maven.apache.org/maven2/ . If the configuration property" - + " gav-checker.maven-repository.url (or the environment variable" - + " GAV_CHECKER_MAVEN_REPOSITORY_URL) is set, that will take precedence" - + " over this option.", + + " maven-gav-checker.maven-repository.url (or the environment variable" + + " MAVEN_GAV_CHECKER_MAVEN_REPOSITORY_URL) is set, that will take" + + " precedence over this option.", defaultValue = "https://repo.maven.apache.org/maven2/") private String repoRoot; @@ -88,9 +89,10 @@ public class Main implements Callable { names = {"-k", "--insecure"}, description = "Disable TLS validation on the remote Maven repository. This can also be set" - + " with the configuration property" - + " gav-checker.maven-repository.skip-tls-validation (or the environment" - + " variable GAV_CHECKER_MAVEN_REPOSITORY_SKIP_TLS_VALIDATION).", + + " with the configuration property" + + " maven-gav-checker.maven-repository.skip-tls-validation (or the" + + " environment variable" + + " MAVEN_GAV_CHECKER_MAVEN_REPOSITORY_SKIP_TLS_VALIDATION).", defaultValue = "false") private boolean insecure; @@ -119,11 +121,21 @@ public Integer call() throws Exception { Collection gavs = new CopyOnWriteArrayList<>(); try { var url = new URL(gav); + boolean matched = false; for (var integration : sourceIntegrations) { - if (integration.test(url)) { + boolean applies = integration.test(url); + matched |= applies; + Log.debugv( + "integration {0} applies to {1} -> {2}", + integration.getClass().getName(), url, applies); + if (applies) { gavs.addAll(integration.apply(url)); + Log.trace(gavs.toString()); } } + if (!matched) { + throw new RuntimeException("No matching integrations found for provided URL"); + } } catch (IOException | InterruptedException mue) { var matcher = GAV_PATTERN.matcher(gav); if (!matcher.matches()) { @@ -135,6 +147,7 @@ public Integer call() throws Exception { gavs.add(new GroupArtifactVersion(groupId, artifactId, version)); } } + Log.tracev("Processing GAVs: {0}", gavs); return processor.execute(gavs, repoRoot, count).get(); } diff --git a/src/main/java/com/github/andrewazores/MavenVersioning.java b/src/main/java/com/github/andrewazores/MavenVersioning.java index 14b162e..bf4e8f5 100644 --- a/src/main/java/com/github/andrewazores/MavenVersioning.java +++ b/src/main/java/com/github/andrewazores/MavenVersioning.java @@ -25,7 +25,6 @@ import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Node; -import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public record MavenVersioning(String latest, String release, List versions) { @@ -47,13 +46,15 @@ public static MavenVersioning from(String uri) var root = xmlDoc.getDocumentElement(); - var versioning = getChild(root, "versioning").orElseThrow(); - var latest = getChild(versioning, "latest").map(Node::getTextContent).orElse("N/A"); - var release = getChild(versioning, "release").map(Node::getTextContent).orElse("N/A"); - var versions = getChild(versioning, "versions").orElseThrow(); + var versioning = XmlParser.getChild(root, "versioning").orElseThrow(); + var latest = + XmlParser.getChild(versioning, "latest").map(Node::getTextContent).orElse("N/A"); + var release = + XmlParser.getChild(versioning, "release").map(Node::getTextContent).orElse("N/A"); + var versions = XmlParser.getChild(versioning, "versions").orElseThrow(); var versionList = new ArrayList<>( - getChildren(versions, "version").stream() + XmlParser.getChildren(versions, "version").stream() .map(Node::getTextContent) .toList()); Collections.reverse(versionList); @@ -61,35 +62,6 @@ public static MavenVersioning from(String uri) return new MavenVersioning(latest, release, versionList); } - private static Optional getChild(Node parent, String childName) { - NodeList list = parent.getChildNodes(); - int idx = 0; - while (idx < list.getLength()) { - var node = list.item(idx); - var name = node.getNodeName(); - if (childName.equals(name)) { - return Optional.of(node); - } - idx++; - } - return Optional.empty(); - } - - private static List getChildren(Node parent, String childName) { - List out = new ArrayList<>(); - NodeList list = parent.getChildNodes(); - int idx = 0; - while (idx < list.getLength()) { - var node = list.item(idx); - var name = node.getNodeName(); - if (childName.equals(name)) { - out.add(node); - } - idx++; - } - return out; - } - private static boolean versionCompare(String request, String found) { return found.startsWith(String.format("%s-", request)) || found.startsWith(String.format("%s.", request)); diff --git a/src/main/java/com/github/andrewazores/XmlParser.java b/src/main/java/com/github/andrewazores/XmlParser.java new file mode 100644 index 0000000..873441f --- /dev/null +++ b/src/main/java/com/github/andrewazores/XmlParser.java @@ -0,0 +1,55 @@ +/* + * Copyright Andrew Azores. + * + * 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.github.andrewazores; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +class XmlParser { + + static Optional getChild(Node parent, String childName) { + NodeList list = parent.getChildNodes(); + int idx = 0; + while (idx < list.getLength()) { + var node = list.item(idx); + var name = node.getNodeName(); + if (childName.equals(name)) { + return Optional.of(node); + } + idx++; + } + return Optional.empty(); + } + + static List getChildren(Node parent, String childName) { + List out = new ArrayList<>(); + NodeList list = parent.getChildNodes(); + int idx = 0; + while (idx < list.getLength()) { + var node = list.item(idx); + var name = node.getNodeName(); + if (childName.equals(name)) { + out.add(node); + } + idx++; + } + return out; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index fb320a9..85fea09 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,5 +12,7 @@ quarkus.native.enable-https-url-handler=true quarkus.native.container-build=true quarkus.native.container-runtime=podman-rootless -gav-checker.maven-repository.url= -gav-checker.maven-repository.skip-tls-validation= +maven-gav-checker.maven-repository.url= +maven-gav-checker.maven-repository.skip-tls-validation= + +maven-gav-checker.integration.github-repository.transitive-deps=false