Skip to content

Commit 762cba3

Browse files
Add signed Android release workflow
1 parent 27903c2 commit 762cba3

6 files changed

Lines changed: 221 additions & 2 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
name: Android Release APK
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
name: Build signed release APK
14+
runs-on: ubuntu-latest
15+
16+
defaults:
17+
run:
18+
shell: bash
19+
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
24+
- name: Set up JDK
25+
uses: actions/setup-java@v4
26+
with:
27+
distribution: temurin
28+
java-version: '21'
29+
30+
- name: Set up Android SDK
31+
uses: android-actions/setup-android@v3
32+
33+
- name: Install Android SDK packages
34+
run: |
35+
set +o pipefail
36+
yes | sdkmanager --licenses > /dev/null
37+
set -o pipefail
38+
sdkmanager "platforms;android-36" "build-tools;36.0.0"
39+
40+
- name: Set up Gradle
41+
uses: gradle/actions/setup-gradle@v4
42+
with:
43+
cache-read-only: false
44+
45+
- name: Decode release keystore
46+
env:
47+
KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }}
48+
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}
49+
KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }}
50+
KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }}
51+
run: |
52+
if [[ -z "$KEYSTORE_BASE64" || -z "$KEYSTORE_PASSWORD" || -z "$KEY_ALIAS" || -z "$KEY_PASSWORD" ]]; then
53+
echo "::error::Missing Android release signing secrets."
54+
exit 1
55+
fi
56+
57+
mkdir -p "$RUNNER_TEMP/android-signing"
58+
printf '%s' "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/android-signing/release.jks"
59+
60+
{
61+
echo "ANDROID_RELEASE_KEYSTORE=$RUNNER_TEMP/android-signing/release.jks"
62+
echo "ANDROID_RELEASE_KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD"
63+
echo "ANDROID_RELEASE_KEY_ALIAS=$KEY_ALIAS"
64+
echo "ANDROID_RELEASE_KEY_PASSWORD=$KEY_PASSWORD"
65+
} >> "$GITHUB_ENV"
66+
67+
- name: Make Gradle wrapper executable
68+
working-directory: android
69+
run: chmod +x gradlew
70+
71+
- name: Extract release notes
72+
run: |
73+
python3 - <<'PY'
74+
from pathlib import Path
75+
import os
76+
import re
77+
import sys
78+
79+
tag = os.environ["GITHUB_REF_NAME"]
80+
version = tag.removeprefix("v")
81+
changelog = Path("CHANGELOG.md")
82+
if not changelog.exists():
83+
print("::error::CHANGELOG.md is missing.")
84+
sys.exit(1)
85+
86+
lines = changelog.read_text(encoding="utf-8").splitlines()
87+
heading = re.compile(rf"^## \[{re.escape(version)}\](?:\s+-\s+.*)?\s*$")
88+
next_heading = re.compile(r"^## \[")
89+
start = None
90+
for index, line in enumerate(lines):
91+
if heading.match(line):
92+
start = index + 1
93+
break
94+
95+
if start is None:
96+
print(f"::error::CHANGELOG.md has no section for version {version}.")
97+
sys.exit(1)
98+
99+
end = len(lines)
100+
for index in range(start, len(lines)):
101+
if next_heading.match(lines[index]):
102+
end = index
103+
break
104+
105+
notes = "\n".join(lines[start:end]).strip()
106+
if not notes:
107+
print(f"::error::CHANGELOG.md section for version {version} is empty.")
108+
sys.exit(1)
109+
110+
output = Path(os.environ["RUNNER_TEMP"]) / "release-notes.md"
111+
output.write_text(notes + "\n", encoding="utf-8")
112+
print(f"Release notes written to {output}")
113+
PY
114+
115+
- name: Build signed release APK
116+
working-directory: android
117+
run: |
118+
version_name="${GITHUB_REF_NAME#v}"
119+
./gradlew --no-daemon test assembleRelease \
120+
-PversionName="$version_name" \
121+
-PversionCode="${GITHUB_RUN_NUMBER}"
122+
123+
- name: Prepare APK artifact
124+
run: |
125+
tag="${GITHUB_REF_NAME}"
126+
apk_name="clhs-socre-${tag}-arm64-v8a-release.apk"
127+
apk_path="$RUNNER_TEMP/$apk_name"
128+
cp android/app/build/outputs/apk/release/app-release.apk "$apk_path"
129+
echo "APK_PATH=$apk_path" >> "$GITHUB_ENV"
130+
echo "APK_NAME=$apk_name" >> "$GITHUB_ENV"
131+
132+
- name: Verify APK signature and ABI
133+
run: |
134+
"$ANDROID_HOME/build-tools/36.0.0/apksigner" verify --verbose --print-certs "$APK_PATH"
135+
python3 - <<'PY'
136+
from zipfile import ZipFile
137+
import os
138+
import sys
139+
140+
apk_path = os.environ["APK_PATH"]
141+
allowed_abis = {"arm64-v8a"}
142+
with ZipFile(apk_path) as apk:
143+
abis = {
144+
name.split("/")[1]
145+
for name in apk.namelist()
146+
if name.startswith("lib/") and len(name.split("/")) > 2
147+
}
148+
149+
invalid_abis = abis - allowed_abis
150+
if invalid_abis:
151+
print(f"::error::APK contains unsupported native ABIs: {sorted(invalid_abis)}")
152+
sys.exit(1)
153+
154+
print(f"Native ABIs in APK: {sorted(abis) or 'none'}")
155+
PY
156+
157+
- name: Create GitHub Release
158+
env:
159+
GH_TOKEN: ${{ github.token }}
160+
run: |
161+
if gh release view "$GITHUB_REF_NAME" > /dev/null 2>&1; then
162+
gh release edit "$GITHUB_REF_NAME" \
163+
--title "CLHS Socre $GITHUB_REF_NAME" \
164+
--notes-file "$RUNNER_TEMP/release-notes.md"
165+
gh release upload "$GITHUB_REF_NAME" "$APK_PATH" --clobber
166+
else
167+
gh release create "$GITHUB_REF_NAME" "$APK_PATH" \
168+
--verify-tag \
169+
--title "CLHS Socre $GITHUB_REF_NAME" \
170+
--notes-file "$RUNNER_TEMP/release-notes.md"
171+
fi

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ venv.bak/
3333

3434
# Environment Variables (CRITICAL for security)
3535
.env
36+
*.jks
37+
*.keystore
38+
*.p12
3639

3740
# Application Data & Temporary Files
3841
grades_raw.json
@@ -47,6 +50,7 @@ shared_grades/
4750
*.md
4851
!README.md
4952
!AGENTS.md
53+
!CHANGELOG.md
5054
0_build_and_deploy.txt
5155
# Exclude specific JSONs but allow config templates if any
5256

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@
2525
- Android:在 `android/` 內執行 `.\gradlew.bat test`
2626

2727
在 Windows Codex 環境跑 Android Gradle 時,若 `java` 不在 PATH,使用 Android Studio 內建 JBR,並將 `GRADLE_USER_HOME``ANDROID_USER_HOME` 指到 workspace 內的暫存目錄。若測試一開始就出現 `could not open ...\jbr\lib\jvm.cfg`,通常是設定的 Android Studio JBR 路徑不存在或不完整;先用 `Test-Path` 或列出 `C:\Program Files\Android\Android Studio*` 確認實際 JBR 位置。本機曾遇到 `C:\Program Files\Android\Android Studio\jbr` 不可用,而 `C:\Program Files\Android\Android Studio1\jbr` 可用。
28+
29+
## Android release
30+
31+
- 推送 `v*` tag 會觸發 `.github/workflows/android-release.yml`,建立 signed `arm64-v8a` release APK 並發布 GitHub Release。
32+
- Release notes 來自 `CHANGELOG.md` 內與 tag 對應的 `## [x.y.z]` 區塊;新增版本時要先補 changelog。
33+
- GitHub Secrets 需設定 `ANDROID_RELEASE_KEYSTORE_BASE64``ANDROID_RELEASE_KEYSTORE_PASSWORD``ANDROID_RELEASE_KEY_ALIAS``ANDROID_RELEASE_KEY_PASSWORD`。不要提交 keystore 或密碼。

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Changelog
2+
3+
## [1.0.0] - 2026-05-20
4+
5+
## 更新內容
6+
- 優化卡片展開動畫
7+
- 改善成績模擬器操作體驗

android/app/build.gradle.kts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
22

3+
val releaseVersionCode = providers.gradleProperty("versionCode")
4+
.map { it.toInt() }
5+
.orElse(1)
6+
val releaseVersionName = providers.gradleProperty("versionName")
7+
.orElse("1.0")
8+
9+
val releaseKeystore = providers.environmentVariable("ANDROID_RELEASE_KEYSTORE")
10+
val releaseKeystorePassword = providers.environmentVariable("ANDROID_RELEASE_KEYSTORE_PASSWORD")
11+
val releaseKeyAlias = providers.environmentVariable("ANDROID_RELEASE_KEY_ALIAS")
12+
val releaseKeyPassword = providers.environmentVariable("ANDROID_RELEASE_KEY_PASSWORD")
13+
val hasReleaseSigning = listOf(
14+
releaseKeystore,
15+
releaseKeystorePassword,
16+
releaseKeyAlias,
17+
releaseKeyPassword,
18+
).all { it.isPresent }
19+
320
plugins {
421
id("com.android.application")
522
id("org.jetbrains.kotlin.plugin.compose")
@@ -13,8 +30,8 @@ android {
1330
applicationId = "com.clhs.score"
1431
minSdk = 29
1532
targetSdk = 36
16-
versionCode = 1
17-
versionName = "1.0"
33+
versionCode = releaseVersionCode.get()
34+
versionName = releaseVersionName.get()
1835

1936
ndk {
2037
abiFilters += "arm64-v8a"
@@ -39,10 +56,24 @@ android {
3956
}
4057
}
4158

59+
signingConfigs {
60+
create("release") {
61+
if (hasReleaseSigning) {
62+
storeFile = file(releaseKeystore.get())
63+
storePassword = releaseKeystorePassword.get()
64+
keyAlias = releaseKeyAlias.get()
65+
keyPassword = releaseKeyPassword.get()
66+
}
67+
}
68+
}
69+
4270
buildTypes {
4371
release {
4472
isMinifyEnabled = true
4573
isShrinkResources = true
74+
if (hasReleaseSigning) {
75+
signingConfig = signingConfigs.getByName("release")
76+
}
4677
proguardFiles(
4778
getDefaultProguardFile("proguard-android-optimize.txt"),
4879
"proguard-rules.pro",

android/gradlew

100644100755
File mode changed.

0 commit comments

Comments
 (0)