Skip to content

Commit dacd0fe

Browse files
committed
Support publishing to Sonatype with kotlinx-team-infra
1 parent b8e0294 commit dacd0fe

File tree

5 files changed

+199
-20
lines changed

5 files changed

+199
-20
lines changed

main/resources/teamcity/settings.kts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ project {
4646

4747
val deployVersion = deployVersion().apply {
4848
dependsOnSnapshot(buildAll, onFailure = FailureAction.IGNORE)
49+
dependsOnSnapshot(BUILD_CREATE_STAGING_REPO_ABSOLUTE_ID)
4950
}
5051
val deploys = platforms.map { deploy(it, deployVersion) }
5152
val deployPublish = deployPublish(deployVersion).apply {
@@ -139,6 +140,8 @@ fun Project.deployVersion() = BuildType {
139140
param("bintray-user", bintrayUserName)
140141
password("bintray-key", bintrayToken)
141142
param(versionSuffixParameter, "dev-%build.counter%")
143+
param("reverse.dep.$BUILD_CREATE_STAGING_REPO_ABSOLUTE_ID.system.libs.repo.description", libraryStagingRepoDescription)
144+
param("env.libs.repository.id", "%dep.$BUILD_CREATE_STAGING_REPO_ABSOLUTE_ID.env.libs.repository.id%")
142145
}
143146

144147
requirements {
@@ -149,7 +152,7 @@ fun Project.deployVersion() = BuildType {
149152
steps {
150153
gradle {
151154
name = "Verify Gradle Configuration"
152-
tasks = "clean publishBintrayCreateVersion"
155+
tasks = "clean publishPrepareVersion"
153156
gradleParams = "--info --stacktrace -P$versionSuffixParameter=%$versionSuffixParameter% -P$releaseVersionParameter=%$releaseVersionParameter% -PbintrayApiKey=%bintray-key% -PbintrayUser=%bintray-user%"
154157
buildFile = ""
155158
jdkHome = "%env.$jdk%"
@@ -167,6 +170,7 @@ fun Project.deployPublish(configureBuild: BuildType) = BuildType {
167170
// Tell configuration build how to get release version parameter from this build
168171
// "dev" is the default and means publishing is not releasing to public
169172
text(configureBuild.reverseDepParamRefs[releaseVersionParameter].name, "dev", display = ParameterDisplay.PROMPT, label = "Release Version")
173+
param("env.libs.repository.id", "%dep.$BUILD_CREATE_STAGING_REPO_ABSOLUTE_ID.env.libs.repository.id%")
170174
}
171175
commonConfigure()
172176
}.also { buildType(it) }

main/resources/teamcity/utils.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const val releaseVersionParameter = "releaseVersion"
1111

1212
const val bintrayUserName = "<<BINTRAY_USER>>"
1313
const val bintrayToken = "<<BINTRAY_TOKEN>>"
14+
const val libraryStagingRepoDescription = "<<LIBRARY_STAGING_REPO_DESCRIPTION>>"
1415

1516
val platforms = Platform.values()
1617
const val jdk = "<<JDK>>"
@@ -37,6 +38,8 @@ const val BUILD_ALL_ID = "Build_All"
3738
const val DEPLOY_CONFIGURE_VERSION_ID = "Deploy_Configure"
3839
const val DEPLOY_PUBLISH_ID = "Deploy_Publish"
3940

41+
val BUILD_CREATE_STAGING_REPO_ABSOLUTE_ID = AbsoluteId("KotlinTools_CreateSonatypeStagingRepository")
42+
4043
class KnownBuilds(private val project: Project) {
4144
private fun buildWithId(id: String): BuildType {
4245
return project.buildTypes.single { it.id.toString().endsWith(id) }
@@ -103,12 +106,12 @@ fun BuildType.commonConfigure() {
103106
}
104107
}
105108

106-
fun BuildType.dependsOn(build: BuildType, configure: Dependency.() -> Unit) =
109+
fun BuildType.dependsOn(build: IdOwner, configure: Dependency.() -> Unit) =
107110
apply {
108111
dependencies.dependency(build, configure)
109112
}
110113

111-
fun BuildType.dependsOnSnapshot(build: BuildType, onFailure: FailureAction = FailureAction.FAIL_TO_START, configure: SnapshotDependency.() -> Unit = {}) = apply {
114+
fun BuildType.dependsOnSnapshot(build: IdOwner, onFailure: FailureAction = FailureAction.FAIL_TO_START, configure: SnapshotDependency.() -> Unit = {}) = apply {
112115
dependencies.dependency(build) {
113116
snapshot {
114117
configure()

main/src/kotlinx/team/infra/Publishing.kt

Lines changed: 182 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,30 @@ import groovy.lang.*
44
import org.gradle.api.*
55
import org.gradle.api.plugins.*
66
import org.gradle.api.publish.*
7+
import org.gradle.api.publish.maven.MavenPom
8+
import org.gradle.api.publish.maven.MavenPublication
79
import org.gradle.api.publish.maven.plugins.*
810
import org.gradle.api.publish.maven.tasks.*
911
import org.gradle.api.publish.plugins.*
1012
import org.gradle.api.tasks.*
13+
import org.gradle.jvm.tasks.Jar
14+
import org.gradle.plugins.signing.SigningExtension
15+
import org.gradle.plugins.signing.SigningPlugin
1116
import org.gradle.util.*
1217
import java.net.*
1318
import java.text.*
1419
import java.util.*
1520

1621
@Suppress("DEPRECATION")
1722
open class PublishingConfiguration {
23+
var libraryRepoUrl: String? = null
24+
25+
val sonatype = SonatypeConfiguration()
26+
fun sonatype(configure: Action<SonatypeConfiguration>) {
27+
configure.execute(sonatype)
28+
sonatype.isSelected = true
29+
}
30+
1831
@Deprecated("Avoid publishing to bintray")
1932
val bintray = BintrayConfiguration()
2033
@Deprecated("Avoid publishing to bintray")
@@ -45,7 +58,11 @@ open class PublishingConfiguration {
4558
}
4659
}
4760

48-
// TODO: Add sonatype configuration
61+
class SonatypeConfiguration {
62+
// no things to configure here for now
63+
internal var isSelected: Boolean = false
64+
}
65+
4966
// TODO: Add space configuration
5067

5168
// TODO: Remove all bintray-related configuration after migration
@@ -81,7 +98,7 @@ fun Project.configureProjectVersion() {
8198
}
8299

83100
@Suppress("DEPRECATION")
84-
fun Project.configurePublishing(publishing: PublishingConfiguration) {
101+
internal fun Project.configurePublishing(publishing: PublishingConfiguration) {
85102
val ext = extensions.getByType(ExtraPropertiesExtension::class.java)
86103

87104
val buildLocal = "buildLocal"
@@ -96,19 +113,34 @@ fun Project.configurePublishing(publishing: PublishingConfiguration) {
96113
includeProjects.forEach { subproject ->
97114
subproject.applyMavenPublish()
98115
subproject.createBuildRepository(buildLocal, rootBuildLocal)
116+
subproject.configurePublications(publishing)
99117
}
100118

101-
// If bintray is configured, create version task and configure subprojects
102-
val bintray = if (ext.get("infra.release") == true)
103-
publishing.bintray
104-
else
105-
publishing.bintrayDev ?: publishing.bintray
106-
107-
val enableBintray = verifyBintrayConfiguration(bintray)
108-
if (enableBintray) {
109-
createBintrayVersionTask(bintray)
110-
includeProjects.forEach { subproject ->
111-
subproject.createBintrayRepository(bintray)
119+
createVersionPrepareTask()
120+
121+
if (publishing.sonatype.isSelected) {
122+
if (verifySonatypeConfiguration()) {
123+
if (ext.get("infra.release") != true) {
124+
throw KotlinInfrastructureException("Cannot publish development version to Sonatype.")
125+
}
126+
includeProjects.forEach { subproject ->
127+
subproject.createSonatypeRepository()
128+
subproject.configureSigning()
129+
}
130+
}
131+
} else {
132+
// If bintray is configured, create version task and configure subprojects
133+
val bintray = if (ext.get("infra.release") == true)
134+
publishing.bintray
135+
else
136+
publishing.bintrayDev ?: publishing.bintray
137+
138+
val enableBintray = verifyBintrayConfiguration(bintray)
139+
if (enableBintray) {
140+
createBintrayVersionTask(bintray)
141+
includeProjects.forEach { subproject ->
142+
subproject.createBintrayRepository(bintray)
143+
}
112144
}
113145
}
114146

@@ -194,7 +226,7 @@ private fun Project.verifyBintrayConfiguration(bintray: BintrayConfiguration): B
194226
return true
195227
}
196228

197-
fun Project.createBintrayRepository(bintray: BintrayConfiguration) {
229+
private fun Project.createBintrayRepository(bintray: BintrayConfiguration) {
198230
val username = bintray.username
199231
?: throw KotlinInfrastructureException("Cannot create version. User has not been specified.")
200232
val password = bintray.password
@@ -222,8 +254,14 @@ private fun BintrayConfiguration.api(section: String): String {
222254
return "https://api.bintray.com/$section/$organization/$repository/$library"
223255
}
224256

225-
fun Project.createBintrayVersionTask(bintray: BintrayConfiguration) {
226-
task<DefaultTask>("publishBintrayCreateVersion") {
257+
private fun Project.createVersionPrepareTask(): TaskProvider<DefaultTask> {
258+
return task<DefaultTask>("publishPrepareVersion") {
259+
group = PublishingPlugin.PUBLISH_TASK_GROUP
260+
}
261+
}
262+
263+
private fun Project.createBintrayVersionTask(bintray: BintrayConfiguration) {
264+
val bintrayCreateVersion = task<DefaultTask>("publishBintrayCreateVersion") {
227265
group = PublishingPlugin.PUBLISH_TASK_GROUP
228266
doFirst {
229267
val username = bintray.username
@@ -265,4 +303,131 @@ fun Project.createBintrayVersionTask(bintray: BintrayConfiguration) {
265303
}
266304
}
267305
}
268-
}
306+
tasks.named("publishPrepareVersion").configure {
307+
it.dependsOn(bintrayCreateVersion)
308+
}
309+
}
310+
311+
312+
private fun Project.verifySonatypeConfiguration(): Boolean {
313+
fun missing(what: String): Boolean {
314+
logger.warn("INFRA: Sonatype publishing will not be possible due to missing $what.")
315+
return false
316+
}
317+
318+
sonatypeUsername ?: return missing("username")
319+
val password = sonatypePassword ?: return missing("password")
320+
if (password.startsWith("credentialsJSON")) {
321+
logger.warn("INFRA: API key secure token was not expanded, publishing is not possible.")
322+
return false
323+
}
324+
325+
if (password.trim() != password) {
326+
logger.warn("INFRA: API key secure token was expanded to a value with whitespace around it.")
327+
}
328+
329+
if (password.trim().isEmpty()) {
330+
logger.warn("INFRA: API key secure token was expanded to empty string.")
331+
}
332+
return true
333+
}
334+
335+
private fun Project.createSonatypeRepository() {
336+
val username = project.sonatypeUsername
337+
?: throw KotlinInfrastructureException("Cannot setup publication. User has not been specified.")
338+
val password = project.sonatypePassword
339+
?: throw KotlinInfrastructureException("Cannot setup publication. Password (API key) has not been specified.")
340+
341+
extensions.configure(PublishingExtension::class.java) { publishing ->
342+
publishing.repositories.maven { repo ->
343+
repo.name = "sonatype"
344+
repo.url = sonatypeRepositoryUri()
345+
repo.credentials { credentials ->
346+
credentials.username = username
347+
credentials.password = password.trim()
348+
}
349+
}
350+
}
351+
}
352+
353+
private fun Project.configurePublications(publishing: PublishingConfiguration) {
354+
if (publishing.libraryRepoUrl.isNullOrEmpty()) {
355+
logger.warn("INFRA: library source control repository URL is not set, publication won't be accepted by Sonatype.")
356+
}
357+
val javadocJar = tasks.create("javadocJar", Jar::class.java).apply {
358+
archiveClassifier.set("javadoc")
359+
}
360+
extensions.configure(PublishingExtension::class.java) { publishingExtension ->
361+
publishingExtension.publications.all {
362+
with(it as MavenPublication) {
363+
artifact(javadocJar)
364+
configureRequiredPomAttributes(project, publishing)
365+
}
366+
}
367+
}
368+
}
369+
370+
fun Project.mavenPublicationsPom(action: Action<MavenPom>) {
371+
extensions.configure(PublishingExtension::class.java) { publishingExtension ->
372+
publishingExtension.publications.all {
373+
action.execute((it as MavenPublication).pom)
374+
}
375+
}
376+
}
377+
378+
private fun MavenPublication.configureRequiredPomAttributes(project: Project, publishing: PublishingConfiguration) {
379+
val publication = this
380+
// TODO: get rid of 'it's
381+
pom {
382+
it.name.set(publication.artifactId)
383+
it.description.set(project.description ?: publication.artifactId)
384+
it.url.set(publishing.libraryRepoUrl)
385+
it.licenses {
386+
it.license {
387+
it.name.set("The Apache License, Version 2.0")
388+
it.url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
389+
}
390+
}
391+
it.scm {
392+
it.url.set(publishing.libraryRepoUrl)
393+
}
394+
it.developers {
395+
it.developer {
396+
it.name.set("JetBrains Team")
397+
it.organization.set("JetBrains")
398+
it.organizationUrl.set("https://www.jetbrains.com")
399+
}
400+
}
401+
}
402+
}
403+
404+
private fun Project.configureSigning() {
405+
project.pluginManager.apply(SigningPlugin::class.java)
406+
val keyId = project.propertyOrEnv("libs.sign.key.id")
407+
val signingKey = project.propertyOrEnv("libs.sign.key.private")
408+
val signingKeyPassphrase = project.propertyOrEnv("libs.sign.passphrase")
409+
410+
if (keyId != null) {
411+
project.extensions.configure<SigningExtension>("signing") {
412+
it.useInMemoryPgpKeys(keyId, signingKey, signingKeyPassphrase)
413+
it.sign(extensions.getByType(PublishingExtension::class.java).publications) // all publications
414+
}
415+
} else {
416+
logger.warn("INFRA: signing key id is not specified, artifact signing is not enabled.")
417+
}
418+
}
419+
420+
421+
private fun Project.sonatypeRepositoryUri(): URI {
422+
val repositoryId: String? = System.getenv("libs.repository.id")
423+
return if (repositoryId == null) {
424+
// Using implicitly created staging, for MPP it's likely a mistake
425+
logger.warn("INFRA: using an implicitly created staging for ${project.rootProject.name}")
426+
URI("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
427+
} else {
428+
URI("https://oss.sonatype.org/service/local/staging/deployByRepositoryId/$repositoryId")
429+
}
430+
}
431+
432+
private val Project.sonatypeUsername: String? get() = propertyOrEnv("libs.sonatype.user")
433+
private val Project.sonatypePassword: String? get() = propertyOrEnv("libs.sonatype.password")

main/src/kotlinx/team/infra/TeamCity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import org.gradle.api.*
44
import java.io.*
55

66
class TeamCityConfiguration {
7+
var libraryStagingRepoDescription: String? = null
8+
79
@Deprecated("Avoid publishing to bintray")
810
var bintrayUser: String? = null
911
@Deprecated("Avoid publishing to bintray")
@@ -42,9 +44,12 @@ fun Project.configureTeamCityConfigGenerator(teamcity: TeamCityConfiguration) {
4244
?: "%env.BINTRAY_USER%" //throw KotlinInfrastructureException("TeamCity configuration should specify `bintrayUser` parameter")
4345
val bintrayToken = teamcity.bintrayToken
4446
?: "%env.BINTRAY_API_KEY%" //throw KotlinInfrastructureException("TeamCity configuration should specify `bintrayToken` parameter")
47+
val libraryStagingRepoDescription = teamcity.libraryStagingRepoDescription
48+
?: throw KotlinInfrastructureException("TeamCity configuration should specify `libraryStagingRepoDescription`: the library description for staging repositories")
4549
text
4650
.replace("<<BINTRAY_USER>>", bintrayUser)
4751
.replace("<<BINTRAY_TOKEN>>", bintrayToken)
52+
.replace("<<LIBRARY_STAGING_REPO_DESCRIPTION>>", libraryStagingRepoDescription)
4853
.replace("<<JDK>>", teamcity.jdk)
4954
}
5055
copyResource(teamcityDir, "additionalConfiguration.kt", override = false)

main/src/kotlinx/team/infra/Utils.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,5 @@ fun Logger.infra(message: String) {
5757
info("INFRA: $message")
5858
}
5959

60+
fun Project.propertyOrEnv(name: String): String? =
61+
findProperty(name) as? String ?: System.getenv(name)

0 commit comments

Comments
 (0)