From 32474cc76700984794d2079d962fb7bfb3692fb1 Mon Sep 17 00:00:00 2001 From: Robert Stupp Date: Thu, 16 Oct 2025 12:51:13 +0200 Subject: [PATCH] Reproducible builds: make parent pom content reproducible The parent pom contains the `` and `` elements. The former is populated from ASF people information including role information (champion, mentor, chair, (P)PMC member, committer). The latter is retrieved from a GitHub API endpoint, ordered by number contributions. Especially the latter list is prone to vary between builds, which makes the parent pom not reproducible as the locally built one is likely different from the one that was built by the release managed (staged artifact). This change removes both lists, leaving a single static `` entry pointing to `https://polaris.apache.org/community/`. Related build-script code has been updated and no longer retrieves people information. --- .../main/kotlin/publishing/configurePom.kt | 71 ++++-------- .../src/main/kotlin/publishing/util.kt | 105 ++++++------------ 2 files changed, 52 insertions(+), 124 deletions(-) diff --git a/build-logic/src/main/kotlin/publishing/configurePom.kt b/build-logic/src/main/kotlin/publishing/configurePom.kt index b81c51d2a6..c140dd2cd7 100644 --- a/build-logic/src/main/kotlin/publishing/configurePom.kt +++ b/build-logic/src/main/kotlin/publishing/configurePom.kt @@ -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 @@ -36,13 +35,14 @@ 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 `` and ``. Most of the - * information is taken from publicly consumable Apache project information from - * `https://projects.apache.org/json/projects/>.json`. `` contains all - * (P)PMC members and committers from that project info JSON, ordered by real name. `` - * is taken from GitHub's - * `https://api.github.com/repos/apache//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/.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 { @@ -50,7 +50,7 @@ internal fun configurePom(project: Project, mavenPublication: MavenPublication, 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 { @@ -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") @@ -84,7 +84,7 @@ 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 { @@ -92,7 +92,7 @@ internal fun configurePom(project: Project, mavenPublication: MavenPublication, 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") } @@ -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 { @@ -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 = - 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> = - 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 `` in `` that do not have a * `` and adds one, if possible. Maven kinda requires `` tags there, even if the diff --git a/build-logic/src/main/kotlin/publishing/util.kt b/build-logic/src/main/kotlin/publishing/util.kt index 864a14c72c..fd1366731d 100644 --- a/build-logic/src/main/kotlin/publishing/util.kt +++ b/build-logic/src/main/kotlin/publishing/util.kt @@ -99,36 +99,26 @@ internal fun 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> = - parseJson("https://whimsy.apache.org/public/public_ldap_projects.json") - val projects = unsafeCast>>(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> = - parseJson("https://whimsy.apache.org/public/public_podlings.json") - val podlings = unsafeCast>>(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> = - parseJson("https://whimsy.apache.org/public/committee-info.json") - val committees = unsafeCast>>(committeesAll["committees"]) - val committee = unsafeCast>(committees[apacheId]) + val committee = projectCommitteeMap(apacheId) return committee["display_name"] as String } } -internal fun fetchProjectPeople(apacheId: String): ProjectPeople { +internal fun projectCommitteeMap(apacheId: String): Map { + val committeesAll: Map> = + parseJson("https://whimsy.apache.org/public/committee-info.json") + val committees = unsafeCast>>(committeesAll["committees"]) + return unsafeCast(committees[apacheId]) +} + +internal fun projectMap(apacheId: String): Map { val projectsAll: Map> = parseJson("https://whimsy.apache.org/public/public_ldap_projects.json") val projects = unsafeCast>>(projectsAll["projects"]) @@ -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 { + val podlingsAll: Map> = + parseJson("https://whimsy.apache.org/public/public_podlings.json") + val podlings = unsafeCast>>(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> = mutableMapOf() - val members = unsafeCast(project["members"]) as List - 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 - owners.forEach { member -> peopleProjectRoles[member]!!.add(pmcRoleName) } + val inceptionYear = (project["createTimestamp"] as String).subSequence(0, 4).toString().toInt() val projectName: String val description: String @@ -158,14 +155,7 @@ internal fun fetchProjectPeople(apacheId: String): ProjectPeople { val licenseUrl: String val bugDatabase: String if (isPodlingCurrent) { - val podlingsAll: Map> = - parseJson("https://whimsy.apache.org/public/public_podlings.json") - val podlings = unsafeCast>>(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 @@ -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 - mentors.forEach { member -> peopleProjectRoles[member]!!.add("Mentor") } } else { // top-level-project val tlpPrj: Map = @@ -189,33 +173,12 @@ internal fun fetchProjectPeople(apacheId: String): ProjectPeople { bugDatabase = tlpPrj["bug-database"] as String licenseUrl = tlpPrj["license"] as String - val committeesAll: Map> = - parseJson("https://whimsy.apache.org/public/committee-info.json") - val committees = unsafeCast>>(committeesAll["committees"]) - val committee = unsafeCast>(committees[apacheId]) - val pmcChair = unsafeCast>>(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> = - parseJson("https://whimsy.apache.org/public/public_ldap_people.json") - val people: Map> = - unsafeCast(peopleNames["people"]) as Map> - 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, @@ -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, @@ -237,7 +199,4 @@ internal class ProjectPeople( val licenseUrl: String, val bugDatabase: String, val inceptionYear: Int, - val people: List, ) - -internal class ProjectMember(val apacheId: String, val name: String, val roles: List)