chore(release): v1.2.3 #44
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: Release APK | |
| "on": | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| tag_name: | |
| description: "Existing v* tag to release" | |
| required: true | |
| type: string | |
| prerelease: | |
| description: "Mark the GitHub Release as a prerelease" | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: write | |
| jobs: | |
| resolve-release: | |
| name: Resolve release metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag: ${{ steps.release_tag.outputs.tag }} | |
| version_name: ${{ steps.release_tag.outputs.version_name }} | |
| version_code: ${{ steps.release_tag.outputs.version_code }} | |
| steps: | |
| - name: Resolve release tag | |
| id: release_tag | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then | |
| input_tag="${{ inputs.tag_name }}" | |
| else | |
| input_tag="${GITHUB_REF_NAME}" | |
| fi | |
| version_name="${input_tag#v}" | |
| if [[ ! "${version_name}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then | |
| echo "Release tag must be semantic, for example v1.2.3. Got: ${input_tag}" >&2 | |
| exit 1 | |
| fi | |
| tag="v${version_name}" | |
| major="${BASH_REMATCH[1]}" | |
| minor="${BASH_REMATCH[2]}" | |
| patch="${BASH_REMATCH[3]}" | |
| version_code=$((major * 10000 + minor * 100 + patch)) | |
| { | |
| echo "tag=${tag}" | |
| echo "version_name=${version_name}" | |
| echo "version_code=${version_code}" | |
| } >> "${GITHUB_OUTPUT}" | |
| release-apk: | |
| name: Build signed release APK | |
| runs-on: ubuntu-latest | |
| needs: resolve-release | |
| timeout-minutes: 90 | |
| env: | |
| CMAKE_VERSION: "3.22.1" | |
| NDK_VERSION: "29.0.14206865" | |
| ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} | |
| ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} | |
| ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.resolve-release.outputs.tag }} | |
| fetch-depth: 1 | |
| - name: Initialize required submodules | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git submodule update --init --recursive third_party/hoshidicts-kotlin-bridge | |
| git submodule status --recursive | |
| - name: Set up JDK | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: "21" | |
| - name: Set up Android SDK | |
| uses: android-actions/setup-android@v3 | |
| - name: Install Android build tools | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| sdkmanager \ | |
| "platforms;android-36.1" \ | |
| "build-tools;36.0.0" \ | |
| "cmake;${CMAKE_VERSION}" \ | |
| "ndk;${NDK_VERSION}" | |
| ndk_home="${ANDROID_HOME}/ndk/${NDK_VERSION}" | |
| echo "ANDROID_NDK_HOME=${ndk_home}" >> "${GITHUB_ENV}" | |
| - name: Set up Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android | |
| - name: Restore Cargo cache | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: app/src/main/rust/hoshiepub | |
| - name: Install cargo-ndk | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if ! command -v cargo-ndk >/dev/null 2>&1; then | |
| cargo install cargo-ndk --locked | |
| fi | |
| - name: Restore Gradle cache | |
| uses: gradle/actions/setup-gradle@v4 | |
| - name: Decode release keystore | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" ]]; then | |
| echo "ANDROID_KEYSTORE_BASE64 secret is required." >&2 | |
| exit 1 | |
| fi | |
| keystore="${RUNNER_TEMP}/hoshi-release-keystore.jks" | |
| printf '%s' "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" \ | |
| | base64 --decode > "${keystore}" | |
| echo "ANDROID_KEYSTORE_FILE=${keystore}" >> "${GITHUB_ENV}" | |
| - name: Warn if Gradle defaults differ from tag | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version_name="${{ needs.resolve-release.outputs.version_name }}" | |
| version_code="${{ needs.resolve-release.outputs.version_code }}" | |
| actual_name="$(sed -n 's/^[[:space:]]*versionName[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' app/build.gradle.kts | head -n 1)" | |
| actual_code="$(sed -n 's/^[[:space:]]*versionCode[[:space:]]*=[[:space:]]*\([0-9][0-9]*\).*/\1/p' app/build.gradle.kts | head -n 1)" | |
| if [[ "${actual_name}" != "${version_name}" ]]; then | |
| echo "::warning::app/build.gradle.kts versionName=${actual_name}, expected ${version_name}" | |
| fi | |
| if [[ "${actual_code}" != "${version_code}" ]]; then | |
| echo "::warning::app/build.gradle.kts versionCode=${actual_code}, expected ${version_code}" | |
| fi | |
| - name: Prepare split ABI release build | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python3 - <<'PY' | |
| from pathlib import Path | |
| path = Path("app/build.gradle.kts") | |
| text = path.read_text() | |
| text = text.replace( | |
| 'val releaseVersionCode = providers.gradleProperty("releaseVersionCode").orNull?.toIntOrNull()\n', | |
| 'val releaseVersionCode = providers.gradleProperty("releaseVersionCode").orNull?.toIntOrNull()\n' | |
| 'val releaseAbi = providers.gradleProperty("releaseAbi").orNull\n', | |
| ) | |
| text = text.replace( | |
| ' ndk {\n' | |
| ' abiFilters += listOf("arm64-v8a")\n' | |
| ' }\n', | |
| ' ndk {\n' | |
| ' abiFilters += listOf(releaseAbi ?: "arm64-v8a")\n' | |
| ' }\n', | |
| ) | |
| text = text.replace( | |
| ' outputs.file(rustReleaseJniLibsDir.resolve("arm64-v8a/libhoshiepub.so"))\n', | |
| ' outputs.file(rustReleaseJniLibsDir.resolve("${releaseAbi ?: "arm64-v8a"}/libhoshiepub.so"))\n', | |
| ) | |
| text = text.replace( | |
| ' "-t",\n' | |
| ' "arm64-v8a",\n' | |
| ' "-o",\n' | |
| ' rustReleaseJniLibsDir.absolutePath,\n' | |
| ' "build",\n' | |
| ' "--lib",\n' | |
| ' "--release",\n' | |
| ' )\n' | |
| '}\n' | |
| '\n' | |
| 'tasks.named("preBuild")', | |
| ' "-t",\n' | |
| ' releaseAbi ?: "arm64-v8a",\n' | |
| ' "-o",\n' | |
| ' rustReleaseJniLibsDir.absolutePath,\n' | |
| ' "build",\n' | |
| ' "--lib",\n' | |
| ' "--release",\n' | |
| ' )\n' | |
| '}\n' | |
| '\n' | |
| 'tasks.named("preBuild")', | |
| ) | |
| path.write_text(text) | |
| PY | |
| - name: Build and check split release APKs | |
| id: assets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tag="${{ needs.resolve-release.outputs.tag }}" | |
| version_name="${{ needs.resolve-release.outputs.version_name }}" | |
| version_code="${{ needs.resolve-release.outputs.version_code }}" | |
| build_tools="${ANDROID_HOME}/build-tools/36.0.0" | |
| apk_assets=() | |
| for abi in arm64-v8a armeabi-v7a; do | |
| rm -rf app/build/jniLibs/release app/build/outputs/apk/release | |
| ./gradlew --no-daemon clean lintVitalRelease assembleRelease \ | |
| -PreleaseAbi="${abi}" \ | |
| -PreleaseVersionName="${version_name}" \ | |
| -PreleaseVersionCode="${version_code}" | |
| python3 - <<PY | |
| import json | |
| from pathlib import Path | |
| expected_name = "${version_name}" | |
| expected_code = int("${version_code}") | |
| metadata_path = Path("app/build/outputs/apk/release/output-metadata.json") | |
| metadata = json.loads(metadata_path.read_text()) | |
| element = metadata["elements"][0] | |
| actual_name = element["versionName"] | |
| actual_code = element["versionCode"] | |
| if actual_name != expected_name or actual_code != expected_code: | |
| raise SystemExit( | |
| f"Release APK metadata mismatch for ${abi}: " | |
| f"versionName={actual_name}, versionCode={actual_code}; " | |
| f"expected versionName={expected_name}, versionCode={expected_code}" | |
| ) | |
| PY | |
| apk_source="app/build/outputs/apk/release/app-release.apk" | |
| apk_asset="Hoshi-Reader-${tag}-${abi}.apk" | |
| if [[ ! -f "${apk_source}" ]]; then | |
| echo "Expected signed APK was not found at ${apk_source}" >&2 | |
| find app/build/outputs/apk -type f -name '*.apk' -print >&2 || true | |
| exit 1 | |
| fi | |
| mapfile -t packaged_abis < <(zipinfo -1 "${apk_source}" \ | |
| | sed -n 's#^lib/\([^/]*\)/.*\.so$#\1#p' \ | |
| | sort -u) | |
| if [[ "${#packaged_abis[@]}" -ne 1 || "${packaged_abis[0]}" != "${abi}" ]]; then | |
| echo "Expected only ${abi} native libraries, found: ${packaged_abis[*]:-none}" >&2 | |
| zipinfo -1 "${apk_source}" | grep '^lib/' >&2 || true | |
| exit 1 | |
| fi | |
| zipinfo -1 "${apk_source}" | grep -qx "lib/${abi}/libhoshiepub.so" | |
| "${build_tools}/apksigner" verify --print-certs "${apk_source}" | |
| cp "${apk_source}" "${apk_asset}" | |
| apk_assets+=("${apk_asset}") | |
| done | |
| echo "apk_assets=${apk_assets[*]}" >> "${GITHUB_OUTPUT}" | |
| - name: Generate release notes from CHANGELOG | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version_name="${{ needs.resolve-release.outputs.version_name }}" | |
| changelog="docs/CHANGELOG.md" | |
| changelog_section="$(awk -v v="${version_name}" ' | |
| $0 ~ "^## \\[(v?" v ")\\]" { found = 1; next } | |
| found && $0 ~ "^## \\[" { exit } | |
| found { print } | |
| ' "${changelog}")" | |
| if [[ -z "${changelog_section//[[:space:]]/}" ]]; then | |
| changelog_section="$(awk ' | |
| $0 ~ "^## \\[Unreleased\\]" { found = 1; next } | |
| found && $0 ~ "^## \\[" { exit } | |
| found { print } | |
| ' "${changelog}")" | |
| fi | |
| if [[ -n "${changelog_section//[[:space:]]/}" ]]; then | |
| printf '%s\n' "${changelog_section}" > release_notes.md | |
| else | |
| { | |
| echo "Release ${{ needs.resolve-release.outputs.tag }}" | |
| echo | |
| echo "No matching section found in docs/CHANGELOG.md for ${version_name}." | |
| } > release_notes.md | |
| fi | |
| - name: Publish GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tag="${{ needs.resolve-release.outputs.tag }}" | |
| apk_assets="${{ steps.assets.outputs.apk_assets }}" | |
| prerelease_flag=() | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" \ | |
| && "${{ inputs.prerelease }}" == "true" ]]; then | |
| prerelease_flag=(--prerelease) | |
| fi | |
| if gh release view "${tag}" >/dev/null 2>&1; then | |
| gh release edit "${tag}" --notes-file release_notes.md | |
| if [[ ${#prerelease_flag[@]} -gt 0 ]]; then | |
| gh release edit "${tag}" "${prerelease_flag[@]}" | |
| fi | |
| else | |
| gh release create "${tag}" \ | |
| --title "Hoshi Reader ${tag}" \ | |
| --notes-file release_notes.md \ | |
| "${prerelease_flag[@]}" | |
| fi | |
| gh release upload "${tag}" ${apk_assets} LICENSE --clobber |