Repository navigation
296 lines (268 loc) · 13.6 KB
/
Copy pathrelease.yml
File metadata and controls
296 lines (268 loc) · 13.6 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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
name: 🚀 Release APK
# Quy trình: làm việc trên `develop`, mở MR vào `main`.
#
# - MR vào main → build + kiểm tra, đính APK làm artifact. KHÔNG phát hành.
# - Merge vào main → phát hành thật: GitHub Release + mail cho tester.
# - Chạy tay → phát hành, dùng khi cần phát lại một bản.
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
inputs:
version:
description: 'Ghi đè số phiên bản (bỏ trống thì lấy từ pubspec.yaml)'
required: false
permissions:
contents: write # cần để tạo GitHub Release
jobs:
release:
name: Build, ký và phát hành APK
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
publish: ${{ steps.ver.outputs.publish }}
# Khai ở cấp job: `env` của một step KHÔNG dùng được trong `if` của chính
# step đó, nên bước Firebase phải đọc biến này từ đây.
env:
FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/fetch-core
with:
deploy-key: ${{ secrets.CORE_DEPLOY_KEY }}
# ---------------------------------------------------------------- version
- name: Xác định phiên bản
id: ver
run: |
INPUT="${{ github.event.inputs.version }}"
if [ -n "$INPUT" ]; then
VERSION="$INPUT"
else
# pubspec: "version: 1.2.3+4" -> lấy phần 1.2.3
VERSION=$(grep '^version:' pubspec.yaml \
| sed 's/^version:[[:space:]]*//' | cut -d+ -f1 | tr -d '[:space:]')
fi
if [ -z "$VERSION" ]; then
echo "::error::Không đọc được phiên bản từ pubspec.yaml."
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "MR: chỉ build kiểm tra, không phát hành. Phiên bản $VERSION"
else
echo "publish=true" >> "$GITHUB_OUTPUT"
echo "Phát hành $VERSION (build ${{ github.run_number }})"
fi
# Chặn sớm, trước khi tốn 7 phút build: không cho phát hành đè lên một
# phiên bản đã có. Quên tăng version trong pubspec.yaml là dính bước này.
- name: Kiểm tra phiên bản chưa từng phát hành
if: steps.ver.outputs.publish == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="v${{ steps.ver.outputs.version }}"
if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "::error::Release $TAG đã tồn tại. Hãy tăng version trong pubspec.yaml."
exit 1
fi
echo "$TAG chưa tồn tại, tiếp tục."
# ------------------------------------------------------- ghi chú phát hành
# Cùng lý do "chặn sớm" như bước trên, và cùng thời điểm: chỉ cần
# CHANGELOG.md, thứ đã có ngay sau checkout. Đứng cạnh bước kiểm tra
# phiên bản thì cả hai điều kiện phát hành đều được trả lời trong ~15 giây
# đầu, thay vì để việc quên viết CHANGELOG lộ ra sau 7 phút build.
# Các file sinh ra ở đây tồn tại đến hết job, nên phần phân phối bên dưới
# vẫn dùng lại được.
#
# Cả hai kênh phân phối trước đây chỉ đưa ra TIÊU ĐỀ. GitHub Release dùng
# generate_release_notes, tức là một danh sách title PR; còn mail Firebase
# gửi head_commit.message, mà body mặc định của merge commit đúng bằng
# title của PR. Nên bản v1.0.0 đến tay tester chỉ có:
#
# AegisNet 1.0.0 (build N)
# Merge pull request #12 from vannt-dev/develop
# Release gate: develop → main (mở sớm để CI chạy liên tục)
#
# Không một chữ nào về những gì đã thay đổi. Phần viết tử tế nhất nằm ở
# CHANGELOG.md và không tới được đâu cả, kể cả cảnh báo kiểu "bản này chưa
# verify trên iOS" — thứ mà tester cần biết trước khi cài.
#
# So khớp theo tiền tố chính xác (index == 1) chứ không dùng regex: số
# phiên bản có dấu chấm, và trong regex thì "1.1.0" khớp luôn cả "1x1y0".
- name: Trích ghi chú phát hành từ CHANGELOG
if: steps.ver.outputs.publish == 'true'
run: |
VERSION="${{ steps.ver.outputs.version }}"
awk -v hdr="## [$VERSION]" '
index($0, hdr) == 1 { found = 1; next }
/^## \[/ { if (found) exit }
found
' CHANGELOG.md > RELEASE_NOTES.md
# Quên viết CHANGELOG cho phiên bản mới thì dừng ở đây, thay vì phát
# hành một bản không có ghi chú rồi mới phát hiện.
if [ ! -s RELEASE_NOTES.md ]; then
echo "::error::CHANGELOG.md không có mục [$VERSION]. Thêm mục đó rồi phát hành lại."
exit 1
fi
# Firebase giới hạn ghi chú ở 16384 ký tự và cắt cụt trong im lặng nếu
# vượt. Cắt sẵn, có chừa chỗ cho dòng tiêu đề ghép vào bên dưới.
{
echo "AegisNet $VERSION (build ${{ github.run_number }})"
echo
head -c 15000 RELEASE_NOTES.md
} > FIREBASE_NOTES.md
echo "Ghi chú: $(wc -c < RELEASE_NOTES.md) ký tự cho GitHub, $(wc -c < FIREBASE_NOTES.md) cho Firebase."
# ------------------------------------------------------------- toolchains
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '17'
- uses: android-actions/setup-android@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
# QUAN TRỌNG: thiếu cargo-ndk thì task Gradle buildRustEngine bị bỏ qua
# trong im lặng (onlyIf + isIgnoreExitValue) và APK sẽ RA ĐỜI MÀ KHÔNG CÓ
# engine DNS — app cài được nhưng không chặn gì cả.
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
- uses: Swatinem/rust-cache@v2
with:
workspaces: rust/aegis_core
# cargo-ndk được build từ mã nguồn mất hơn nửa phút; giữ lại binary giữa các lần chạy.
- name: Lấy cargo-ndk từ cache
id: cargo-ndk
uses: actions/cache@v5
with:
path: ~/.cargo/bin/cargo-ndk
key: cargo-ndk-${{ runner.os }}-v1
- name: Cài cargo-ndk
if: steps.cargo-ndk.outputs.cache-hit != 'true'
run: cargo install cargo-ndk --locked
# ------------------------------------------------------------------ gate
- name: Cài dependency
run: flutter pub get
# Trên PR, build.yml đã analyze, test và cargo test đúng commit này; chỉ chạy lại khi phát hành thật.
- name: Kiểm tra trước khi phát hành
if: steps.ver.outputs.publish == 'true'
run: |
flutter analyze
flutter test
cd rust/aegis_core && cargo test
# --------------------------------------------------------------- ký tên
- name: Dựng lại keystore từ secret
env:
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
STORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
if [ -z "$KEYSTORE_B64" ]; then
echo "::error::Thiếu secret ANDROID_KEYSTORE_BASE64. Xem RELEASE_GUIDE.md."
exit 1
fi
echo "$KEYSTORE_B64" | base64 -d > android/app/upload-keystore.jks
cat > android/key.properties <<EOF
storePassword=$STORE_PASSWORD
keyPassword=$KEY_PASSWORD
keyAlias=$KEY_ALIAS
storeFile=upload-keystore.jks
EOF
# ----------------------------------------------------------------- build
- name: Build APK release
run: |
# Obfuscated: the names in the shipped Dart code are replaced, which
# makes the APK harder to read back. The symbol files stay on the
# runner: an artifact of a public repository can be downloaded by
# anyone, and with the APK they undo the obfuscation.
flutter build apk --release \
--obfuscate --split-debug-info=build/symbols \
--build-name="${{ steps.ver.outputs.version }}" \
--build-number="${{ github.run_number }}"
# Chốt chặn cuối: APK phải chứa engine cho cả 3 ABI. Nếu cargo-ndk hỏng
# giữa chừng, Gradle vẫn báo thành công — chỉ bước này bắt được.
- name: Xác minh engine Rust nằm trong APK
run: |
APK=build/app/outputs/flutter-apk/app-release.apk
COUNT=$(unzip -l "$APK" | grep -c 'libaegis_core\.so' || true)
echo "Tìm thấy libaegis_core.so trong $COUNT ABI:"
unzip -l "$APK" | grep 'libaegis_core\.so' || true
if [ "$COUNT" -ne 3 ]; then
echo "::error::Cần đủ 3 ABI (arm64-v8a, armeabi-v7a, x86_64), chỉ có $COUNT."
echo "::error::APK này sẽ chạy ở chế độ fallback và KHÔNG lọc DNS. Không phát hành."
exit 1
fi
- name: Xác minh APK không ký bằng khoá debug
run: |
APK=build/app/outputs/flutter-apk/app-release.apk
# Có thể cài nhiều bản build-tools; lấy bản mới nhất thay vì để glob
# nở ra nhiều đường dẫn rồi biến phần thừa thành tham số.
APKSIGNER=$(ls "$ANDROID_HOME"/build-tools/*/apksigner | sort -V | tail -1)
echo "Dùng $APKSIGNER"
DN=$("$APKSIGNER" verify --print-certs "$APK" \
| grep -i 'certificate DN' || true)
echo "$DN"
if [ -z "$DN" ]; then
echo "::error::Không đọc được chữ ký của APK."
exit 1
fi
if echo "$DN" | grep -qi 'CN=Android Debug'; then
echo "::error::APK đang ký bằng khoá DEBUG — key.properties không được đọc."
exit 1
fi
# ------------------------------------------------- MR: chỉ đưa APK ra xem
- name: Đính APK vào MR để tải thử
if: steps.ver.outputs.publish != 'true'
uses: actions/upload-artifact@v6
with:
name: aegisnet-${{ steps.ver.outputs.version }}-pr${{ github.event.pull_request.number }}
path: build/app/outputs/flutter-apk/app-release.apk
retention-days: 14
# ------------------------------------------------------------ phân phối
# GitHub Release đứng TRƯỚC Firebase: nó chỉ phụ thuộc file đã build, còn
# Firebase phụ thuộc dịch vụ ngoài. Xếp ngược lại thì một trục trặc phía
# Firebase sẽ cuốn theo cả bản lưu trữ, dù APK hoàn toàn hợp lệ.
# target_commitish phải khai rõ. Không có nó, GitHub gắn tag vào default
# branch của repo — ở đây là `develop`, không phải `main` — nên v1.0.0 trỏ
# vào tip của develop thay vì merge commit vừa phát hành. Lần đó nội dung
# trùng nhau nên không ai thiệt, nhưng chỉ vì main không có commit riêng;
# một hotfix đi thẳng vào main là tag trỏ sang cây khác hẳn bản đã build.
- name: Tạo GitHub Release
if: steps.ver.outputs.publish == 'true'
uses: softprops/action-gh-release@v3
with:
tag_name: v${{ steps.ver.outputs.version }}
target_commitish: ${{ github.sha }}
name: AegisNet ${{ steps.ver.outputs.version }}
# body_path đứng trước phần tự sinh: người đọc thấy ghi chú thật
# trước, danh sách PR ở dưới làm tham chiếu.
body_path: RELEASE_NOTES.md
generate_release_notes: true
files: build/app/outputs/flutter-apk/app-release.apk
- name: Gửi cho tester qua Firebase App Distribution
if: steps.ver.outputs.publish == 'true' && env.FIREBASE_APP_ID != ''
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
groups: testers
file: build/app/outputs/flutter-apk/app-release.apk
# Dòng tiêu đề đã được ghép sẵn vào đầu file ở bước trích ghi chú.
releaseNotesFile: FIREBASE_NOTES.md
# Mã nguồn engine nằm ở repo riêng, nên người clone repo này không tự build
# được nó. Mỗi bản phát hành kèm engine đã build sẵn (Android lấy từ chính APK
# vừa phát hành, iOS build trên macOS) để Gradle và ios/fetch_prebuilt_engine.sh
# tải về khi thiếu mã nguồn. Xem engine-prebuilt.yml.
engine:
name: Engine dựng sẵn cho người build từ mã nguồn công khai
needs: release
if: needs.release.outputs.publish == 'true'
uses: ./.github/workflows/engine-prebuilt.yml
with:
tag: v${{ needs.release.outputs.version }}
secrets: inherit