diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index cbf71e7..bbf7f36 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } group = "xyz.jpenilla" -version = "2.2.1-SNAPSHOT" +version = "2.2.2-SNAPSHOT" description = "Gradle plugins adding run tasks for Minecraft server and proxy software" repositories { diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/DownloadPluginsSpec.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/DownloadPluginsSpec.kt index d7a8498..1da6775 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/DownloadPluginsSpec.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/DownloadPluginsSpec.kt @@ -33,6 +33,8 @@ import xyz.jpenilla.runtask.pluginsapi.github.GitHubApi import xyz.jpenilla.runtask.pluginsapi.github.GitHubApiImpl import xyz.jpenilla.runtask.pluginsapi.hangar.HangarApi import xyz.jpenilla.runtask.pluginsapi.hangar.HangarApiImpl +import xyz.jpenilla.runtask.pluginsapi.jenkins.JenkinsPluginProvider +import xyz.jpenilla.runtask.pluginsapi.jenkins.JenkinsPluginProviderImpl import xyz.jpenilla.runtask.pluginsapi.modrinth.ModrinthApi import xyz.jpenilla.runtask.pluginsapi.modrinth.ModrinthApiImpl import xyz.jpenilla.runtask.pluginsapi.url.UrlPluginProvider @@ -67,6 +69,7 @@ public abstract class DownloadPluginsSpec @Inject constructor( registry.registerFactory(ModrinthApi::class) { name -> objects.newInstance(ModrinthApiImpl::class, name) } registry.registerFactory(GitHubApi::class) { name -> objects.newInstance(GitHubApiImpl::class, name) } registry.registerFactory(UrlPluginProvider::class) { name -> objects.newInstance(UrlPluginProviderImpl::class, name) } + registry.registerFactory(JenkinsPluginProvider::class) { name -> objects.newInstance(JenkinsPluginProviderImpl::class, name) } register("hangar", HangarApi::class) { url.set(HangarApi.DEFAULT_URL) @@ -76,6 +79,7 @@ public abstract class DownloadPluginsSpec @Inject constructor( } register("github", GitHubApi::class) register("url", UrlPluginProvider::class) + register("jenkins", JenkinsPluginProvider::class) } /** @@ -96,6 +100,7 @@ public abstract class DownloadPluginsSpec @Inject constructor( is ModrinthApi -> ModrinthApi::class is GitHubApi -> GitHubApi::class is UrlPluginProvider -> UrlPluginProvider::class + is JenkinsPluginProvider -> JenkinsPluginProvider::class else -> throw IllegalStateException("Unknown PluginApi type: ${api.javaClass.name}") } configure(name, type) { @@ -181,6 +186,27 @@ public abstract class DownloadPluginsSpec @Inject constructor( url.configure { add(urlString) } } + // jenkins extensions + + /** + * Access to the built-in [JenkinsPluginProvider]. + */ + @get:Internal + public val jenkins: NamedDomainObjectProvider + get() = named("jenkins", JenkinsPluginProvider::class) + + /** + * Add a plugin download. + * + * @param baseUrl The root url to the jenkins instance + * @param job The id of the target job to download the plugin from + * @param artifactRegex In case multiple artifacts are provided, a regex to pick the correct artifact + * @param build A specific build for the [job] or the lastSuccessfulBuild if none provided + */ + public fun jenkins(baseUrl: String, job: String, artifactRegex: Regex? = null, build: String? = null) { + jenkins.configure { add(baseUrl, job, artifactRegex, build) } + } + // All zero-arg methods must be annotated or Gradle will think it's an input @Internal override fun getAsMap(): SortedMap> = registry.asMap diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginApiDownload.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginApiDownload.kt index 7eec27b..dd2d7ca 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginApiDownload.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginApiDownload.kt @@ -18,6 +18,7 @@ package xyz.jpenilla.runtask.pluginsapi import org.gradle.api.provider.Property import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional import xyz.jpenilla.runtask.util.HashingAlgorithm import xyz.jpenilla.runtask.util.calculateHash import xyz.jpenilla.runtask.util.toHexString @@ -173,3 +174,46 @@ public abstract class UrlDownload : PluginApiDownload() { return toHexString(url.get().byteInputStream().calculateHash(HashingAlgorithm.SHA1)) } } + +public abstract class JenkinsDownload : PluginApiDownload() { + + @get:Input + public abstract val baseUrl: Property + + @get:Input + public abstract val job: Property + + @get:Input @get:Optional + public abstract val artifactRegex: Property + + @get:Input @get:Optional + public abstract val build: Property + + override fun toString(): String { + return "JenkinsDownload(baseUrl=$baseUrl, job=$job, artifactRegex=$artifactRegex, build=$build)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (javaClass != other?.javaClass) { + return false + } + + other as JenkinsDownload + + return baseUrl.get() == other.baseUrl.get() && + job.get() == other.job.get() && + artifactRegex.orNull == other.artifactRegex.orNull && + build.orNull == other.build.orNull + } + + override fun hashCode(): Int { + var result = baseUrl.hashCode() + result = 31 * result + job.hashCode() + result = 31 * result + artifactRegex.hashCode() + result = 31 * result + build.hashCode() + return result + } +} diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadServiceImpl.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadServiceImpl.kt index 400a0d3..0e25fde 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadServiceImpl.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/PluginDownloadServiceImpl.kt @@ -89,6 +89,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { is ModrinthApiDownload -> resolveModrinthPlugin(project, download) is GitHubApiDownload -> resolveGitHubPlugin(project, download) is UrlDownload -> resolveUrl(project, download) + is JenkinsDownload -> resolveJenkins(project, download) } } @@ -104,7 +105,54 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val version = manifest.urlProvider[urlHash] ?: PluginVersion(fileName = "$urlHash.jar", displayName = download.url.get()) val targetFile = targetDir.resolve(version.fileName) val setter: (PluginVersion) -> Unit = { manifest.urlProvider[urlHash] = it } - val ctx = DownloadCtx(project, "url", download.url.get(), targetDir, targetFile, version, setter) + val ctx = DownloadCtx(project, "url", { download.url.get() }, targetDir, targetFile, version, setter) + return download(ctx) + } + + private fun resolveJenkins(project: Project, download: JenkinsDownload): Path { + val cacheDir = parameters.cacheDirectory.get().asFile.toPath() + val targetDir = cacheDir.resolve(Constants.JENKINS_PLUGIN_DIR) + + val baseUrl = download.baseUrl.get().trimEnd('/') + val job = download.job.get() + val regex = download.artifactRegex.orNull + val jobUrl = "$baseUrl/job/$job" + val build = download.build.getOrElse( + URI("$jobUrl/${Constants.JENKINS_LAST_SUCCESSFUL_BUILD}/buildNumber") + .toURL().readText() + ) + val restEndpoint = URI(Constants.JENKINS_REST_ENDPOINT.format(jobUrl, build)) + + val provider = manifest.jenkinsProvider.computeIfAbsent(baseUrl) { JenkinsProvider() } + val versions = provider.computeIfAbsent(job) { PluginVersions() } + val version = versions[build] ?: PluginVersion( + fileName = "$job-$build.jar", + displayName = "jenkins:$baseUrl/$job/$build" + ) + + val targetFile = targetDir.resolve(version.fileName) + val setter: (PluginVersion) -> Unit = { versions[build] = it } + + val downloadUrlSupplier: () -> String = supplier@{ + val artifacts = mapper.readValue(restEndpoint.toURL()).artifacts + if (artifacts.isEmpty()) { + throw IllegalStateException("No artifacts provided for build $build at $jobUrl") + } + if (artifacts.size == 1) { + val path = artifacts.first().relativePath + if (regex != null && !(regex.containsMatchIn(path))) { + throw NullPointerException("Regex does not match only-found artifact: $path") + } + return@supplier "$jobUrl/$build/artifact/$path" + } + if (regex == null) { + throw NullPointerException("Regex is null but multiple artifacts were found for $jobUrl/$build") + } + val artifactPaths = artifacts.map { it.relativePath } + val artifact = artifactPaths.firstOrNull { regex.containsMatchIn(it) } ?: throw NullPointerException("Failed to find artifact for regex ($regex) - Artifacts are: ${artifactPaths.joinToString(", ")}") + "$jobUrl/$build/artifact/$artifact" + } + val ctx = DownloadCtx(project, jobUrl, downloadUrlSupplier, targetDir, targetFile, version, setter) return download(ctx) } @@ -130,7 +178,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val downloadUrl = "$apiUrl/api/v1/projects/$apiPlugin/versions/$apiVersion/$platformType/download" val setter: (PluginVersion) -> Unit = { platform[apiVersion] = it } - val ctx = DownloadCtx(project, apiUrl, downloadUrl, targetDir, targetFile, version, setter) + val ctx = DownloadCtx(project, apiUrl, { downloadUrl }, targetDir, targetFile, version, setter) return download(ctx) } @@ -152,7 +200,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val versionRequestUrl = "$apiUrl/v2/project/$apiPlugin/version/$apiVersion" val versionJsonPath = download( - DownloadCtx(project, apiUrl, versionRequestUrl, targetDir, jsonFile, jsonVersion, setter = { plugin[jsonVersionName] = it }) + DownloadCtx(project, apiUrl, { versionRequestUrl }, targetDir, jsonFile, jsonVersion, setter = { plugin[jsonVersionName] = it }) ) val versionInfo = mapper.readValue(versionJsonPath.toFile()) val primaryFile = versionInfo.files.find { it.primary } ?: error("Could not find primary file for $download in $versionInfo") @@ -165,7 +213,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val targetFile = targetDir.resolve(version.fileName) return download( - DownloadCtx(project, apiUrl, primaryFile.url, targetDir, targetFile, version, setter = { plugin[apiVersion] = it }) + DownloadCtx(project, apiUrl, { primaryFile.url }, targetDir, targetFile, version, setter = { plugin[apiVersion] = it }) ) } @@ -188,7 +236,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { val downloadUrl = "https://github.com/$owner/$repo/releases/download/$tag/$asset" val setter: (PluginVersion) -> Unit = { tagProvider[asset] = it } - val ctx = DownloadCtx(project, "github.com", downloadUrl, targetDir, targetFile, version, setter) + val ctx = DownloadCtx(project, "github.com", { downloadUrl }, targetDir, targetFile, version, setter) return download(ctx) } @@ -217,7 +265,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { } private fun downloadFile(ctx: DownloadCtx): Path { - val url = URI.create(ctx.downloadUrl).toURL() + val url = URI.create(ctx.downloadUrl()).toURL() val connection = url.openConnection() as HttpURLConnection try { @@ -277,7 +325,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService { private data class DownloadCtx( val project: Project, val baseUrl: String, - val downloadUrl: String, + val downloadUrl: () -> String, val targetDir: Path, val targetFile: Path, val version: PluginVersion, @@ -290,7 +338,8 @@ private data class PluginsManifest( val hangarProviders: MutableMap = HashMap(), val modrinthProviders: MutableMap = HashMap(), val githubProvider: GitHubProvider = GitHubProvider(), - val urlProvider: PluginVersions = PluginVersions() + val urlProvider: PluginVersions = PluginVersions(), + val jenkinsProvider: MutableMap = HashMap() ) // hangar types: @@ -330,6 +379,20 @@ private typealias GitHubRepo = MutableMap private fun GitHubRepo(): GitHubRepo = HashMap() +// jenkins types: +private typealias JenkinsProvider = MutableMap + +private fun JenkinsProvider(): JenkinsProvider = HashMap() + +@JsonIgnoreProperties(ignoreUnknown = true) +private data class JenkinsBuildResponse( + val artifacts: List +) { + data class Artifact( + val relativePath: String + ) +} + // general types: private typealias PluginVersions = MutableMap diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/jenkins/JenkinsPluginProvider.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/jenkins/JenkinsPluginProvider.kt new file mode 100644 index 0000000..a6c606e --- /dev/null +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/jenkins/JenkinsPluginProvider.kt @@ -0,0 +1,36 @@ +/* + * Run Task Gradle Plugins + * Copyright (c) 2023 Jason Penilla + * + * 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 xyz.jpenilla.runtask.pluginsapi.jenkins + +import xyz.jpenilla.runtask.pluginsapi.JenkinsDownload +import xyz.jpenilla.runtask.pluginsapi.PluginApi + +/** + * [PluginApi] implementation for downloading plugins from Jenkins CI. + */ +public interface JenkinsPluginProvider : PluginApi { + + /** + * Add a plugin download. + * + * @param baseUrl The root url to the jenkins instance + * @param job The id of the target job to download the plugin from + * @param artifactRegex In case multiple artifacts are provided, a regex to pick the correct artifact + * @param build A specific build for the [job] or the lastSuccessfulBuild if none provided + */ + public fun add(baseUrl: String, job: String, artifactRegex: Regex? = null, build: String? = null) +} diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/jenkins/JenkinsPluginProviderImpl.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/jenkins/JenkinsPluginProviderImpl.kt new file mode 100644 index 0000000..dde798a --- /dev/null +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/pluginsapi/jenkins/JenkinsPluginProviderImpl.kt @@ -0,0 +1,49 @@ +/* + * Run Task Gradle Plugins + * Copyright (c) 2023 Jason Penilla + * + * 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 xyz.jpenilla.runtask.pluginsapi.jenkins + +import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.newInstance +import xyz.jpenilla.runtask.pluginsapi.JenkinsDownload +import javax.inject.Inject + +public abstract class JenkinsPluginProviderImpl @Inject constructor(private val name: String, private val objects: ObjectFactory) : JenkinsPluginProvider { + + private val jobs: MutableList = mutableListOf() + + override fun getName(): String = name + + override fun add(baseUrl: String, job: String, artifactRegex: Regex?, build: String?) { + val download = objects.newInstance(JenkinsDownload::class) + download.baseUrl.set(baseUrl) + download.job.set(job) + if (artifactRegex != null) { + download.artifactRegex.set(artifactRegex) + } + if (build != null) { + download.build.set(build) + } + jobs += download + } + + override fun copyConfiguration(api: JenkinsPluginProvider) { + jobs.addAll(api.downloads) + } + + override val downloads: Iterable + get() = jobs +} diff --git a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/Constants.kt b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/Constants.kt index b692169..286c246 100644 --- a/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/Constants.kt +++ b/plugin/src/main/kotlin/xyz/jpenilla/runtask/util/Constants.kt @@ -39,6 +39,10 @@ internal object Constants { const val MODRINTH_PLUGIN_DIR = "modrinth" const val GITHUB_PLUGIN_DIR = "github" const val URL_PLUGIN_DIR = "url" + const val JENKINS_PLUGIN_DIR = "jenkins" + + const val JENKINS_LAST_SUCCESSFUL_BUILD = "lastSuccessfulBuild" + const val JENKINS_REST_ENDPOINT = "%s/%s/api/json?tree=artifacts[relativePath]" object Plugins { const val SHADOW_PLUGIN_ID = "com.github.johnrengelman.shadow" diff --git a/tester/build.gradle.kts b/tester/build.gradle.kts index b973b5b..0510d76 100644 --- a/tester/build.gradle.kts +++ b/tester/build.gradle.kts @@ -13,6 +13,7 @@ val paperPlugins = runPaper.downloadPluginsSpec { github("jpenilla", "MiniMOTD", "v2.0.13", "minimotd-bukkit-2.0.13.jar") hangar("squaremap", "1.2.0") url("https://download.luckperms.net/1515/bukkit/loader/LuckPerms-Bukkit-5.4.102.jar") + jenkins("https://ci.athion.net", "FastAsyncWorldEdit", Regex("Bukkit")) } tasks {