Skip to content

Add APK signing configuration to CI/CD workflow#4

Merged
xingguangcuican6666 merged 8 commits intomasterfrom
copilot/update-workflow-fixed-signature
Dec 9, 2025
Merged

Add APK signing configuration to CI/CD workflow#4
xingguangcuican6666 merged 8 commits intomasterfrom
copilot/update-workflow-fixed-signature

Conversation

Copy link

Copilot AI commented Dec 9, 2025

Implements fixed APK signing for Release builds via GitHub Secrets. Currently Release APKs are unsigned.

Changes

Build Configuration (app/build.gradle)

  • Added signingConfigs.release block supporting both local (keystore.properties) and CI (environment variables) credential sources
  • Signing applied only when credentials are complete; debug builds work without credentials

Workflow (.github/workflows/android.yml)

  • Validates signing secrets presence before build
  • Decodes keystore from KEYSTORE_BASE64 secret to /tmp/signing/ with chmod 600
  • Passes credentials to Gradle via environment variables
  • Cleanup step removes keystore post-build using always() condition

Security

  • Added keystore patterns to .gitignore: *.jks, *.keystore, keystore.properties
  • Keystore stored in restricted temporary directory, not workspace root
  • Credentials passed directly to build step, not persisted in GITHUB_ENV

Documentation

  • Created SIGNING_SETUP.md with keytool commands and GitHub Secrets setup
  • Added keystore.properties.template for local builds
  • Updated README.md with signing configuration section

Required GitHub Secrets

Add to repository settings:

  • KEYSTORE_BASE64 - base64-encoded keystore file
  • KEYSTORE_PASSWORD - keystore password
  • KEY_ALIAS - key alias (typically release)
  • KEY_PASSWORD - key password

Example: Generate and Encode Keystore

# Generate
keytool -genkey -v -keystore release.keystore -alias release \
  -keyalg RSA -keysize 2048 -validity 10000

# Encode for GitHub Secret
base64 release.keystore | tr -d '\n' > keystore.base64.txt

For local builds, copy keystore.properties.template to keystore.properties and populate with actual credentials.

Original prompt

修改工作流,使用固定签名


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits December 9, 2025 08:33
Co-authored-by: xingguangcuican6666 <119158495+xingguangcuican6666@users.noreply.github.com>
Co-authored-by: xingguangcuican6666 <119158495+xingguangcuican6666@users.noreply.github.com>
@xingguangcuican6666 xingguangcuican6666 marked this pull request as ready for review December 9, 2025 08:41
Copilot AI review requested due to automatic review settings December 9, 2025 08:41
Copilot AI and others added 4 commits December 9, 2025 08:42
Co-authored-by: xingguangcuican6666 <119158495+xingguangcuican6666@users.noreply.github.com>
Co-authored-by: xingguangcuican6666 <119158495+xingguangcuican6666@users.noreply.github.com>
Co-authored-by: xingguangcuican6666 <119158495+xingguangcuican6666@users.noreply.github.com>
Co-authored-by: xingguangcuican6666 <119158495+xingguangcuican6666@users.noreply.github.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements APK signing configuration for automated release builds using fixed signing credentials managed through GitHub Secrets. The implementation provides a dual-configuration approach supporting both local development (via keystore.properties file) and CI environments (via environment variables), ensuring release APKs are properly signed while keeping credentials secure.

Key Changes

  • Signing Configuration: Added Gradle signing configuration in app/build.gradle that reads from either local keystore.properties or environment variables
  • Workflow Integration: Modified GitHub Actions workflow to decode and use keystore from secrets for release builds
  • Documentation: Created comprehensive signing setup guide (SIGNING_SETUP.md) and updated README with signing instructions

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
app/build.gradle Added signingConfigs.release block with dual-source configuration (local file vs environment variables) and applied it to release build type
.github/workflows/android.yml Added signing configuration step to decode keystore from secrets and updated APK handling to work with signed releases
.gitignore Added patterns to exclude keystore files (*.jks, *.keystore) and keystore.properties from version control
keystore.properties.template Created template file showing required properties for local signing configuration
SIGNING_SETUP.md Added comprehensive documentation covering keystore generation, GitHub Secrets configuration, local builds, and troubleshooting
README.md Added signing configuration section with quick setup instructions and reference to detailed guide
.gradle/buildOutputCleanup/buildOutputCleanup.lock Build artifact incorrectly included in commit - should be removed

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +30 to +33
def keystoreFile = System.getenv("KEYSTORE_FILE")
def keystorePassword = System.getenv("KEYSTORE_PASSWORD")
def keyAlias = System.getenv("KEY_ALIAS")
def keyPassword = System.getenv("KEY_PASSWORD")
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.
if: startsWith(github.ref, 'refs/tags/')
run: |
# 从GitHub Secrets解码keystore文件
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > release.keystore
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 keystore file is written to the workspace and may not be cleaned up if subsequent steps fail. This could potentially expose the keystore in workflow artifacts or logs.

Consider adding a cleanup step that always runs, even on failure:

- name: 清理密钥文件
  if: always() && startsWith(github.ref, 'refs/tags/')
  run: |
    rm -f release.keystore

Place this step after all build steps to ensure the keystore is removed from the runner.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +97
if [ ! -f release.keystore ] || [ ! -s release.keystore ]; then
echo "Error: Failed to create keystore file or file is empty"
exit 1
fi
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.

Setting secrets as environment variables using $GITHUB_ENV makes them available to all subsequent steps, which may not be necessary. This increases the risk of accidental exposure in logs or to unintended steps.

A better practice is to pass secrets only to the steps that need them via the env block:

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

Remove lines 94-97 and pass the environment variables directly to the build step as shown.

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +67
| `KEYSTORE_BASE64` | `keystore.base64.txt` 的内容 | 密钥库文件的 base64 编码 |
| `KEYSTORE_PASSWORD` | 你设置的密钥库密码 | 密钥库密码 |
| `KEY_ALIAS` | `release` | 密钥别名(与生成时使用的一致) |
| `KEY_PASSWORD` | 你设置的密钥密码 | 密钥密码 |
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.
Comment on lines +136 to +140
# 查看签名信息
keytool -printcert -jarfile app-release.apk

# 或使用 apksigner
apksigner verify --verbose app-release.apk
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.
Comment on lines +8 to +14
storePassword=your_keystore_password

# 密钥别名
keyAlias=release

# 密钥密码
keyPassword=your_key_password
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.
Comment on lines +16 to +35
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")

// 只有当所有环境变量都存在且非空时才配置签名
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 signing configuration lacks proper error handling. If the keystore file path is invalid or the properties file is malformed, the build will fail with cryptic errors from Gradle/Java rather than clear messages about what went wrong.

Consider adding validation and helpful error messages:

signingConfigs {
    release {
        def keystorePropertiesFile = rootProject.file("keystore.properties")
        if (keystorePropertiesFile.exists()) {
            def keystoreProperties = new Properties()
            keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
            
            def requiredProps = ['storeFile', 'storePassword', 'keyAlias', 'keyPassword']
            requiredProps.each { prop ->
                if (!keystoreProperties[prop]) {
                    throw new GradleException("Missing required property '$prop' in keystore.properties")
                }
            }
            
            def keystoreFile = file(keystoreProperties['storeFile'])
            if (!keystoreFile.exists()) {
                throw new GradleException("Keystore file not found: ${keystoreFile.absolutePath}")
            }
            
            storeFile keystoreFile
            storePassword keystoreProperties['storePassword']
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
        } else {
            // CI environment validation
            def keystoreFile = System.getenv("KEYSTORE_FILE")
            if (!keystoreFile) {
                throw new GradleException("KEYSTORE_FILE environment variable not set")
            }
            // ... similar validation for other properties
        }
    }
}

Copilot uses AI. Check for mistakes.
./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.
Comment on lines +118 to 120

- name: 重命名APK文件
run: |
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.
Co-authored-by: xingguangcuican6666 <119158495+xingguangcuican6666@users.noreply.github.com>
Copilot AI changed the title [WIP] Update workflow to use fixed signature Add APK signing configuration to CI/CD workflow Dec 9, 2025
@xingguangcuican6666 xingguangcuican6666 merged commit 07aaab9 into master Dec 9, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants