Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 20 additions & 51 deletions build-logic/src/main/kotlin/publishing/configurePom.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.component.ModuleComponentSelector
import org.gradle.api.provider.Provider
import org.gradle.api.publish.maven.MavenPom
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.internal.extensions.stdlib.capitalized

Expand All @@ -36,21 +35,22 @@ import org.gradle.internal.extensions.stdlib.capitalized
* consumable by Maven.
*
* The root project generates the parent pom, containing all the necessary elements to pass Sonatype
* validation and some more information like `<developers>` and `<contributors>`. Most of the
* information is taken from publicly consumable Apache project information from
* `https://projects.apache.org/json/projects/<project-name>>.json`. `<developers>` contains all
* (P)PMC members and committers from that project info JSON, ordered by real name. `<contributors>`
* is taken from GitHub's
* `https://api.github.com/repos/apache/<project-name>/contributors?per_page=1000` endpoint to give
* all contributors credit, ordered by number of contributions (as returned by that endpoint).
* validation. Most of the information is taken from publicly consumable Apache project information
* from `https://projects.apache.org/json/projects/<project-name>.json`. Changes to the Apache
* project metadata, including podling information, will break the reproducibility of the build.
*
* Developer and contributor elements are intentionally *not* included in the POM. Such information
* is not considered stable (enough) to satisfy reproducible build requirements. The generated POM
* must be exactly the same when built by a release manager and by someone else to verify the built
* artifact(s).
*/
internal fun configurePom(project: Project, mavenPublication: MavenPublication, task: Task) =
mavenPublication.run {
val e = project.extensions.getByType(PublishingHelperExtension::class.java)

pom {
if (project != project.rootProject) {
// Add the license to every pom to make it easier for downstream project to retrieve the
// Add the license to every pom to make it easier for downstream projects to retrieve the
// license.
licenses {
license {
Expand All @@ -75,7 +75,7 @@ internal fun configurePom(project: Project, mavenPublication: MavenPublication,
task.doFirst {
mavenPom.run {
val asfName = e.asfProjectId.get()
val projectPeople = fetchProjectPeople(asfName)
val projectInfo = fetchProjectInformation(asfName)

organization {
name.set("The Apache Software Foundation")
Expand All @@ -84,15 +84,15 @@ internal fun configurePom(project: Project, mavenPublication: MavenPublication,
licenses {
license {
name.set("Apache-2.0") // SPDX identifier
url.set(projectPeople.licenseUrl)
url.set(projectInfo.licenseUrl)
}
}
mailingLists {
e.mailingLists.get().forEach { ml ->
mailingList {
name.set("${ml.capitalized()} Mailing List")
subscribe.set("$ml-subscribe@$asfName.apache.org")
unsubscribe.set("$ml-ubsubscribe@$asfName.apache.org")
unsubscribe.set("$ml-unsubscribe@$asfName.apache.org")
post.set("$ml@$asfName.apache.org")
archive.set("https://lists.apache.org/list.html?$ml@$asfName.apache.org")
}
Expand All @@ -104,7 +104,7 @@ internal fun configurePom(project: Project, mavenPublication: MavenPublication,
e.overrideScm.orElse(
githubRepoName
.map { r -> "https://github.com/apache/$r" }
.orElse(projectPeople.repository)
.orElse(projectInfo.repository)
)

scm {
Expand All @@ -115,59 +115,28 @@ internal fun configurePom(project: Project, mavenPublication: MavenPublication,
val version = project.version.toString()
if (!version.endsWith("-SNAPSHOT")) {
val tagPrefix: String =
e.overrideTagPrefix.orElse("apache-${projectPeople.apacheId}").get()
e.overrideTagPrefix.orElse("apache-${projectInfo.apacheId}").get()
tag.set("$tagPrefix-$version")
}
}
issueManagement {
val issuesUrl: Provider<String> =
codeRepo.map { r -> "$r/issues" }.orElse(projectPeople.bugDatabase)
codeRepo.map { r -> "$r/issues" }.orElse(projectInfo.bugDatabase)
url.set(e.overrideIssueManagement.orElse(issuesUrl))
}

name.set(e.overrideName.orElse("Apache ${projectPeople.name}"))
description.set(e.overrideDescription.orElse(projectPeople.description))
url.set(e.overrideProjectUrl.orElse(projectPeople.website))
inceptionYear.set(projectPeople.inceptionYear.toString())

developers {
projectPeople.people.forEach { person ->
developer {
this.id.set(person.apacheId)
this.name.set(person.name)
this.organization.set("Apache Software Foundation")
this.email.set("${person.apacheId}@apache.org")
this.roles.addAll(person.roles)
}
}
}
name.set(e.overrideName.orElse("Apache ${projectInfo.name}"))
description.set(e.overrideDescription.orElse(projectInfo.description))
url.set(e.overrideProjectUrl.orElse(projectInfo.website))
inceptionYear.set(projectInfo.inceptionYear.toString())

addContributorsToPom(mavenPom, githubRepoName.get(), "Apache ${projectPeople.name}")
developers { developer { url.set("https://$asfName.apache.org/community/") } }
}
}
}
}
}

/** Adds contributors as returned by GitHub, in descending `contributions` order. */
fun addContributorsToPom(mavenPom: MavenPom, asfName: String, asfProjectName: String) =
mavenPom.run {
contributors {
val contributors: List<Map<String, Any>> =
parseJson("https://api.github.com/repos/apache/$asfName/contributors?per_page=1000")
contributors
.filter { contributor -> contributor["type"] == "User" }
.forEach { contributor ->
contributor {
name.set(contributor["login"] as String)
url.set(contributor["url"] as String)
organization.set("$asfProjectName, GitHub contributors")
organizationUrl.set("https://github.com/apache/$asfName")
}
}
}
}

/**
* Scans the generated `pom.xml` for `<dependencies>` in `<dependencyManagement>` that do not have a
* `<version>` and adds one, if possible. Maven kinda requires `<version>` tags there, even if the
Expand Down
105 changes: 32 additions & 73 deletions build-logic/src/main/kotlin/publishing/util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,36 +99,26 @@ internal fun <T : Any> parseJson(urlStr: String): T {

/** Retrieves the project name, for example `Polaris` using the lower-case project ID. */
internal fun fetchAsfProjectName(apacheId: String): String {
val projectsAll: Map<String, Map<String, Any>> =
parseJson("https://whimsy.apache.org/public/public_ldap_projects.json")
val projects = unsafeCast<Map<String, Map<String, Any>>>(projectsAll["projects"])
val project =
projects[apacheId]
?: throw IllegalArgumentException(
"No project '$apacheId' found in https://whimsy.apache.org/public/public_ldap_projects.json"
)
val project = projectMap(apacheId)
val isPodlingCurrent = project.containsKey("podling") && project["podling"] == "current"
if (isPodlingCurrent) {
val podlingsAll: Map<String, Map<String, Any>> =
parseJson("https://whimsy.apache.org/public/public_podlings.json")
val podlings = unsafeCast<Map<String, Map<String, Any>>>(podlingsAll["podling"])
val podling =
podlings[apacheId]
?: throw IllegalArgumentException(
"No podling '$apacheId' found in https://whimsy.apache.org/public/public_podlings.json"
)
val podling = podlingMap(apacheId)
return podling["name"] as String
} else {
// top-level-project
val committeesAll: Map<String, Map<String, Any>> =
parseJson("https://whimsy.apache.org/public/committee-info.json")
val committees = unsafeCast<Map<String, Map<String, Any>>>(committeesAll["committees"])
val committee = unsafeCast<Map<String, Any>>(committees[apacheId])
val committee = projectCommitteeMap(apacheId)
return committee["display_name"] as String
}
}

internal fun fetchProjectPeople(apacheId: String): ProjectPeople {
internal fun projectCommitteeMap(apacheId: String): Map<String, Any> {
val committeesAll: Map<String, Map<String, Any>> =
parseJson("https://whimsy.apache.org/public/committee-info.json")
val committees = unsafeCast<Map<String, Map<String, Any>>>(committeesAll["committees"])
return unsafeCast(committees[apacheId])
}

internal fun projectMap(apacheId: String): Map<String, Any> {
val projectsAll: Map<String, Map<String, Any>> =
parseJson("https://whimsy.apache.org/public/public_ldap_projects.json")
val projects = unsafeCast<Map<String, Map<String, Any>>>(projectsAll["projects"])
Expand All @@ -137,19 +127,26 @@ internal fun fetchProjectPeople(apacheId: String): ProjectPeople {
?: throw IllegalArgumentException(
"No project '$apacheId' found in https://whimsy.apache.org/public/public_ldap_projects.json"
)
val isPodlingCurrent = project.containsKey("podling") && project["podling"] == "current"
return project
}

val inceptionYear = (project["createTimestamp"] as String).subSequence(0, 4).toString().toInt()
internal fun podlingMap(apacheId: String): Map<String, Any> {
val podlingsAll: Map<String, Map<String, Any>> =
parseJson("https://whimsy.apache.org/public/public_podlings.json")
val podlings = unsafeCast<Map<String, Map<String, Any>>>(podlingsAll["podling"])
val podling =
podlings[apacheId]
?: throw IllegalArgumentException(
"No podling '$apacheId' found in https://whimsy.apache.org/public/public_podlings.json"
)
return podling
}

// Committers
val peopleProjectRoles: MutableMap<String, MutableList<String>> = mutableMapOf()
val members = unsafeCast(project["members"]) as List<String>
members.forEach { member -> peopleProjectRoles.put(member, mutableListOf("Committer")) }
internal fun fetchProjectInformation(apacheId: String): ProjectInformation {
val project = projectMap(apacheId)
val isPodlingCurrent = project.containsKey("podling") && project["podling"] == "current"

// (P)PMC Members
val pmcRoleName = if (isPodlingCurrent) "PPMC member" else "PMC member"
val owners = unsafeCast(project["owners"]) as List<String>
owners.forEach { member -> peopleProjectRoles[member]!!.add(pmcRoleName) }
val inceptionYear = (project["createTimestamp"] as String).subSequence(0, 4).toString().toInt()

val projectName: String
val description: String
Expand All @@ -158,14 +155,7 @@ internal fun fetchProjectPeople(apacheId: String): ProjectPeople {
val licenseUrl: String
val bugDatabase: String
if (isPodlingCurrent) {
val podlingsAll: Map<String, Map<String, Any>> =
parseJson("https://whimsy.apache.org/public/public_podlings.json")
val podlings = unsafeCast<Map<String, Map<String, Any>>>(podlingsAll["podling"])
val podling =
podlings[apacheId]
?: throw IllegalArgumentException(
"No podling '$apacheId' found in https://whimsy.apache.org/public/public_podlings.json"
)
val podling = podlingMap(apacheId)
projectName = podling["name"] as String
description = podling["description"] as String
val podlingStatus = unsafeCast(podling["podlingStatus"]) as Map<String, Any>
Expand All @@ -174,12 +164,6 @@ internal fun fetchProjectPeople(apacheId: String): ProjectPeople {
repository = "https://github.com/apache/$apacheId.git"
bugDatabase = "https://github.com/apache/$apacheId/issues"
licenseUrl = "https://www.apache.org/licenses/LICENSE-2.0.txt"

val champion = podling["champion"] as String
peopleProjectRoles[champion]!!.add("Champion")

val mentors = unsafeCast(podling["mentors"]) as List<String>
mentors.forEach { member -> peopleProjectRoles[member]!!.add("Mentor") }
} else {
// top-level-project
val tlpPrj: Map<String, Any> =
Expand All @@ -189,33 +173,12 @@ internal fun fetchProjectPeople(apacheId: String): ProjectPeople {
bugDatabase = tlpPrj["bug-database"] as String
licenseUrl = tlpPrj["license"] as String

val committeesAll: Map<String, Map<String, Any>> =
parseJson("https://whimsy.apache.org/public/committee-info.json")
val committees = unsafeCast<Map<String, Map<String, Any>>>(committeesAll["committees"])
val committee = unsafeCast<Map<String, Any>>(committees[apacheId])
val pmcChair = unsafeCast<Map<String, Map<String, Any>>>(committee["chair"])
val committee = projectCommitteeMap(apacheId)
projectName = committee["display_name"] as String
description = committee["description"] as String
pmcChair.keys.forEach { chair -> peopleProjectRoles[chair]!!.add("PMC Chair") }
}

val peopleNames: Map<String, Map<String, Any>> =
parseJson("https://whimsy.apache.org/public/public_ldap_people.json")
val people: Map<String, Map<String, Any>> =
unsafeCast(peopleNames["people"]) as Map<String, Map<String, Any>>
val peopleList =
peopleProjectRoles.entries
.map { entry ->
val person =
people[entry.key]
?: throw IllegalStateException(
"No person '${entry.key}' found in https://whimsy.apache.org/public/public_ldap_people.json"
)
ProjectMember(entry.key, person["name"]!! as String, entry.value)
}
.sortedBy { it.name }

return ProjectPeople(
return ProjectInformation(
apacheId,
projectName,
description,
Expand All @@ -224,11 +187,10 @@ internal fun fetchProjectPeople(apacheId: String): ProjectPeople {
licenseUrl,
bugDatabase,
inceptionYear,
peopleList,
)
}

internal class ProjectPeople(
internal class ProjectInformation(
val apacheId: String,
val name: String,
val description: String,
Expand All @@ -237,7 +199,4 @@ internal class ProjectPeople(
val licenseUrl: String,
val bugDatabase: String,
val inceptionYear: Int,
val people: List<ProjectMember>,
)

internal class ProjectMember(val apacheId: String, val name: String, val roles: List<String>)