From 66152b49a40c931a44b43bf4cc244509f55e0bc5 Mon Sep 17 00:00:00 2001 From: limuzi013 <128580527+limuzi013@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:49:12 +0800 Subject: [PATCH] Release build compiles and lints while main's CI runs The tag job used to wait for CI first and then repeat the unit tests, lintRelease and the whole compile. Now it checks the tag is on main, builds and lints the release variant unsigned (no secrets read) while CI is still running, waits for CI, and only then signs: the signed assembleRelease is packaging alone (15s in a local rehearsal). Unit tests are not repeated: the gate requires "Build and verify" on the same commit. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 78 +++++++++++++++++++++-------------- docs/RELEASE.md | 8 ++-- scripts/release_build.sh | 6 ++- 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e461021..4a0ff7b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,11 +45,9 @@ 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 @@ -57,33 +55,6 @@ jobs: 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: @@ -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 @@ -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 diff --git a/docs/RELEASE.md b/docs/RELEASE.md index ff3d4dfc..2c172cea 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -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`, 在真机上安装、启用、试打。 diff --git a/scripts/release_build.sh b/scripts/release_build.sh index 36efc194..7a7a948e 100755 --- a/scripts/release_build.sh +++ b/scripts/release_build.sh @@ -7,7 +7,8 @@ # Optional: # OPENIME_RELEASE_TAG tag being released; must equal v # 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) @@ -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[@]}"