You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从密钥文件/环境加载配置后发布至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 签名错误修复

核心原因

  1. 签名依赖的敏感参数(signingKeyId、密钥文件内容、signingPassword)可能为空,导致useInMemoryPgpKeys无法初始化有效签名者。
  2. 签名配置的时机与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=yoursontypepassword
    
    脚本中直接读取属性:
    val 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的自定义插件中,实现跨模块复用:

  1. 在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
    }
    
  2. 在根项目build.gradle.kts中配置buildSrc:
    plugins {
        `kotlin-dsl`
    }
    
    repositories {
        mavenCentral()
    }
    
  3. 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"
    }
    
  4. Android库模块中应用插件:
    plugins {
        id("com.android.library")
        id("org.jetbrains.kotlin.android")
        id("maven-publish-plugin")
    }
    
    android {
        // ... Android基础配置 ...
        publishing {
            singleVariant("release") {
                withSourcesJar()
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 14:17:52