All Downloads are FREE. Search and download functionalities are using the official Maven repository.

name.remal.gradle_plugins.toolkit.build_logic.github-submit-dependencies.gradle Maven / Gradle / Ivy

import static java.nio.charset.StandardCharsets.UTF_8

import groovy.json.JsonGenerator
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.transform.EqualsAndHashCode
import groovy.transform.TupleConstructor
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration

allprojects {
    pluginManager.withPlugin('java') {
        project.apply plugin: 'reporting-base'

        Task githubSubmitDependencies = tasks.create('githubSubmitDependencies') { Task task ->
            task.group = 'documentation'

            SetProperty lazyDependenciesToSubmitSet = project.objects.setProperty(DependencyToSubmit.class)
            task.ext.dependenciesToSubmit = lazyDependenciesToSubmitSet
            task.inputs.property('dependenciesToSubmit', lazyDependenciesToSubmitSet)

            File reportFile = project.file("${project.reporting.baseDir}/${task.name}.json")
            task.ext.reportFile = reportFile
            task.outputs.file(reportFile).withPropertyName('reportFile')
            doFirst { reportFile.delete() }

            Closure nullIfEmpty = { String string ->
                return string != null && string.isEmpty() ? null : string
            }

            Closure nullIfJar = { String string ->
                return string != null && string == 'jar' ? null : string
            }

            Closure isExcludedCategory = { ResolvedDependencyResult resolvedDependency ->
                return isPlatformDependency(resolvedDependency) || isDocumentationDependency(resolvedDependency) || isVerificationDependency(resolvedDependency)
            }

            Closure processConfiguration = { Configuration conf, boolean isBuildscript = false ->
                lazyDependenciesToSubmitSet.addAll(
                    provider {
                        if (!conf.canSafelyBeResolved()) return []

                        Collection resolvedArtifacts = conf.resolvedConfiguration.resolvedArtifacts

                        Set result = [].toSet()
                        Closure processResolvedDependency = {
                            ResolvedDependencyResult resolvedDependency,
                            DependencyToSubmitScope scope,
                            DependencyToSubmitRelationship relationship ->
                                ModuleComponentIdentifier id = resolvedDependency.selected.id.with { id ->
                                    if (id instanceof ModuleComponentIdentifier) {
                                        return id
                                    } else {
                                        return null
                                    }
                                }
                                if (id == null) return

                                if (isPlatformDependency(resolvedDependency)) {
                                    result.add(
                                        new DependencyToSubmit(
                                            nullIfEmpty(id.group),
                                            nullIfEmpty(id.module),
                                            nullIfEmpty(id.version),
                                            null,
                                            null,
                                            scope,
                                            relationship
                                        )
                                    )
                                } else {
                                    Collection dependencyArtifacts = resolvedArtifacts.findAll { it.id.componentIdentifier == id }
                                    dependencyArtifacts.forEach { dependencyArtifact ->
                                        result.add(
                                            new DependencyToSubmit(
                                                nullIfEmpty(id.group),
                                                nullIfEmpty(id.module),
                                                nullIfEmpty(id.version),
                                                nullIfEmpty(dependencyArtifact.classifier),
                                                nullIfEmpty(nullIfJar(dependencyArtifact.extension)),
                                                scope,
                                                relationship
                                            )
                                        )
                                    }
                                }
                        }

                        DependencyToSubmitScope scope = DependencyToSubmitScope.DEVELOPMENT
                        if (isBuildscript) {
                            // do nothing
                        } else if (['runtimeClasspath', 'relocateClasses'].contains(conf.name)) {
                            scope = DependencyToSubmitScope.RUNTIME
                        }

                        conf.incoming.resolutionResult
                            .root
                            .dependencies
                            .collect { (DependencyResult) it }
                            .findAll { !it.constraint }
                            .findAll { it instanceof ResolvedDependencyResult }
                            .collect { (ResolvedDependencyResult) it }
                            .findAll { !isExcludedCategory(it) }
                            .forEach { processResolvedDependency(it, scope, DependencyToSubmitRelationship.DIRECT) }

                        conf.incoming.resolutionResult
                            .allDependencies
                            .collect { (DependencyResult) it }
                            .findAll { !it.constraint }
                            .findAll { it instanceof ResolvedDependencyResult }
                            .collect { (ResolvedDependencyResult) it }
                            .findAll { !isExcludedCategory(it) }
                            .findAll { !it.selected.selectionReason.expected }
                            .forEach { processResolvedDependency(it, scope, DependencyToSubmitRelationship.INDIRECT) }

                        return result
                    }
                )
            }

            project.buildscript.configurations.all { processConfiguration(it, true) }
            project.configurations.all { processConfiguration(it) }

            ListProperty lazyDependenciesToSubmit = project.objects.listProperty(DependencyToSubmit.class)
            lazyDependenciesToSubmit.set(
                provider {
                    lazyDependenciesToSubmitSet.get()
                        .findAll { it.version != null && !it.version.isEmpty() }
                        .toSorted()
                        .groupBy { it.toString() }
                        .values()
                        .toList()
                        .collect { it.get(0) }
                }
            )

            JsonGenerator jsonGenerator = new JsonGenerator.Options()
                .excludeNulls()
                .excludeFieldsByName('contentHash', 'originalClassName')
                .build()

            doLast {
                Closure encodeUrlPart = { String string ->
                    if (string == null) return ''
                    return URLEncoder.encode(string, UTF_8)
                        .replace("+", "%20")
                        .replace("*", "%2A")
                        .replace("%7E", "~");
                }

                Map resolved = [:]
                lazyDependenciesToSubmit.get().forEach { DependencyToSubmit dep ->
                    String packageUrl = "pkg:maven/${encodeUrlPart(dep.group)}/${encodeUrlPart(dep.name)}@${encodeUrlPart(dep.version)}"
                    if (dep.classifier) {
                        if (!packageUrl.contains('?')) {
                            packageUrl += '?'
                        } else {
                            packageUrl += '&'
                        }
                        packageUrl += "packaging=${encodeUrlPart(dep.classifier)}"
                    }

                    resolved.put(
                        dep.toString(),
                        [
                            package_url: packageUrl,
                            relationship: dep.relationship?.name()?.toLowerCase(),
                            scope: dep.scope?.name()?.toLowerCase(),
                        ]
                    )
                }

                Map manifests = [:]
                manifests.put(
                    rootProject.relativePath(project.buildFile),
                    [
                        name: rootProject.relativePath(project.buildFile),
                        file: [
                            source_location: rootProject.relativePath(project.buildFile),
                        ],
                        resolved: resolved,
                    ]
                )

                String runId = [
                    'github-actions-run',
                    project.property('github-actions-run-id'),
                    project.property('github-actions-run-attempt'),
                    project.property('github-actions-job'),
                    project.property('github-actions-job-index'),
                ].collect { it ?: '' }.join('-')

                Map requestBody = [
                    version: System.currentTimeMillis(),
                    job: [
                        id: runId,
                        correlator: project.path,
                    ],
                    sha: project.property('git-sha'),
                    ref: project.property('git-ref'),
                    detector: [
                        name: 'name.remal.gradle-plugins.toolkit',
                        version: '0.65.13',
                        url: 'https://github.com/remal-gradle-plugins/toolkit',
                    ],
                    manifests: manifests,
                    scanned: new Date(),
                ]
                String jsonContent = jsonGenerator.toJson(requestBody)


                reportFile.parentFile.mkdirs()
                reportFile.setText(JsonOutput.prettyPrint(jsonContent), 'UTF-8')


                int maxAttempts = 3
                int attempt = 0
                while (true) {
                    ++attempt

                    HttpRequest httpRequest = HttpRequest.newBuilder()
                        .timeout(Duration.ofMinutes(1))
                        .uri(new URI("${project.property('repository-api-url')}/dependency-graph/snapshots"))
                        .header('Authorization', "token ${project.property('github-actions-token')}")
                        .header('Accept', 'application/vnd.github+json')
                        .header('Content-Type', 'application/json;charset=UTF-8')
                        .POST(HttpRequest.BodyPublishers.ofString(jsonContent, UTF_8))
                        .build()

                    HttpResponse httpResponse = HttpClient.newHttpClient().send(httpRequest, HttpResponse.BodyHandlers.ofString(UTF_8))
                    int statusCode = httpResponse.statusCode()
                    if (statusCode >= 400) {
                        if (statusCode == 429
                            || statusCode >= 500
                        ) {
                            if (attempt < maxAttempts) {
                                Thread.sleep(5_000)
                                continue
                            }
                        }

                        throw new GradleException("Dependencies submitting REST API call returned ${httpResponse.statusCode()} error:\n${httpResponse.body()}")
                    }

                    String responseBodyJson = httpResponse.body()
                    Map responseBody = new JsonSlurper().parseText(responseBodyJson)
                    if (responseBody.message) {
                        println responseBody.message
                    } else {
                        println responseBodyJson
                    }

                    break
                }
            }
        }

        if (project.isBuildSrcProject) {
            if (gradle.startParameter.taskNames.contains(githubSubmitDependencies.name)
                || (gradle.parent && gradle.parent.startParameter.taskNames.contains(githubSubmitDependencies.name))
            ) {
                project.rootProject.tasks.named('build') {
                    dependsOn(githubSubmitDependencies)
                }
            }
        }
    }
}

@TupleConstructor
@EqualsAndHashCode
class DependencyToSubmit implements Serializable, Comparable {

    String group
    String name
    String version
    String classifier
    String type

    DependencyToSubmitScope scope
    DependencyToSubmitRelationship relationship

    String toString() {
        String group = this.group ?: ''
        String name = this.name ?: ''
        String version = this.version ?: ''
        String classifier = this.classifier ?: ''
        String type = this.type ?: ''
        return "$group:$name:$version:$classifier@$type"
            .replaceFirst(/:?@(jar)?$/, '')
    }

    private static final Comparator scopeComparator = Comparator.nullsLast(Comparator.naturalOrder())
    private static final Comparator relationshipComparator = Comparator.nullsLast(Comparator.naturalOrder())

    @Override
    int compareTo(DependencyToSubmit other) {
        int result = toString() <=> other.toString()
        if (result == 0) result = scopeComparator.compare(scope, other.scope)
        if (result == 0) result = relationshipComparator.compare(relationship, other.relationship)
        return result
    }

}

enum DependencyToSubmitScope {
    RUNTIME,
    DEVELOPMENT,
}

enum DependencyToSubmitRelationship {
    DIRECT,
    INDIRECT,
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy