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

name.remal.gradle_plugins.plugins.publish.gradle_plugin_portal.PublishToGradlePluginPortalRepository.kt Maven / Gradle / Ivy

There is a newer version: 1.9.2
Show newest version
package name.remal.gradle_plugins.plugins.publish.gradle_plugin_portal

import com.gradle.protocols.ServerResponseBase
import com.gradle.publish.Hasher
import com.gradle.publish.OAuthHttpClient
import com.gradle.publish.protocols.v1.models.ClientPostRequest
import com.gradle.publish.protocols.v1.models.publish.*
import com.gradle.publish.upload.Uploader
import name.remal.*
import name.remal.gradle_plugins.dsl.*
import name.remal.gradle_plugins.dsl.extensions.*
import name.remal.gradle_plugins.dsl.utils.retryIO
import name.remal.gradle_plugins.plugins.publish.BasePublishToMavenRepository
import org.gradle.api.tasks.TaskAction
import org.gradle.util.GradleVersion
import java.io.File
import java.lang.System.getProperty
import java.net.URI
import java.net.URL
import kotlin.text.isNotEmpty
import kotlin.text.toLowerCase
import kotlin.text.trim

@BuildTask
class PublishToGradlePluginPortalRepository : BasePublishToMavenRepository() {

    private val alreadyPublishedPlugins: List by lazy { repository.plugins.filter { isPluginPublished(it.id!!, publication.version) } }
    private val notPublishedPlugins: List by lazy { repository.plugins.toMutableList().also { it.removeAll(alreadyPublishedPlugins) } }

    private fun isPluginPublished(pluginId: String, version: String): Boolean {
        val url = URL("https://plugins.gradle.org/plugin/${encodeURIComponent(pluginId)}/${encodeURIComponent(version)}")
        return retryIO { 200 == url.fetchHttpStatus() }
    }


    init {
        onlyIf {
            if (repository.publishKey.isNullOrEmpty()) {
                throw IllegalStateException("Repository ${repository.name} doesn't have ${GradlePluginPortalRepository::publishKey.name} property set")
            }
            if (repository.publishSecret.isNullOrEmpty()) {
                throw IllegalStateException("Repository ${repository.name} doesn't have ${GradlePluginPortalRepository::publishSecret.name} property set")
            }
            return@onlyIf true
        }

        onlyIf { _ ->
            if (SNAPSHOT_REGEX.containsMatchIn(publication.version)) {
                logWarn("SNAPSHOT version can't be published to {}", repository.name)
                return@onlyIf false
            } else {
                return@onlyIf true
            }
        }

        onlyIf {
            parseExtendedPluginProperties()
            checkThatPluginIdsAreUnique()
            validateArtifacts()
            alreadyPublishedPlugins.forEach { logWarn("Plugin {} has been already published", it.id) }
            return@onlyIf notPublishedPlugins.isNotEmpty()
        }
    }

    private fun parseExtendedPluginProperties() {
        publication.artifacts.filter { it.classifier.isNullOrEmpty() }.forEach forEachArtifact@{ artifact ->
            project.autoFileTree(artifact.file)
                .include("META-INF/gradle-plugins/*.properties")
                .forEach forEachPluginProps@{ pluginPropsFile ->
                    val pluginId = pluginPropsFile.nameWithoutExtension

                    val pluginProps = loadProperties(pluginPropsFile)
                    if ("true" == pluginProps[IS_HIDDEN_DESCRIPTOR_KEY]) return@forEachPluginProps

                    val pluginInfo = repository.plugins.firstOrNull { it.id == pluginId }
                        ?: repository.plugins.createWithOptionalUniqueSuffix(pluginId) { it.id = pluginId }
                    if (pluginInfo.displayName.isNullOrEmpty()) {
                        pluginProps.getProperty(DISPLAY_NAME_PLUGIN_DESCRIPTOR_KEY).nullIfEmpty()?.let { pluginInfo.displayName = it }
                    }
                    if (pluginInfo.description.isNullOrEmpty()) {
                        pluginInfo.description = listOf(
                            pluginProps.getProperty(DESCRIPTION_PLUGIN_DESCRIPTOR_KEY),
                            pluginProps.getProperty(MIN_GRADLE_VERSION_PLUGIN_DESCRIPTOR_KEY)?.let { "Min Gradle version: $it." },
                            pluginProps.getProperty(MAX_GRADLE_VERSION_PLUGIN_DESCRIPTOR_KEY)?.let { "Max Gradle version: $it." }
                        )
                            .asSequence()
                            .filterNotNull()
                            .map(String::trim)
                            .filter(String::isNotEmpty)
                            .joinToString(" ")
                    }
                    if (pluginInfo.websiteUrl.isNullOrEmpty()) {
                        pluginProps.getProperty(WEBSITE_PLUGIN_DESCRIPTOR_KEY).nullIfEmpty()?.let { pluginInfo.websiteUrl = it }
                    }
                    pluginProps.getProperty(TAGS_PLUGIN_DESCRIPTOR_KEY).default().splitToSequence(',', ';')
                        .map(String::trim)
                        .filter(String::isNotEmpty)
                        .forEach { pluginInfo.tags.add(it) }
                }
        }
    }

    private fun checkThatPluginIdsAreUnique() {
        repository.plugins.groupBy(PluginInfo::id).forEach { id, infos ->
            if (2 <= infos.size) {
                throw IllegalStateException("There are ${infos.size} plugins with ID $id")
            }
        }
    }

    private fun validateArtifacts() {
        publication.artifacts.filter { it.classifier.isNullOrEmpty() }.forEach forEachArtifact@{ artifact ->
            val mappings = buildMap {
                project.autoFileTree(artifact.file)
                    .include("META-INF/gradle-plugins/*.properties")
                    .visitFiles {
                        val pluginId = it.nameWithoutExtension
                        val pluginProps = loadProperties(it.open())
                        val implementationClassName = pluginProps.getProperty(IMPLEMENTATION_CLASS_PLUGIN_DESCRIPTOR_KEY).nullIfEmpty()
                            ?: throw IllegalStateException("$IMPLEMENTATION_CLASS_PLUGIN_DESCRIPTOR_KEY property can't be found in ${it.relativePath}")
                        put(pluginId, implementationClassName)
                    }
            }

            val allClassNames = buildSet {
                project.autoFileTree(artifact.file).include("**/*.class").visitFiles {
                    add(resourceNameToClassName(it.relativePath.toString()))
                }
            }

            mappings.forEach { pluginId, implementationClassName ->
                if (implementationClassName !in allClassNames) {
                    throw IllegalStateException("Implementation class $implementationClassName for plugin $pluginId can't be found in ${artifact.file}")
                }
            }
        }
    }


    private val buildMetadata: BuildMetadata = BuildMetadata(GradleVersion.current().version)

    private val pomUrl: String? by lazy { pomDocument.rootElement.getChild("url", POM_NAMESPACE)?.textTrim.nullIfEmpty() }

    private val pomScmUrl: String? by lazy { pomDocument.rootElement.getChild("scm", POM_NAMESPACE)?.getChild("url", POM_NAMESPACE)?.textTrim.nullIfEmpty() }

    private val mavenCoordinates: PublishMavenCoordinates by lazy { PublishMavenCoordinates(publication.groupId, publication.artifactId, publication.version) }

    private val publishArtifacts: Map by lazy {
        buildMap {
            publication.artifacts.forEach { artifact ->
                val type = ArtifactType.find(artifact.extension, artifact.classifier) ?: return@forEach
                val hash: String = artifact.file.inputStream().use { Hasher.hash(it) }
                put(PublishArtifact(type.name, hash), artifact.file)
            }

            run {
                val pomFile = pomFile
                val hash: String = pomFile.inputStream().use { sha256(it) }
                put(PublishArtifact(ArtifactType.POM.name, hash), pomFile)
            }
        }
    }

    @TaskAction
    protected fun doPublish() {
        val publishResponses = mutableListOf()
        notPublishedPlugins.forEach { plugin ->
            logLifecycle("Publishing plugin {} version {}", plugin.id, publication.version)
            val request = PublishNewVersionRequest().also {
                it.buildMetadata = buildMetadata
                it.pluginId = plugin.id ?: throw IllegalStateException("Plugin ID is not set for plugin ${plugin.id}")
                it.pluginVersion = publication.version
                it.displayName = plugin.displayName?.trim().nullIfEmpty() ?: plugin.id
                it.description = plugin.description?.trim().nullIfEmpty() ?: plugin.id
                it.tags = plugin.tags.map(String::toLowerCase)
                it.webSite = plugin.websiteUrl.nullIfEmpty() ?: repository.websiteUrl.nullIfEmpty() ?: pomUrl ?: throw IllegalStateException("Website URL is not set for plugin ${plugin.id}")
                it.vcsUrl = plugin.vcsUrl.nullIfEmpty() ?: repository.vcsUrl.nullIfEmpty() ?: pomScmUrl ?: throw IllegalStateException("VCS URL is not set for plugin ${plugin.id}")
                it.mavenCoordinates = mavenCoordinates
                it.artifacts = publishArtifacts.keys.toList()

                it.webSite = it.webSite.replace("\${pluginId}", it.pluginId)
                it.vcsUrl = it.vcsUrl.replace("\${pluginId}", it.pluginId)
            }

            val response = retryIO { sendGradlePortalRequest(request) }
            publishResponses.add(response)
        }

        if (publishResponses.isEmpty()) {
            logWarn("No plugins were published")
            return
        }

        run {
            val uploadURLs: Map = publishResponses.first().publishTo
            publishArtifacts.forEach { publishArtifact, file ->
                val uploadURL = uploadURLs[publishArtifact.hash]
                if (uploadURL == null) {
                    logWarn("Skipping upload of artifact {} as it has been previously uploaded", project.rootProject.relativePath(file))
                    return@forEach
                }

                logLifecycle("Uploading artifact {}", project.rootProject.relativePath(file))
                logInfo("Uploading artifact {} to {}", project.rootProject.relativePath(file), uploadURL)
                retryIO { Uploader.putFile(file, uploadURL) }
            }
        }

        publishResponses.forEach { publishResponse ->
            val activateRequest = publishResponse.nextRequest
            logLifecycle("Activating plugin {} version {}", activateRequest.pluginId, activateRequest.version)
            retryIO { sendGradlePortalRequest(activateRequest) }
        }
    }

    private fun  sendGradlePortalRequest(request: ClientPostRequest): Response {
        val baseURI = URI(
            getProperty("gradle.portal.url").nullIfEmpty()
                ?: project.findProperty("gradle.portal.url").unwrapProviders()?.toString().nullIfEmpty()
                ?: "https://plugins.gradle.org"
        )

        if (isInfoLogEnabled) {
            logInfo("Requesting POST {}: {}", baseURI.resolve(request.requestProtocolURL()), request.postJsonString)
        }

        val client = OAuthHttpClient(baseURI.toString(), repository.publishKey, repository.publishSecret)
        val response: Response? = client.send(request)

        if (isInfoLogEnabled) {
            logInfo("Response: {}", response?.convertToJsonString())
        }

        if (response == null) {
            throw PublishingToGradlePluginPortalException("Did not get a response from server")
        } else if (response.hasFailed()) {
            throw PublishingToGradlePluginPortalException(buildString {
                append("Request failed")
                if (!response.errorMessage.isNullOrEmpty()) {
                    append(". Server responded with: ").append(response.errorMessage)
                }
            })
        }

        response.errorMessage.nullIfEmpty()?.let { logError("{}", it) }
        when (response) {
            is PublishNewVersionResponse -> response.warningMessage.nullIfEmpty()?.let { logWarn("{}", it) }
            is PublishActivateResponse -> response.successMessage.nullIfEmpty()?.let { logLifecycle("{}", it) }
        }

        return response
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy