Repository navigation
232 lines (213 loc) · 11.3 KB
/
Copy pathandroid-release.yml
File metadata and controls
232 lines (213 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
name: Android Release
# 打 Tag(如 v1.0.0)时触发:构建三个签名 Release APK
# - universal : arm64-v8a + x86_64,含 arm + x86 两套容器镜像(通用,体积最大)
# - armsolo : 仅 arm64-v8a,仅含 arm 镜像(真机首选,体积约为 universal 的一半)
# - x86solo : 仅 x86_64,仅含 x86 镜像(x86 模拟器/Chromebook,体积约为 universal 的一半)
# 创建 GitHub Release 并把三个 APK 作为资产发布。
on:
push:
tags:
- 'v*'
# 允许在 Actions 页面手动触发,便于验证构建链路。
workflow_dispatch:
jobs:
build:
name: Build & Release
runs-on: ubuntu-latest
permissions:
contents: write # 创建 Release 与上传资产需要写权限
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# versionCode 由 build.gradle.kts 的 gitCommitCount() 取 git rev-list --count HEAD,
# 浅克隆(默认 depth=1)会得到 1,必须拉完整历史,否则 Release APK 版本号错乱。
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
# 复用 Gradle 构建缓存,加速后续构建。
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
# 打印当前 Tag,Tag 驱动直接生成 versionName。
# 仅打 tag 触发时校验;workflow_dispatch(手动,无 tag)跳过。
- name: Display release tag info
if: startsWith(github.ref, 'refs/tags/v')
run: |
set -e
tag_ver="${GITHUB_REF_NAME#v}" # 1.7.0 / 1.7.0-rc1 / ...
echo "tag version (without v) = $tag_ver"
echo "✓ tag 将直接驱动应用 versionName ($tag_ver)"
# 校验 versionCode 单调递增:versionCode = git commit count,rebase/squash 改写历史
# 可能让 commit count 变小,导致新版本 versionCode < 旧版本、升级判定失效。
# 取按提交时间倒序最近的非当前 tag,算其 commit count,要求当前 HEAD 不小于上个 Release。
# 允许相等:rc 转正式版时常在同一提交上打 tag,versionCode 相等是正常的。
# 仅打 tag 触发时校验;workflow_dispatch(手动,无 tag)跳过。
- name: Verify versionCode monotonic
if: startsWith(github.ref, 'refs/tags/v')
run: |
set -e
cur_vc=$(git rev-list --count HEAD)
prev_tag=$(git tag --sort=-creatordate | grep -vE "^${GITHUB_REF_NAME}$" | head -1)
if [[ -z "$prev_tag" ]]; then
echo "无历史 tag,跳过 versionCode 单调校验(首次发版)"
echo "当前 versionCode(commit count) = $cur_vc"
exit 0
fi
prev_vc=$(git rev-list --count "$prev_tag")
echo "上个 Release($prev_tag) versionCode = $prev_vc"
echo "当前(${GITHUB_REF_NAME}) versionCode = $cur_vc"
if [[ "$cur_vc" -lt "$prev_vc" ]]; then
echo "::error::versionCode 回退: 当前 $cur_vc < 上个 Release($prev_tag) $prev_vc。通常因 rebase/squash 改写历史使 commit count 变小。请确保新版本在旧 tag 之上线性推进。"
exit 1
fi
echo "✓ versionCode 单调递增: $prev_vc -> $cur_vc"
- name: Restore release keystore
env:
# keystore 文件以 base64 存入 Secret,避免二进制文件被破坏。
# 还原到 app/aicode.jks,与 build.gradle.kts 期望的位置一致。
KEYSTORE_BASE64: ${{ secrets.AICODE_KEYSTORE_BASE64 }}
run: |
echo "$KEYSTORE_BASE64" | base64 -d > app/aicode.jks
echo "keystore restored ($(stat -c%s app/aicode.jks) bytes)"
- name: Generate keystore.properties
env:
KEYSTORE_PASSWORD: ${{ secrets.AICODE_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.AICODE_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.AICODE_KEY_PASSWORD }}
run: |
cat > app/keystore.properties <<EOF
storeFile=aicode.jks
storePassword=$KEYSTORE_PASSWORD
keyAlias=$KEY_ALIAS
keyPassword=$KEY_PASSWORD
EOF
echo "app/keystore.properties generated"
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
# 迁移版本号对账(发版前最后一道闸):编号连续、SCHEMA_VERSION 一致、
# 已发布迁移未被篡改/复用。tag 是迁移版本号的分界线,发版前必须通过。
- name: Check migration versions
run: python3 scripts/check_migrations.py
# 发版质量门禁:跑单元测试,失败即中止,避免发出回归包。
# 只跑 universal debug 变体:单测逻辑与 flavor 无关,跑全 flavor 6 变体是重复编译。
- name: Run unit tests
run: ./gradlew :app:testUniversalDebugUnitTest
# 一次性构建全部 flavor 的 release APK(universal/armsolo/x86solo)
# --stacktrace:构建失败时输出完整调用栈,便于在 CI 定位 OOM/内存类根因。
- name: Build all flavor Release APKs
run: ./gradlew assembleRelease --stacktrace
# 给三个 APK 重命名为「包名-架构-版本」格式,架构用真实 ABI 名便于用户识别:
# aicode-arm64-v1.0.0-rc1.apk (armsolo)
# aicode-x86_64-v1.0.0-rc1.apk (x86solo)
# aicode-universal-v1.0.0-rc1.apk (universal,含两套镜像)
- name: Rename APKs by flavor
run: |
mkdir -p dist
ver="${GITHUB_REF_NAME:-manual}" # tag 如 v1.0.0-rc1;手动触发时为 manual-N
declare -A abi=( [armsolo]=arm64 [x86solo]=x86_64 [universal]=universal )
for f in universal armsolo x86solo; do
src="app/build/outputs/apk/$f/release/app-$f-release.apk"
if [[ -f "$src" ]]; then
out="dist/aicode-${abi[$f]}-${ver}.apk"
cp "$src" "$out"
echo "packaged $out"
else
echo "::warning::missing $src"
fi
done
# 保留 R8 混淆映射(mapping.txt)到 dist/,随 Release 一起发布,供崩溃堆栈反混淆。
# 三个 flavor 共享同一份字节码与混淆结果,取 universal 一份即可;
# gzip 压缩后只有几 MB(原始约 90MB),retrace/Android Studio 解析前 gunzip 一下。
# 命名沿用 aicode-<abi>-<ver> 约定,看到崩溃报告里的版本号即可取对应 mapping。
# AGP 8.x 的 mapping 输出目录是 variant 驼峰名(flavor + BuildType),
# 即 outputs/mapping/universalRelease/,不是 outputs/mapping/universal/release/。
if [[ -f "app/build/outputs/mapping/universalRelease/mapping.txt" ]]; then
gzip -c "app/build/outputs/mapping/universalRelease/mapping.txt" > "dist/aicode-universal-${ver}-mapping.txt.gz"
echo "packaged aicode-universal-${ver}-mapping.txt.gz"
else
echo "::warning::missing mapping.txt(R8 混淆映射)"
fi
ls -lh dist/
- name: Determine release name
id: release
run: |
# 手动触发(无 tag)时用 run id 作为临时名,避免破坏 tag 发布。
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
else
echo "tag=manual-${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
fi
- name: Determine prerelease flag
id: prerelease
run: |
tag="${{ steps.release.outputs.tag }}"
if [[ "$tag" =~ -(rc|dev|beta|alpha)([.0-9]*)?$ ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
echo "检测到预发布后缀,标记为 pre-release"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
echo "正式版,不标记 pre-release"
fi
# GitHub 的 generate_release_notes 只列 merged PR,直接 push 的 commit 不会出现。
# 本仓库在 main 上直接 commit,不走 PR 流程,所以手动用 git log 生成 changelog。
- name: Generate changelog from git log
id: changelog
if: startsWith(github.ref, 'refs/tags/v')
run: |
cur_tag="${GITHUB_REF_NAME}"
# 正式版(vX.Y.Z 无后缀)从上一个正式版起算,包含整个发布周期(含全部 RC)的提交,
# 保证应用内检测更新能看到完整日志;RC/预发布从最近创建的 tag 起算(增量)。
if [[ "$cur_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
prev_tag=$(git tag --sort=-creatordate | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$" | grep -vE "^${cur_tag}$" | head -1)
echo "正式版:从上一个正式版 ${prev_tag:-无} 起算 changelog(含全部 RC 提交)"
else
prev_tag=$(git tag --sort=-creatordate | grep -vE "^${cur_tag}$" | head -1)
echo "预发布:从最近 tag ${prev_tag:-无} 起算 changelog"
fi
if [[ -z "$prev_tag" ]]; then
echo "无历史 tag,列出最近 30 条提交"
log=$(git log --oneline --no-decorate -30)
else
echo "生成 ${prev_tag}..${cur_tag} 的 changelog"
log=$(git log --oneline --no-decorate "${prev_tag}..HEAD")
fi
# 转义为 GitHub Actions 输出(多行用 EOF 分隔符)
delimiter="EOF_$(date +%s)"
echo "body<<${delimiter}" >> "$GITHUB_OUTPUT"
echo "## What's Changed" >> "$GITHUB_OUTPUT"
echo "" >> "$GITHUB_OUTPUT"
if [[ -z "$log" ]]; then
echo "- 无新增提交(与上次发布同一提交)" >> "$GITHUB_OUTPUT"
else
echo "$log" | while IFS= read -r line; do
# 提取 commit hash 和 message,格式化为列表项
sha=$(echo "$line" | cut -d' ' -f1)
msg=$(echo "$line" | cut -d' ' -f2-)
echo "- ${msg} (${sha})" >> "$GITHUB_OUTPUT"
done
fi
echo "" >> "$GITHUB_OUTPUT"
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${prev_tag}...${cur_tag}" >> "$GITHUB_OUTPUT"
echo "${delimiter}" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release & Upload assets
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.release.outputs.tag }}
name: Release ${{ steps.release.outputs.tag }}
body: ${{ steps.changelog.outputs.body }}
prerelease: ${{ steps.prerelease.outputs.is_prerelease }}
# 产物名约定 aicode-<abi>-<版本>.apk(见 Rename 步骤),故 glob 把 ABI 段放 aicode- 紧后、版本段用 * 兜底。
# 不要写成 aicode-*-<abi>.apk——那会把 abi 误放末段,与 aicode-<abi>-<ver> 永远匹配不上(rc3 翻车原因)。
files: |
dist/aicode-universal-*.apk
dist/aicode-arm64-*.apk
dist/aicode-x86_64-*.apk
# R8 映射与 APK 同版本发布,反混淆崩溃报告的凭据(gzip 压缩,体积小)
dist/aicode-universal-*mapping.txt.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}