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
78 changes: 47 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,45 +45,16 @@ jobs:
lfs: true
fetch-depth: 0

- name: Release tag is on main and its CI passed
- name: Release tag is on main
if: github.ref_type == 'tag'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
git fetch --no-tags origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "::error::$GITHUB_REF_NAME must point at a commit on main"
exit 1
fi

required=("Build and verify" "Compatibility API 29" "Compatibility API 31")
deadline=$((SECONDS + 2400))
while true; do
runs="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs?per_page=100" --jq '.check_runs')"
waiting=0
for name in "${required[@]}"; do
latest="$(jq -c --arg n "$name" '[.[] | select(.name == $n)] | sort_by(.started_at) | last // {}' <<<"$runs")"
conclusion="$(jq -r '.conclusion // "pending"' <<<"$latest")"
case "$conclusion" in
success) ;;
pending) waiting=1 ;;
*)
echo "::error::CI check '$name' ended as '$conclusion' for $GITHUB_SHA"
exit 1
;;
esac
done
[[ "$waiting" -eq 0 ]] && break
if (( SECONDS >= deadline )); then
echo "::error::CI for $GITHUB_SHA did not finish within 40 minutes"
exit 1
fi
echo "Waiting for CI on $GITHUB_SHA ..."
sleep 30
done
echo "CI passed for $GITHUB_SHA"

- name: Set up JDK 17
uses: actions/setup-java@v6
with:
Expand Down Expand Up @@ -121,6 +92,47 @@ jobs:
shell: bash
run: chmod +x gradlew

# Compile everything and lint the release variant while main's CI is still
# running. Unsigned, and before any secret is read; the signed build after
# the CI gate only packages and signs.
- name: Build and lint the release variant ahead of the CI gate
shell: bash
run: ./gradlew :app:lintRelease :app:assembleRelease --no-daemon --console=plain

- name: CI passed for the release commit
if: github.ref_type == 'tag'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
# The unit tests ran in "Build and verify"; the release build below does not repeat them.
required=("Build and verify" "Compatibility API 29" "Compatibility API 31")
deadline=$((SECONDS + 2400))
while true; do
runs="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs?per_page=100" --jq '.check_runs')"
waiting=0
for name in "${required[@]}"; do
latest="$(jq -c --arg n "$name" '[.[] | select(.name == $n)] | sort_by(.started_at) | last // {}' <<<"$runs")"
conclusion="$(jq -r '.conclusion // "pending"' <<<"$latest")"
case "$conclusion" in
success) ;;
pending) waiting=1 ;;
*)
echo "::error::CI check '$name' ended as '$conclusion' for $GITHUB_SHA"
exit 1
;;
esac
done
[[ "$waiting" -eq 0 ]] && break
if (( SECONDS >= deadline )); then
echo "::error::CI for $GITHUB_SHA did not finish within 40 minutes"
exit 1
fi
echo "Waiting for CI on $GITHUB_SHA ..."
sleep 30
done
echo "CI passed for $GITHUB_SHA"

# Only this step ever reads the signing secrets, and only for tags. Pull
# requests and manual runs sign with a key that exists for this job alone.
- name: Use the release signing key
Expand Down Expand Up @@ -173,9 +185,13 @@ jobs:
echo "OPENIME_REHEARSAL=1"
} >> "$GITHUB_ENV"

- name: Test, lint, build and verify the release APK
- name: Sign and verify the release APK
id: release
shell: bash
env:
# Unit tests passed in CI on this commit, lintRelease ran above.
OPENIME_SKIP_TESTS: '1'
OPENIME_SKIP_LINT: '1'
run: bash scripts/release_build.sh

- name: Summary
Expand Down
8 changes: 5 additions & 3 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,14 @@ bash scripts/setup_release_signing.sh

标签只有管理员能创建,创建后不能被移动或删除(见 REPOSITORY.md)。
4. `.github/workflows/release.yml` 自动执行:
- 标签格式、`VERSION`、`CHANGELOG.md` 三者一致;标签在 `main` 上,且该提交的 CI 已通过;
- 单元测试、`lintRelease`、`assembleRelease`;
- 标签在 `main` 上;
- 不等 CI,先用未签名配置编译并跑 `lintRelease`(和 main 的 CI 同时进行,不读取任何密钥);
- 等该提交的 CI 通过(Build and verify、Compatibility API 29/31)。单元测试已在 CI 里跑过,这里不重复;
- 标签格式、`VERSION`、`CHANGELOG.md` 三者一致,然后用正式密钥 `assembleRelease`(只剩打包和签名);
- APK 签名校验(不能是 Debug 证书)、只含 `arm64-v8a`、APK 内版本与 `VERSION` 一致、
签名证书与 `release-cert.sha256` 一致;
- 生成 SHA-256 和发布说明(测试版带 Beta 提示);
- 另一个只有写权限、不接触密钥的 job 先建**草稿** Release,确认三个附件齐全后才公开(测试版标为 pre-release,不是 latest)。
- 另一个只有写权限、不接触密钥的 job 先建**草稿** Release,确认 APK 已附上后才公开(测试版标为 pre-release,不是 latest)。
5. 发布后核对:下载 APK,`sha256sum` 与发布说明里的值对比,`apksigner verify --print-certs`,
在真机上安装、启用、试打。

Expand Down
6 changes: 4 additions & 2 deletions scripts/release_build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
# Optional:
# OPENIME_RELEASE_TAG tag being released; must equal v<VERSION>
# OPENIME_REHEARSAL=1 the keystore is a throwaway: skip the certificate continuity check
# OPENIME_SKIP_TESTS=1 skip :app:testDebugUnitTest (quick local runs only)
# OPENIME_SKIP_TESTS=1 skip :app:testDebugUnitTest (the workflow: CI ran them on this commit)
# OPENIME_SKIP_LINT=1 skip :app:lintRelease (the workflow: it lints before the CI gate)
# OPENIME_OUT_DIR output directory (default: build/release-files)
# OPENIME_GRADLE_ARGS extra Gradle arguments, e.g. --offline
# ANDROID_HOME SDK containing build-tools/35.0.0 (apksigner, aapt2)
Expand Down Expand Up @@ -50,7 +51,8 @@ python3 scripts/release_check.py "${check_args[@]}"
VERSION="$(python3 scripts/release_check.py version | cut -d' ' -f1)"

# 2. Build. lintRelease runs here because pull-request CI only lints the debug variant.
tasks=(:app:lintRelease :app:assembleRelease)
tasks=(:app:assembleRelease)
[[ "${OPENIME_SKIP_LINT:-0}" == "1" ]] || tasks=(:app:lintRelease "${tasks[@]}")
[[ "${OPENIME_SKIP_TESTS:-0}" == "1" ]] || tasks=(:app:testDebugUnitTest "${tasks[@]}")
read -r -a extra_gradle_args <<< "${OPENIME_GRADLE_ARGS:-}"
./gradlew "${tasks[@]}" --no-daemon --console=plain "${extra_gradle_args[@]}"
Expand Down
Loading