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[@]}"