1.使用 GitLab CI/CD 的 Secret 管理机制 + 动态生成签名文件
1.1 将签名信息存储在 GitLab CI/CD 的变量中(Secret Variables)
在 GitLab 项目或 Group 的 Settings → CI/CD → Variables 中添加以下内容:
KEYSTORE_BASE64:将 .keystore 文件转成 base64 字符串后存储
KEY_ALIAS
KEY_PASSWORD
STORE_PASSWORD
⚠️:这些变量需要设置为 “Protected” 且 “Masked”,确保不会被打印在日志中。
你可以使用命令行工具将 .keystore 文件转换成 base64 字符串,非常简单。以下是在 Windows、macOS、Linux 下的操作方法。
macOS / Linux / Git Bash 处理方式:
base64 -w 0 release.keystore > keystore.base64.txt
-w 0 表示 不要换行
有些系统用的是 --wrap=0 或 -b 0:
base64 --wrap=0 release.keystore > keystore.base64.txt
1.2 在 CI 中恢复 keystore 文件
在 .gitlab-ci.yml 的构建步骤中,先将 keystore 文件从 base64 解码出来:
- echo $KEYSTORE_BASE64 | base64 -d > release.keystore
或(macOS/Linux runner 有时需要加 -n 处理换行):
- echo -n "$KEYSTORE_BASE64" | base64 -d > release.keystore
然后在 gradle.properties 或 build.gradle 中引用这些环境变量。
android {
signingConfigs {
release {
storeFile file("${rootDir}/release.keystore")
storePassword System.getenv("STORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
避免将 release.keystore 保留到后续阶段中,打完包后立刻清除:
after_script:
- rm -f release.keystore
1.3 如果项目根目录已经有 release.keystore 文件,就跳过 base64 解码;否则再生成:
script:
- |
if [ ! -f "release.keystore" ]; then
echo "release.keystore not found, generating from base64..."
echo -n "$KEYSTORE_BASE64" | base64 -d > release.keystore
else
echo "release.keystore already exists, skip generation."
fi
- ./gradlew assembleRelease
2 ✅ 正确防止 keystore 被打包进 APK 的方法
1. 不要把 release.keystore 放在 app 模块的 src/ 子目录或 assets/ 目录中。
建议放在项目根目录或 ci_keys/ 目录下,绝不放在如下路径:
app/src/main/assets/
app/src/main/res/
app/libs/
否则会被误打入 APK。
2. 配置 Gradle 排除非必要文件(保险起见)
在 app/build.gradle 中添加打包排除逻辑:
android {
packagingOptions {
exclude "release.keystore"
exclude "**/release.keystore"
}
}
或者更强硬一点:
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputDir = output.outputFile.parentFile
def keystoreFile = file("${project.rootDir}/release.keystore")
if (keystoreFile.exists()) {
println "Deleting keystore before packaging: ${keystoreFile}"
keystoreFile.delete()
}
}
}
这个做法是在打包之前删除 keystore,可防止漏掉清理操作。
3. CI 中删除 keystore 的时机要合理
在打包完成后,立即删除:
script:
- |
if [ ! -f "release.keystore" ]; then
echo -n "$KEYSTORE_BASE64" | base64 -d > release.keystore
fi
- ./gradlew assembleRelease
- rm -f release.keystore # ✅ 及时删除
问题 1:将 keystore 放在根目录是否一定不会被打包进 APK??
✅ 答案:确定。
前提是你没有人为操作或配置将根目录的文件 copy 进 APK 的资源目录(比如 assets/ 或 res/)。
原因:
Gradle 打包 APK 时,默认只会打包以下内容到 APK 中:
文件夹路径 是否打包进 APK 内容说明
src/main/res/ ✅ 是 编译成 resources.arsc
src/main/assets/ ✅ 是 打包为原始文件到 APK
libs/ ✅ 是 (.jar) 打进 classes.dex 或 classpath
src/main/java/ ✅ 是 编译为 class 并打包进 dex
项目根目录、build/ ❌ 否 不会自动打进 APK
🔒 所以只要你没主动将根目录的文件拷贝到打包路径(比如 assets/),它就绝对不会被打包进 APK 中。
3.推荐做法
3.1 keystore 位置
将 keystore 放置在项目根目录,或者通过 gitlab CI 变量配置下发,然后重新生成keystore文件。
从安全性来说是放在 gitlab 中最好。但是,将keystore放置在项目根目录更方便。看公司对于签名信息的安全的重视程度。
3.2 签名信息
1.将签名信息配置在local.properties中:
RELEASE_STORE_PASSWORD=你的keystore密码
RELEASE_KEY_ALIAS=你的别名
KEYSTORE_PASSWORD=你的key密码
2.在 build.gradle.kts 中读取 local.properties 的方法
local.properties 不是 Gradle 默认加载的配置文件,所以你需要手动加载它:
import java.util.Properties
import java.io.FileInputStream
// 定义一个变量读取 local.properties
val localProperties = Properties().apply {
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
load(FileInputStream(localPropertiesFile))
}
}
android {
signingConfigs {
getByName("debug") {
storeFile = file("${rootDir}/xxx.jks")
storePassword = localProperties.getProperty("RELEASE_STORE_PASSWORD") ?: System.getenv("KEYSTORE_PASSWORD")
keyAlias = localProperties.getProperty("RELEASE_KEY_ALIAS")?: System.getenv("KEYSTORE_ALIAS")
keyPassword = localProperties.getProperty("KEYSTORE_PASSWORD") ?: System.getenv("KEYSTORE_PASSWORD")
}
create("release") {
storeFile = file("${rootDir}/xxx.jks")
storePassword = localProperties.getProperty("RELEASE_STORE_PASSWORD") ?: System.getenv("KEYSTORE_PASSWORD")
keyAlias = localProperties.getProperty("RELEASE_KEY_ALIAS")?: System.getenv("KEYSTORE_ALIAS")
keyPassword = localProperties.getProperty("KEYSTORE_PASSWORD") ?: System.getenv("KEYSTORE_PASSWORD")
}
}
buildTypes {
getByName("debug") {
signingConfig = signingConfigs.getByName("debug")
}
getByName("release") {
signingConfig = signingConfigs.getByName("release")
}
}
}
3. 注意事项
local.properties 不要提交到代码仓库,在 .gitignore 中确保忽略它。
如果文件不存在或缺少字段,以上代码也不会报错,会得到 null,你可以做 null 判断或设置默认值。
CI/CD 里不要用 local.properties,应使用环境变量或 CI 的 Secret Variables。