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
161 changes: 148 additions & 13 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ name: Build
on:
push:
branches: [ main ]
# 推 v* tag 會多跑下面的 release job,把簽好章的 APK 放進 GitHub Release。
tags: [ 'v*' ]
pull_request:
workflow_dispatch:

Expand Down Expand Up @@ -65,28 +67,100 @@ jobs:
app/build/reports/lint-results-debug.xml
if-no-files-found: warn

- name: Build debug APK
run: ./gradlew assembleDebug --stacktrace
# F-Droid 建的是 release variant,這裡也走同一條路徑。Gradle 沒有簽章設定,
# 產出的是未簽章的 APK —— 跟 F-Droid 從原始碼建出來的應該位元相同,
# 這是 reproducible build 的前提(見下面的 reproducible job)。
- name: Build release APK
run: ./gradlew assembleRelease --stacktrace

- name: Upload debug APK
- name: Upload unsigned release APK
uses: actions/upload-artifact@v5
with:
name: mapconquer-debug
path: app/build/outputs/apk/debug/app-debug.apk
name: mapconquer-release-unsigned
path: app/build/outputs/apk/release/app-release-unsigned.apk
if-no-files-found: error

# F-Droid 建的是 release variant,這裡也走一遍同一條路徑。
# 專案沒有簽章設定,所以產出的是未簽章的 APK。
- name: Build release APK
run: ./gradlew assembleRelease --stacktrace

- name: Upload release APK
# 用正式金鑰簽名。簽名刻意不放進 Gradle:建置產物保持跟 F-Droid 的一致,
# 簽名是建置之後的一步,F-Droid 比對時把這份的簽章複製到它自己建的 APK 上
# 驗證即可。
#
# 金鑰以 base64 存在 repo secret RELEASE_KEYSTORE,密碼與別名另外三個
# secret。keystore 只在這一步寫進 runner 的暫存目錄,簽完立刻刪除。
# 拿不到 secret 的情況(來自 fork 的 PR)只留警告,不讓建置失敗。
- name: Sign release APK
id: sign
env:
RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }}
RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }}
RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
run: |
if [ -z "$RELEASE_KEYSTORE" ]; then
# 發行版本不能沒有簽章:推 tag 時缺 secret 就直接失敗。
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
echo "::error::RELEASE_KEYSTORE secret 沒有設定,無法簽章發行版本。"
exit 1
fi
echo "::warning::RELEASE_KEYSTORE secret 沒有設定,只產出未簽章的 release APK。"
exit 0
fi
apksigner="$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -n 1)/apksigner"
keystore="$RUNNER_TEMP/release.keystore"
trap 'rm -f "$keystore"' EXIT
echo "$RELEASE_KEYSTORE" | base64 -d > "$keystore"
"$apksigner" sign \
--ks "$keystore" \
--ks-pass env:RELEASE_KEYSTORE_PASSWORD \
--ks-key-alias "$RELEASE_KEY_ALIAS" \
--key-pass env:RELEASE_KEY_PASSWORD \
--out app/build/outputs/apk/release/mapconquer-release.apk \
app/build/outputs/apk/release/app-release-unsigned.apk
# 憑證指紋印在 log 上:F-Droid metadata 的 AllowedAPKSigningKeys 填的就是它。
"$apksigner" verify --print-certs app/build/outputs/apk/release/mapconquer-release.apk
echo "signed=true" >> "$GITHUB_OUTPUT"

- name: Upload signed release APK
if: steps.sign.outputs.signed == 'true'
uses: actions/upload-artifact@v5
with:
name: mapconquer-release-unsigned
path: app/build/outputs/apk/release/*.apk
name: mapconquer-release
path: app/build/outputs/apk/release/mapconquer-release.apk
if-no-files-found: error

# 可重現建置的檢查:在另一個目錄、關掉 build cache 再建一次 release,
# 兩份未簽章 APK 必須位元相同。F-Droid 從原始碼建出來的 APK 跟我們簽的
# 那份(去掉簽章後)一致,才會發布我們簽的版本;不一致的話那一版不會上架。
# 這裡先在 CI 抓出時間戳、絕對路徑、排序不穩定這類問題。
reproducible:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Build twice from two different directories
run: |
./gradlew assembleRelease --no-build-cache --stacktrace
cp app/build/outputs/apk/release/app-release-unsigned.apk "$RUNNER_TEMP/first.apk"
copy="$RUNNER_TEMP/second-checkout"
git worktree add "$copy" HEAD
(cd "$copy" && ./gradlew assembleRelease --no-build-cache --stacktrace)
cp "$copy/app/build/outputs/apk/release/app-release-unsigned.apk" "$RUNNER_TEMP/second.apk"
sha256sum "$RUNNER_TEMP/first.apk" "$RUNNER_TEMP/second.apk"
if ! cmp -s "$RUNNER_TEMP/first.apk" "$RUNNER_TEMP/second.apk"; then
echo "::error::兩次建置的 release APK 不一致,這個版本不可重現。"
(cd "$RUNNER_TEMP" && mkdir a b && unzip -q first.apk -d a && unzip -q second.apk -d b && diff -r a b | head -50) || true
exit 1
fi
echo "兩次建置位元相同。"

# 地圖與翻譯都是產生出來的,重跑產生器之後不該有任何差異 ——
# 否則代表有人手動改了 assets,下一次重新產生就會被蓋掉。
generated-assets:
Expand All @@ -111,3 +185,64 @@ jobs:
echo "請在本機執行 python3 tools/genworld.py && python3 tools/genstrings.py 之後一起 commit。"
exit 1
fi

# 推 v<versionName> tag 時發行:前面三個 job 都通過才會跑到這裡,
# 所以發出去的 APK 一定通過了測試、產生檔檢查與可重現建置檢查。
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [ build, reproducible, generated-assets ]
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/checkout@v5

# 版本號寫在原始碼裡(F-Droid 從原始碼讀它),tag 只是指向那個 commit。
# 兩者不一致代表忘了改 versionName,或 tag 打錯了 commit。
- name: Check the tag against the version in the source
id: version
run: |
name=$(sed -n 's/^ *versionName = "\(.*\)"$/\1/p' app/build.gradle.kts)
code=$(sed -n 's/^ *versionCode = \([0-9]*\)$/\1/p' app/build.gradle.kts)
if [ "$GITHUB_REF_NAME" != "v$name" ]; then
echo "::error::tag $GITHUB_REF_NAME 跟 app/build.gradle.kts 的 versionName $name 不一致。"
exit 1
fi
changelog="fastlane/metadata/android/en-US/changelogs/$code.txt"
if [ ! -f "$changelog" ]; then
echo "::error::找不到 versionCode $code 的更新說明 $changelog。"
exit 1
fi
echo "name=$name" >> "$GITHUB_OUTPUT"
echo "code=$code" >> "$GITHUB_OUTPUT"

- name: Download the signed APK
uses: actions/download-artifact@v5
with:
name: mapconquer-release
path: dist

- name: Create the GitHub Release
env:
GH_TOKEN: ${{ github.token }}
NAME: ${{ steps.version.outputs.name }}
CODE: ${{ steps.version.outputs.code }}
run: |
apk="dist/mapconquer-$NAME.apk"
mv dist/mapconquer-release.apk "$apk"
{
cat "fastlane/metadata/android/en-US/changelogs/$CODE.txt"
for lang in zh-TW zh-CN; do
file="fastlane/metadata/android/$lang/changelogs/$CODE.txt"
if [ -f "$file" ]; then echo; echo "---"; echo; cat "$file"; fi
done
echo
echo "---"
echo
echo "SHA-256 of \`mapconquer-$NAME.apk\`: \`$(sha256sum "$apk" | cut -d' ' -f1)\`"
} > notes.md
gh release create "$GITHUB_REF_NAME" "$apk" \
--title "MapConquer $NAME" \
--notes-file notes.md \
--verify-tag
Loading
Loading