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
50 changes: 48 additions & 2 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,64 @@ jobs:
run: ./gradlew assembleDebug --no-daemon
working-directory: ./

# 配置签名(仅在发布时)
- name: 配置Release签名
if: startsWith(github.ref, 'refs/tags/')
run: |
# 验证所有必需的签名凭据是否存在
if [ -z "${{ secrets.KEYSTORE_BASE64 }}" ] || \
[ -z "${{ secrets.KEYSTORE_PASSWORD }}" ] || \
[ -z "${{ secrets.KEY_ALIAS }}" ] || \
[ -z "${{ secrets.KEY_PASSWORD }}" ]; then
echo "Error: Missing required signing secrets"
echo "Please configure: KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD"
exit 1
fi

# 创建临时目录用于存放签名文件
mkdir -p /tmp/signing
chmod 700 /tmp/signing

# 从GitHub Secrets解码keystore文件到临时目录
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > /tmp/signing/release.keystore
chmod 600 /tmp/signing/release.keystore

# 验证keystore文件是否成功创建
if [ ! -f /tmp/signing/release.keystore ] || [ ! -s /tmp/signing/release.keystore ]; then
echo "Error: Failed to create keystore file or file is empty"
exit 1
fi

echo "Keystore file created successfully in secure temporary directory"

- name: Build Release APK
if: startsWith(github.ref, 'refs/tags/')
run: ./gradlew assembleRelease --no-daemon
working-directory: ./
env:
KEYSTORE_FILE: /tmp/signing/release.keystore
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

- name: 清理签名密钥文件
if: always() && startsWith(github.ref, 'refs/tags/')
run: |
# 删除临时的keystore文件和目录以防止泄露
rm -rf /tmp/signing
echo "Keystore file and temporary directory cleaned up"

- name: 重命名APK文件
run: |
Comment on lines +132 to 134
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

While the fallback to handle unsigned APKs is useful, the comment "如果签名失败,仍然复制未签名版本" might be misleading. If signing fails, the build step should fail rather than silently producing an unsigned APK.

Consider either:

  1. Removing the fallback and letting the build fail if signing fails
  2. Adding logging to indicate when the fallback is used:
elif [ -f app/build/outputs/apk/release/app-release-unsigned.apk ]; then
  echo "Warning: Signed APK not found, using unsigned version"
  cp app/build/outputs/apk/release/app-release-unsigned.apk app/build/outputs/apk/release/SCE-API-${VERSION}-release.apk
fi

This makes it clearer when something unexpected happens.

Copilot uses AI. Check for mistakes.
VERSION=${{ steps.version.outputs.version }}
if [ -f app/build/outputs/apk/debug/app-debug.apk ]; then
cp app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/debug/SCE-API-${VERSION}-debug.apk
fi
if [ -f app/build/outputs/apk/release/app-release-unsigned.apk ]; then
# Release APK现在是已签名的
if [ -f app/build/outputs/apk/release/app-release.apk ]; then
cp app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/SCE-API-${VERSION}-release.apk
elif [ -f app/build/outputs/apk/release/app-release-unsigned.apk ]; then
# 如果签名失败,仍然复制未签名版本
cp app/build/outputs/apk/release/app-release-unsigned.apk app/build/outputs/apk/release/SCE-API-${VERSION}-release.apk
fi

Expand Down Expand Up @@ -127,7 +173,7 @@ jobs:

### 下载说明
- **SCE-API-${{ steps.version.outputs.version }}-debug.apk**: 调试版本,包含调试信息
- **SCE-API-${{ steps.version.outputs.version }}-release.apk**: 发布版本(未签名
- **SCE-API-${{ steps.version.outputs.version }}-release.apk**: 发布版本(已签名

### 安装要求
- Android 7.0 (API 24) 或更高版本
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ local.properties

# Local configuration files
local.properties

# Keystore files (签名密钥文件)
*.jks
*.keystore
keystore.properties
release.keystore
Binary file removed .gradle/buildOutputCleanup/buildOutputCleanup.lock
Binary file not shown.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,59 @@ The app automatically grants WebView permission requests for:
- versionCode 必须单调递增
- 建议 versionCode 使用公式:`主版本*10000 + 次版本*100 + 修订号`(例如 1.0.1 -> 10001)

### Release APK 签名配置

项目使用固定签名对 Release APK 进行签名。签名配置通过 GitHub Secrets 管理:

#### 配置 GitHub Secrets

在仓库的 Settings > Secrets and variables > Actions 中添加以下 secrets:

1. **KEYSTORE_BASE64**: 密钥库文件的 base64 编码
2. **KEYSTORE_PASSWORD**: 密钥库密码
3. **KEY_ALIAS**: 密钥别名
4. **KEY_PASSWORD**: 密钥密码

#### 生成密钥库文件

如果还没有密钥库文件,可以使用以下命令生成:

```bash
keytool -genkey -v -keystore release.keystore -alias release \
-keyalg RSA -keysize 2048 -validity 10000
```

#### 将密钥库编码为 base64

```bash
# Linux/macOS
base64 release.keystore | tr -d '\n' > keystore.base64.txt

# Windows (PowerShell)
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Out-File -Encoding ASCII keystore.base64.txt
```

然后将 `keystore.base64.txt` 的内容复制到 GitHub Secrets 的 `KEYSTORE_BASE64` 中。

#### 本地构建签名版本

本地构建需要创建 `keystore.properties` 文件(参考 `keystore.properties.template`):

```properties
storeFile=release.keystore
storePassword=你的密钥库密码
keyAlias=release
keyPassword=你的密钥密码
```

然后运行:

```bash
./gradlew assembleRelease
```

**注意**: `keystore.properties` 和 `*.keystore` 文件已添加到 `.gitignore`,不会被提交到仓库。
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The note mentions files "已添加到 .gitignore" (already added to .gitignore) but this might be clearer if it also referenced the complete documentation available in SIGNING_SETUP.md for detailed setup instructions.

Consider adding a reference:

**注意**: `keystore.properties``*.keystore` 文件已添加到 `.gitignore`,不会被提交到仓库。完整的签名配置说明请参考 [SIGNING_SETUP.md](SIGNING_SETUP.md)
Suggested change
**注意**: `keystore.properties``*.keystore` 文件已添加到 `.gitignore`,不会被提交到仓库。
**注意**: `keystore.properties``*.keystore` 文件已添加到 `.gitignore`,不会被提交到仓库。完整的签名配置说明请参考 [SIGNING_SETUP.md](SIGNING_SETUP.md)

Copilot uses AI. Check for mistakes.

## Building

```bash
Expand Down
158 changes: 158 additions & 0 deletions SIGNING_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# APK 签名配置指南

本文档说明如何为项目配置 APK 签名,以便自动构建已签名的 Release APK。

## 概述

项目使用固定的签名配置对 Release APK 进行签名。签名密钥通过 GitHub Secrets 安全管理,不会暴露在代码仓库中。

## 一、生成签名密钥库

### 1.1 使用 keytool 生成密钥库

在命令行中运行以下命令:

```bash
keytool -genkey -v -keystore release.keystore -alias release \
-keyalg RSA -keysize 2048 -validity 10000
```

**参数说明:**
- `release.keystore`: 密钥库文件名
- `release`: 密钥别名
- `RSA`: 密钥算法
- `2048`: 密钥长度
- `10000`: 有效期(天数,约27年)

### 1.2 填写密钥信息

命令会提示你输入以下信息:

1. **密钥库密码**(keystore password):请使用强密码
2. **密钥密码**(key password):可以与密钥库密码相同
3. **姓名、组织等信息**:根据实际情况填写

**重要提示:**
- 请妥善保管密钥库文件和密码
- 如果丢失密钥库,将无法更新已发布的应用
- 建议将密钥库文件和密码存储在安全的地方(如密码管理器)

## 二、配置 GitHub Secrets

### 2.1 将密钥库编码为 base64

**Linux/macOS:**

```bash
base64 release.keystore | tr -d '\n' > keystore.base64.txt
```

**Windows PowerShell:**

```powershell
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Out-File -Encoding ASCII keystore.base64.txt
```

### 2.2 在 GitHub 中添加 Secrets

1. 打开仓库页面
2. 点击 **Settings** > **Secrets and variables** > **Actions**
3. 点击 **New repository secret** 添加以下 secrets:

| Secret 名称 | 值 | 说明 |
|------------|---|------|
| `KEYSTORE_BASE64` | `keystore.base64.txt` 的内容 | 密钥库文件的 base64 编码 |
| `KEYSTORE_PASSWORD` | 你设置的密钥库密码 | 密钥库密码 |
| `KEY_ALIAS` | `release` | 密钥别名(与生成时使用的一致) |
| `KEY_PASSWORD` | 你设置的密钥密码 | 密钥密码 |
Comment on lines +64 to +67
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The table formatting in the markdown has inconsistent column alignment. The header separator uses |---| but should use proper alignment with at least three dashes per column for better rendering consistency.

Consider using:

| Secret 名称 || 说明 |
|-------------|-----|------|

This ensures the table renders correctly across different Markdown parsers.

Copilot uses AI. Check for mistakes.

### 2.3 验证配置

配置完成后,推送一个新的 tag 来触发自动构建:

```bash
git tag v1.0.1
git push origin v1.0.1
```

检查 GitHub Actions 工作流是否成功运行,并生成已签名的 Release APK。

## 三、本地构建已签名版本

如果需要在本地构建已签名的 Release APK:

### 3.1 创建 keystore.properties 文件

在项目根目录创建 `keystore.properties` 文件(参考 `keystore.properties.template`):

```properties
storeFile=release.keystore
storePassword=你的密钥库密码
keyAlias=release
keyPassword=你的密钥密码
```

### 3.2 将密钥库文件放到项目根目录

将 `release.keystore` 文件复制到项目根目录。

### 3.3 构建 Release APK

```bash
./gradlew assembleRelease
```

构建完成后,已签名的 APK 位于:
```
app/build/outputs/apk/release/app-release.apk
```

**注意:** `keystore.properties` 和 `*.keystore` 文件已添加到 `.gitignore`,不会被提交到仓库。

## 四、安全建议

1. **永远不要将密钥库文件或密码提交到代码仓库**
2. **定期备份密钥库文件**(存储在安全的位置)
3. **使用强密码**(至少16位,包含大小写字母、数字和特殊字符)
4. **限制对 GitHub Secrets 的访问权限**
5. **如果密钥库泄露,立即生成新的密钥库并重新发布应用**

## 五、故障排查

### 5.1 签名失败

如果构建时出现签名错误,检查:

1. GitHub Secrets 是否正确配置
2. `KEYSTORE_BASE64` 是否包含完整的 base64 编码(没有换行符)
3. 密码和别名是否正确
4. 密钥库文件是否有效

### 5.2 验证 APK 签名

使用以下命令验证 APK 是否已正确签名:

```bash
# 查看签名信息
keytool -printcert -jarfile app-release.apk

# 或使用 apksigner
apksigner verify --verbose app-release.apk
Comment on lines +136 to +140
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The keytool -printcert command example is missing a note about where to run it from. Since it references app-release.apk, users should be instructed to either use the full path or navigate to the appropriate directory.

Consider clarifying:

# 查看签名信息(在项目根目录运行)
keytool -printcert -jarfile app/build/outputs/apk/release/app-release.apk
Suggested change
# 查看签名信息
keytool -printcert -jarfile app-release.apk
# 或使用 apksigner
apksigner verify --verbose app-release.apk
# 查看签名信息(在项目根目录运行)
keytool -printcert -jarfile app/build/outputs/apk/release/app-release.apk
# 或使用 apksigner(在项目根目录运行)
apksigner verify --verbose app/build/outputs/apk/release/app-release.apk

Copilot uses AI. Check for mistakes.
```

### 5.3 本地构建时找不到密钥库

确保:
1. `keystore.properties` 文件存在且路径正确
2. `release.keystore` 文件在 `storeFile` 指定的位置
3. 文件权限允许读取

## 六、更新签名配置

如果需要更换签名密钥:

1. 生成新的密钥库文件
2. 更新 GitHub Secrets 中的所有相关值
3. 注意:更换签名后,用户需要卸载旧版本才能安装新版本

**建议:** 除非绝对必要,否则不要更换签名密钥。
40 changes: 40 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,50 @@ android {
versionCode 1
versionName "1.0.0"
}

signingConfigs {
release {
// 从环境变量或 keystore.properties 文件读取签名配置
def keystorePropertiesFile = rootProject.file("keystore.properties")
if (keystorePropertiesFile.exists()) {
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
} else {
// CI环境使用环境变量(如果未设置,签名配置将为null)
def keystoreFile = System.getenv("KEYSTORE_FILE")
def keystorePassword = System.getenv("KEYSTORE_PASSWORD")
def keyAlias = System.getenv("KEY_ALIAS")
def keyPassword = System.getenv("KEY_PASSWORD")
Comment on lines +30 to +33
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Using empty strings as default passwords (?: "") poses a security risk. If the environment variables are not set in CI, the build will proceed with empty passwords, which could result in an unsigned or improperly signed APK without clear error messages.

Instead, consider failing explicitly when required credentials are missing:

storePassword System.getenv("KEYSTORE_PASSWORD") ?: { throw new GradleException("KEYSTORE_PASSWORD not set") }()
keyPassword System.getenv("KEY_PASSWORD") ?: { throw new GradleException("KEY_PASSWORD not set") }()

Or use Gradle's built-in validation to ensure the build fails early if credentials are missing.

Copilot uses AI. Check for mistakes.

// 检查签名配置是否完整
def hasCompleteSigningConfig = [keystoreFile, keystorePassword, keyAlias, keyPassword]
.every { it?.trim() }

// 只有当所有环境变量都存在且非空时才配置签名
if (hasCompleteSigningConfig) {
storeFile file(keystoreFile)
storePassword keystorePassword
keyAlias keyAlias
keyPassword keyPassword
}
// 如果环境变量未设置,签名配置保持为空(允许构建debug)
}
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// 只有当签名配置完整时才应用
if (signingConfigs.release.storeFile != null) {
signingConfig signingConfigs.release
}
}
}
compileOptions {
Expand Down
14 changes: 14 additions & 0 deletions keystore.properties.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Android 签名配置模板
# 复制此文件为 keystore.properties 并填写实际值

# 密钥库文件路径(相对于项目根目录)
storeFile=release.keystore

# 密钥库密码
storePassword=your_keystore_password

# 密钥别名
keyAlias=release

# 密钥密码
keyPassword=your_key_password
Comment on lines +8 to +14
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The template uses placeholder text your_keystore_password and your_key_password, but these might be too generic. Consider using more descriptive placeholders that indicate these should be strong passwords, for example:

storePassword=<your-strong-keystore-password>
keyPassword=<your-strong-key-password>

This makes it clearer that users should replace these with actual secure passwords, not literal strings.

Suggested change
storePassword=your_keystore_password
# 密钥别名
keyAlias=release
# 密钥密码
keyPassword=your_key_password
storePassword=<your-strong-keystore-password>
# 密钥别名
keyAlias=release
# 密钥密码
keyPassword=<your-strong-key-password>

Copilot uses AI. Check for mistakes.