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
115 changes: 115 additions & 0 deletions .github/workflows/android-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Android Release

on:
workflow_dispatch:
inputs:
tag_name:
description: Git tag and GitHub Release name, for example v2.0.0-pro.1
required: true
type: string
version_name:
description: Android versionName
required: true
default: 2.0.0-PRO
type: string
version_code:
description: Android versionCode
required: true
default: "2"
type: string
prerelease:
description: Mark the GitHub Release as a prerelease
required: true
default: true
type: boolean

jobs:
release:
name: Build Signed Release
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: "17"
distribution: temurin

- name: Cache Gradle packages
uses: actions/cache@v5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
~/.android/build-cache
key: gradle-android-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
restore-keys: |
gradle-android-${{ runner.os }}-

- name: Validate release secrets
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
missing=0
for name in ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do
if [ -z "${!name}" ]; then
echo "::error::$name is required for Android release signing"
missing=1
fi
done
exit "$missing"

- name: Decode signing keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/release-keystore.jks"

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build release artifacts
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
./gradlew lintDebug testDebugUnitTest assembleRelease bundleRelease \
--no-daemon \
--max-workers=1 \
-Dorg.gradle.jvmargs=-Xmx768m \
-PGDEI_RELEASE_STORE_FILE="$RUNNER_TEMP/release-keystore.jks" \
-PGDEI_RELEASE_STORE_PASSWORD="$ANDROID_KEYSTORE_PASSWORD" \
-PGDEI_RELEASE_KEY_ALIAS="$ANDROID_KEY_ALIAS" \
-PGDEI_RELEASE_KEY_PASSWORD="$ANDROID_KEY_PASSWORD" \
-PGDEI_VERSION_NAME="${{ inputs.version_name }}" \
-PGDEI_VERSION_CODE="${{ inputs.version_code }}"
Comment on lines +93 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass workflow inputs through env before shell use

Because these workflow_dispatch inputs are interpolated directly into the shell script while the signing secrets are present in the environment, a dispatcher can supply a value containing a double quote and shell metacharacters (for example in version_name) to break out of the quoted -P... argument and run arbitrary commands that can read the keystore passwords or use the write-scoped GITHUB_TOKEN. Put the inputs into step env values and reference those shell variables instead of embedding ${{ inputs.* }} in run.

Useful? React with 👍 / 👎.


- name: Upload release artifacts
uses: actions/upload-artifact@v7
with:
name: android-release-${{ inputs.tag_name }}
path: |
app/build/outputs/apk/release/*.apk
app/build/outputs/bundle/release/*.aab
if-no-files-found: error
retention-days: 30

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.tag_name }}
name: ${{ inputs.tag_name }}
prerelease: ${{ inputs.prerelease }}
generate_release_notes: true
files: |
app/build/outputs/apk/release/*.apk
app/build/outputs/bundle/release/*.aab
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,38 @@ app/build/outputs/apk/debug/app-debug.apk
./gradlew :app:installDebug
```

### 5. 运行方式
### 5. 构建 Release 安装包

仓库提供 `Android Release` GitHub Actions 手动工作流,用于构建签名 APK 和
AAB,并发布到 GitHub Release。发布前需要在仓库或环境中配置:

- `ANDROID_KEYSTORE_BASE64`:release keystore 文件的 base64 内容
- `ANDROID_KEYSTORE_PASSWORD`:keystore 密码
- `ANDROID_KEY_ALIAS`:release key alias
- `ANDROID_KEY_PASSWORD`:release key 密码

工作流输入包括 `tag_name`、`version_name`、`version_code` 和是否标记为
prerelease。工作流会先运行 lint 与单元测试,再执行 `assembleRelease` 和
`bundleRelease`。

本地签名构建可以使用:

```bash
./gradlew :app:assembleRelease \
-PGDEI_RELEASE_STORE_FILE=/path/to/release-keystore.jks \
-PGDEI_RELEASE_STORE_PASSWORD="$ANDROID_KEYSTORE_PASSWORD" \
-PGDEI_RELEASE_KEY_ALIAS="$ANDROID_KEY_ALIAS" \
-PGDEI_RELEASE_KEY_PASSWORD="$ANDROID_KEY_PASSWORD" \
-PGDEI_VERSION_NAME=2.0.0-PRO \
-PGDEI_VERSION_CODE=2
```

### 6. 运行方式

- `mock` 模式:适合本地开发、UI 联调、回归验证
- `remote` 模式:适合连接真实后端接口进行联调

### 6. 远程接口环境
### 7. 远程接口环境

应用内远程接口默认按 `dev / staging / prod` 三套环境运行:

Expand Down
39 changes: 37 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ val stagingBaseUrl = providers.gradleProperty("GDEI_BASE_URL_STAGING").orNull
val prodBaseUrl = providers.gradleProperty("GDEI_BASE_URL_PROD").orNull
?: "https://gdeiassistant.cn/"
val certificatePins = providers.gradleProperty("GDEI_CERTIFICATE_PINS").orNull.orEmpty()
val appVersionCode = providers.gradleProperty("GDEI_VERSION_CODE")
.map { it.toInt() }
.orElse(2)
.get()
val appVersionName = providers.gradleProperty("GDEI_VERSION_NAME")
.orElse("2.0.0-PRO")
.get()
val releaseStoreFile = providers.gradleProperty("GDEI_RELEASE_STORE_FILE").orNull
?: System.getenv("ANDROID_KEYSTORE_FILE")
val releaseStorePassword = providers.gradleProperty("GDEI_RELEASE_STORE_PASSWORD").orNull
?: System.getenv("ANDROID_KEYSTORE_PASSWORD")
val releaseKeyAlias = providers.gradleProperty("GDEI_RELEASE_KEY_ALIAS").orNull
?: System.getenv("ANDROID_KEY_ALIAS")
val releaseKeyPassword = providers.gradleProperty("GDEI_RELEASE_KEY_PASSWORD").orNull
?: System.getenv("ANDROID_KEY_PASSWORD")
val releaseSigningEnabled = listOf(
releaseStoreFile,
releaseStorePassword,
releaseKeyAlias,
releaseKeyPassword
).all { !it.isNullOrBlank() }
val requireCertificatePins = providers.gradleProperty("GDEI_REQUIRE_CERTIFICATE_PINS")
.map { it.toBoolean() }
.orElse(false)
Expand Down Expand Up @@ -75,8 +96,8 @@ android {
applicationId = "cn.gdeiassistant"
minSdk = 26
targetSdk = 35
versionCode = 2
versionName = "2.0.0-PRO"
versionCode = appVersionCode
versionName = appVersionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BASE_URL", "\"$prodBaseUrl\"")
buildConfigField("String", "BASE_URL_DEV", "\"$devBaseUrl\"")
Expand All @@ -86,9 +107,23 @@ android {
buildConfigField("String", "CERTIFICATE_PINS", certificatePins.asBuildConfigString())
}

signingConfigs {
create("release") {
if (releaseSigningEnabled) {
storeFile = file(releaseStoreFile!!)
storePassword = releaseStorePassword
keyAlias = releaseKeyAlias
keyPassword = releaseKeyPassword
}
}
}

buildTypes {
release {
isMinifyEnabled = true
if (releaseSigningEnabled) {
signingConfig = signingConfigs.getByName("release")
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
Expand Down