从密钥文件/环境加载配置后发布至Maven Central遇签名问题求助
问题:发布Java库到Maven Central时的签名错误与配置优化
1. 场景与问题
我用Kotlin编写了build.gradle.kts脚本,尝试从密钥文件/环境变量加载配置,将无依赖纯Java库发布到Maven Central,但遇到签名相关错误。脚本如下:
import java.util.Properties plugins { id("org.jetbrains.kotlin.jvm") id("maven-publish") id("signing") } java { withSourcesJar() withJavadocJar() toolchain.languageVersion.set(JavaLanguageVersion.of(8)) } val POM_ARTIFACT_ID="sizeunit" val VERSION_NAME="0.0.1" val VERSION_CODE=1 val propsFile = File("${rootProject.rootDir.path}/a_secrets/publish.properties") val props = if(propsFile.exists()) Properties().also { it.load(propsFile.inputStream()) } else null val GROUP: String = props?.getProperty("GROUP")?:System.getenv("GROUP")?:"" val POM_URL: String = props?.getProperty("POM_URL")?:System.getenv("POM_URL")?:"" val POM_SCM_URL: String = props?.getProperty("POM_SCM_URL")?:System.getenv("POM_SCM_URL")?:"" val POM_SCM_CONNECTION: String = props?.getProperty("POM_SCM_CONNECTION")?:System.getenv("POM_SCM_CONNECTION")?:"" val POM_SCM_DEV_CONNECTION: String = props?.getProperty("POM_SCM_DEV_CONNECTION")?:System.getenv("POM_SCM_DEV_CONNECTION")?:"" val POM_NAME: String = props?.getProperty("POM_NAME")?:System.getenv("POM_NAME")?:"" val POM_INCEPTION_YEAR: String = props?.getProperty("POM_INCEPTION_YEAR")?:System.getenv("POM_INCEPTION_YEAR")?:"" val POM_DESCRIPTION: String = props?.getProperty("POM_DESCRIPTION")?:System.getenv("POM_DESCRIPTION")?:"" val POM_DEVELOPER_ID: String = props?.getProperty("POM_DEVELOPER_ID")?:System.getenv("POM_DEVELOPER_ID")?:"" val POM_DEVELOPER_NAME: String = props?.getProperty("POM_DEVELOPER_NAME")?:System.getenv("POM_DEVELOPER_NAME")?:"" val POM_DEVELOPER_URL: String = props?.getProperty("POM_DEVELOPER_URL")?:System.getenv("POM_DEVELOPER_URL")?:"" val POM_DEVELOPER_EMAIL: String = props?.getProperty("POM_DEVELOPER_EMAIL")?:System.getenv("POM_DEVELOPER_EMAIL")?:"" val POM_LICENSE_NAME: String = props?.getProperty("POM_LICENSE_NAME")?:System.getenv("POM_LICENSE_NAME")?:"" val POM_LICENSE_URL: String = props?.getProperty("POM_LICENSE_URL")?:System.getenv("POM_LICENSE_URL")?:"" val POM_LICENSE_DIST: String = props?.getProperty("POM_LICENSE_DIST")?:System.getenv("POM_LICENSE_DIST")?:"" val mavenCentralPassword: String = props?.getProperty("mavenCentralPassword")?:System.getenv("mavenCentralPassword")?:"" val mavenCentralUsername: String = props?.getProperty("mavenCentralUsername")?:System.getenv("mavenCentralUsername")?:"" val mavenUseSnapshotLink: Boolean = (props?.getProperty("mavenUseSnapshotLink") ?: System.getenv("mavenUseSnapshotLink") ?: "").toBoolean() val signingPassword: String = props?.getProperty("signing.password")?:System.getenv("signing.password")?:"" val signingSecretKeyRingFile: String = props?.getProperty("signing.secretKeyRingFile")?:System.getenv("signing.secretKeyRingFile")?:"" val signingKeyId: String = props?.getProperty("signing.keyId")?:System.getenv("signing.keyId")?:"" //Configure java doc tasks.javadoc { if (JavaVersion.current().isJava9Compatible) { (options as? StandardJavadocDocletOptions)?.addBooleanOption("html5", true) } } afterEvaluate { publishing { publications { create<MavenPublication>("mavenJava") { groupId = GROUP artifactId =POM_ARTIFACT_ID version = VERSION_NAME from(components["java"]) pom { name.set(POM_NAME) description.set(POM_DESCRIPTION) url.set(POM_URL) inceptionYear.set(POM_INCEPTION_YEAR) licenses { license { name.set(POM_LICENSE_NAME) url.set(POM_LICENSE_URL) distribution.set(POM_LICENSE_DIST) } } developers { developer { id.set(POM_DEVELOPER_ID) name.set(POM_DEVELOPER_NAME) email.set(POM_DEVELOPER_EMAIL) url.set(POM_DEVELOPER_URL) } } scm { connection.set(POM_SCM_CONNECTION) developerConnection.set(POM_SCM_DEV_CONNECTION) url.set(POM_SCM_URL) } } } } repositories { maven { val repoUrl = if (mavenUseSnapshotLink) "https://s01.oss.sonatype.org/content/repositories/snapshots/" else "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" name = "mavenCentral" setUrl(repoUrl) credentials { username = mavenCentralUsername password = mavenCentralPassword } } } } signing { val signingKey = File(signingSecretKeyRingFile).readText(charset = Charsets.US_ASCII) //setRequired { gradle.taskGraph.allTasks.any { it is PublishToMavenRepository } } useInMemoryPgpKeys(signingKeyId,signingKey,signingPassword) sign(publishing.publications["mavenJava"]) } }
执行签名任务时出现错误:
Execution failed for task ':sizeunit:signMavenJavaPublication'. > Cannot perform signing task ':sizeunit:signMavenJavaPublication' because it has no configured signatory
取消注释setRequired { gradle.taskGraph.allTasks.any { it is PublishToMavenRepository } }后,publishToMavenLocal能正常运行,但发布到正式仓库的publishMavenJavaPublicationToMavenCentralRepository任务仍失败。
另外还有两个疑问:
- 当前在模块
build.gradle.kts主代码块中读取密钥文件/环境变量获取敏感数据的方式是否合理?有无更优方案? - 项目是多模块Android应用,该模块是无依赖纯Java库,希望生成适配Java与Android库的通用发布脚本。
2. 解决方案
2.1 签名错误修复
核心原因
- 签名依赖的敏感参数(
signingKeyId、密钥文件内容、signingPassword)可能为空,导致useInMemoryPgpKeys无法初始化有效签名者。 - 签名配置的时机与Gradle任务图解析顺序冲突,导致签名者未正确绑定到远程发布任务。
修复步骤
- 添加参数校验:在签名配置前强制校验必要参数,避免空值导致初始化失败:
signing { require(signingKeyId.isNotEmpty()) { "signing.keyId 必须通过配置文件或环境变量设置" } require(signingPassword.isNotEmpty()) { "signing.password 必须通过配置文件或环境变量设置" } val keyFile = File(signingSecretKeyRingFile) require(keyFile.exists()) { "签名密钥文件不存在:${keyFile.absolutePath}" } val signingKey = keyFile.readText(charset = Charsets.US_ASCII) useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) sign(publishing.publications["mavenJava"]) // 仅针对远程MavenCentral发布要求签名 setRequired { gradle.taskGraph.allTasks.any { task -> task is PublishToMavenRepository && task.repository.name == "mavenCentral" } } } - 调整配置时机:将签名配置嵌套到
publishing块内部,确保发布对象完全初始化后再绑定签名逻辑:afterEvaluate { publishing { // ... 现有 publications 和 repositories 配置 ... signing { // 校验与签名代码同上 } } } - 版本格式检查:正式版本号不能包含
SNAPSHOT,快照版本需确保mavenUseSnapshotLink设为true,否则Sonatype仓库会拒绝接收。
2.2 敏感数据管理优化
当前方案可行,但有更规范的优化方向:
- 使用用户级Gradle属性:将敏感数据放在
~/.gradle/gradle.properties中,避免提交到版本控制:
脚本中直接读取属性:# ~/.gradle/gradle.properties GROUP=com.yourgroup signing.keyId=XXXXXXX signing.password=yourpassword signing.secretKeyRingFile=/path/to/secring.gpg mavenCentralUsername=yoursonatypeusername mavenCentralPassword=yoursontypepasswordval GROUP: String by project val signingKeyId: String by project - 优先级控制:保留环境变量优先于本地属性的逻辑,适配CI/CD场景:
val signingKeyId: String = System.getenv("signing.keyId") ?: project.properties["signing.keyId"]?.toString() ?: "" - 跨平台路径处理:用
rootProject.file()替代File(),让Gradle自动处理不同系统的路径格式:val propsFile = rootProject.file("a_secrets/publish.properties")
2.3 适配Java与Android库的通用发布脚本
将发布逻辑提取到buildSrc的自定义插件中,实现跨模块复用:
- 在
buildSrc/src/main/kotlin下创建MavenPublishPlugin.kt:import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.plugins.signing.SigningExtension import java.util.Properties class MavenPublishPlugin : Plugin<Project> { override fun apply(project: Project) { project.plugins.apply("maven-publish") project.plugins.apply("signing") val propsFile = project.rootProject.file("a_secrets/publish.properties") val props = if (propsFile.exists()) Properties().also { it.load(propsFile.inputStream()) } else null // 定义扩展,允许模块自定义配置 val extension = project.extensions.create("mavenPublishConfig", MavenPublishConfig::class.java) project.afterEvaluate { val publishing = project.extensions.getByType(PublishingExtension::class.java) publishing.publications.create("maven", MavenPublication::class.java) { groupId = extension.groupId ?: props?.getProperty("GROUP") ?: System.getenv("GROUP") ?: "" artifactId = extension.artifactId ?: project.name version = extension.version ?: props?.getProperty("VERSION_NAME") ?: System.getenv("VERSION_NAME") ?: "" // 根据模块类型选择发布组件 val componentName = if (project.plugins.hasPlugin("com.android.library")) "release" else "java" from(project.components[componentName]) // POM配置 pom { name.set(extension.pomName ?: props?.getProperty("POM_NAME") ?: project.name) description.set(extension.pomDescription ?: props?.getProperty("POM_DESCRIPTION") ?: "") url.set(extension.pomUrl ?: props?.getProperty("POM_URL") ?: "") licenses { license { name.set(extension.pomLicenseName ?: props?.getProperty("POM_LICENSE_NAME") ?: "") url.set(extension.pomLicenseUrl ?: props?.getProperty("POM_LICENSE_URL") ?: "") distribution.set(extension.pomLicenseDist ?: props?.getProperty("POM_LICENSE_DIST") ?: "repo") } } developers { developer { id.set(extension.pomDeveloperId ?: props?.getProperty("POM_DEVELOPER_ID") ?: "") name.set(extension.pomDeveloperName ?: props?.getProperty("POM_DEVELOPER_NAME") ?: "") email.set(extension.pomDeveloperEmail ?: props?.getProperty("POM_DEVELOPER_EMAIL") ?: "") } } scm { connection.set(extension.pomScmConnection ?: props?.getProperty("POM_SCM_CONNECTION") ?: "") developerConnection.set(extension.pomScmDevConnection ?: props?.getProperty("POM_SCM_DEV_CONNECTION") ?: "") url.set(extension.pomScmUrl ?: props?.getProperty("POM_SCM_URL") ?: "") } } } // 仓库配置 publishing.repositories.maven { val useSnapshot = extension.useSnapshot ?: props?.getProperty("mavenUseSnapshotLink")?.toBoolean() ?: System.getenv("mavenUseSnapshotLink")?.toBoolean() ?: false val repoUrl = if (useSnapshot) "https://s01.oss.sonatype.org/content/repositories/snapshots/" else "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" name = "mavenCentral" url = project.uri(repoUrl) credentials { username = extension.mavenUsername ?: props?.getProperty("mavenCentralUsername") ?: System.getenv("mavenCentralUsername") ?: "" password = extension.mavenPassword ?: props?.getProperty("mavenCentralPassword") ?: System.getenv("mavenCentralPassword") ?: "" } } // 签名配置 val signing = project.extensions.getByType(SigningExtension::class.java) val signingKeyId = extension.signingKeyId ?: props?.getProperty("signing.keyId") ?: System.getenv("signing.keyId") ?: "" val signingPassword = extension.signingPassword ?: props?.getProperty("signing.password") ?: System.getenv("signing.password") ?: "" val signingKeyPath = extension.signingKeyFile ?: props?.getProperty("signing.secretKeyRingFile") ?: System.getenv("signing.secretKeyRingFile") ?: "" if (signingKeyId.isNotEmpty() && signingPassword.isNotEmpty() && signingKeyPath.isNotEmpty()) { val signingKey = project.file(signingKeyPath).readText(charset = Charsets.US_ASCII) signing.useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) signing.sign(publishing.publications["maven"]) signing.setRequired { project.gradle.taskGraph.allTasks.any { task -> task is org.gradle.api.publish.PublishToMavenRepository && task.repository.name == "mavenCentral" } } } } } } open class MavenPublishConfig { var groupId: String? = null var artifactId: String? = null var version: String? = null var pomName: String? = null var pomDescription: String? = null var pomUrl: String? = null var pomLicenseName: String? = null var pomLicenseUrl: String? = null var pomLicenseDist: String? = null var pomDeveloperId: String? = null var pomDeveloperName: String? = null var pomDeveloperEmail: String? = null var pomScmConnection: String? = null var pomScmDevConnection: String? = null var pomScmUrl: String? = null var mavenUsername: String? = null var mavenPassword: String? = null var useSnapshot: Boolean? = null var signingKeyId: String? = null var signingPassword: String? = null var signingKeyFile: String? = null } - 在根项目
build.gradle.kts中配置buildSrc:plugins { `kotlin-dsl` } repositories { mavenCentral() } - Java模块中应用插件:
plugins { id("org.jetbrains.kotlin.jvm") id("maven-publish-plugin") } java { withSourcesJar() withJavadocJar() toolchain.languageVersion.set(JavaLanguageVersion.of(8)) } mavenPublishConfig { artifactId = "sizeunit" version = "0.0.1" } - Android库模块中应用插件:
plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("maven-publish-plugin") } android { // ... Android基础配置 ... publishing { singleVariant("release") { withSourcesJar()
相关产品推荐
相关产品推荐

