Integrate ASR/Omni fixes, API Route, Android IME and confirmation-based vocabulary learning. Complete settings transitions and macOS single-item credential storage. Synchronize all application versions to 2.0.0-Beta.4+build.20260930. Integrated PRs: #1116, #1120, #1121, #1123, #1124, #1067 and #1128. Linux #1117 remains independent and is excluded from automatic release. Validated: full cross-platform CI, 18 UI motion cases, 112 frontend test files, 1245 Core tests (1 ignored) and 572 macOS native tests. Co-authored-by: HKLHaoBin <we3q@qq.com>
556 lines
24 KiB
YAML
556 lines
24 KiB
YAML
name: Android APK (debug)
|
||
|
||
# Triggers:
|
||
# - push v*-tauri tag → signed release APK + minisign + updater manifest → GitHub Release
|
||
# - workflow_dispatch → signed release APK when ANDROID_KEYSTORE_* secrets exist
|
||
# (overlay install + user data preserved); otherwise unsigned debug APK (annotated, non-blocking)
|
||
# - workflow_dispatch + signed_debug=true — debug APK signed with the release
|
||
# keystore (when secrets exist); artifact remains openless-android-debug-*.
|
||
#
|
||
# #1103 build-time changes:
|
||
# - Do not wipe Cargo/Gradle/target before cache post-save (removed Free disk step).
|
||
# - Bump rust-cache prefix-key so old half-cold caches are not reused.
|
||
# - Dispatch defaults to aarch64 only; tag builds all ABIs in a parallel matrix.
|
||
# - Optional dispatch fast_profile (disable LTO / raise codegen-units); tag never uses it.
|
||
# - Tauri still runs plugin-init cargo + android-studio-script per ABI (first ABI twice).
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- 'v*-tauri'
|
||
workflow_dispatch:
|
||
inputs:
|
||
abis:
|
||
description: 'ABIs to build (comma-separated: aarch64,armv7,i686,x86_64) or all'
|
||
required: false
|
||
default: 'aarch64'
|
||
fast_profile:
|
||
description: 'Dispatch-only fast release profile (no LTO, codegen-units=16). Ignored on tags.'
|
||
type: boolean
|
||
default: false
|
||
signed_debug:
|
||
description: 'Build debug mode but sign it with the release keystore (requires ANDROID_KEYSTORE_* secrets).'
|
||
type: boolean
|
||
default: false
|
||
|
||
# Repeated pushes of the same tag run only the newest; workflow_dispatch isolates by run_id to avoid mutual cancellation.
|
||
concurrency:
|
||
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
|
||
cancel-in-progress: ${{ github.event_name == 'push' }}
|
||
|
||
jobs:
|
||
plan:
|
||
name: Plan Android ABI matrix
|
||
permissions:
|
||
contents: read
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
is_tag_release: ${{ steps.mode.outputs.is_tag_release }}
|
||
mode: ${{ steps.mode.outputs.mode }}
|
||
label: ${{ steps.mode.outputs.label }}
|
||
signing: ${{ steps.mode.outputs.signing }}
|
||
signing_label: ${{ steps.mode.outputs.signing_label }}
|
||
matrix: ${{ steps.abis.outputs.matrix }}
|
||
abi_list: ${{ steps.abis.outputs.abi_list }}
|
||
gradle_abi_list: ${{ steps.abis.outputs.gradle_abi_list }}
|
||
fast_profile: ${{ steps.abis.outputs.fast_profile }}
|
||
release_channel: ${{ steps.mode.outputs.release_channel }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
submodules: false
|
||
|
||
- name: Detect build mode
|
||
id: mode
|
||
shell: bash
|
||
env:
|
||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||
INPUT_SIGNED_DEBUG: ${{ github.event.inputs.signed_debug }}
|
||
run: |
|
||
set -euo pipefail
|
||
is_tag=false
|
||
if [[ "${{ github.ref }}" == refs/tags/v* ]] && [[ "${{ github.ref_name }}" == *-tauri ]]; then
|
||
is_tag=true
|
||
fi
|
||
echo "is_tag_release=$is_tag" >> "$GITHUB_OUTPUT"
|
||
|
||
channel=stable
|
||
if [[ "${{ github.ref_name }}" == *-beta-tauri ]] || [[ "${{ github.ref_name }}" == *-Beta.* ]]; then
|
||
channel=beta
|
||
fi
|
||
echo "release_channel=$channel" >> "$GITHUB_OUTPUT"
|
||
|
||
has_keystore=true
|
||
for name in ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do
|
||
if [ -z "${!name:-}" ]; then
|
||
has_keystore=false
|
||
break
|
||
fi
|
||
done
|
||
|
||
if $is_tag; then
|
||
echo "mode=release" >> "$GITHUB_OUTPUT"
|
||
echo "label=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||
echo "signing=tag-release" >> "$GITHUB_OUTPUT"
|
||
echo "signing_label=Tag release with release keystore" >> "$GITHUB_OUTPUT"
|
||
elif [[ "${INPUT_SIGNED_DEBUG:-false}" == "true" ]] && $has_keystore; then
|
||
echo "mode=debug" >> "$GITHUB_OUTPUT"
|
||
echo "label=signed-debug-run-${{ github.run_number }}" >> "$GITHUB_OUTPUT"
|
||
echo "signing=signed-debug" >> "$GITHUB_OUTPUT"
|
||
echo "signing_label=Manual dispatch debug APK signed with release keystore" >> "$GITHUB_OUTPUT"
|
||
elif $has_keystore; then
|
||
echo "mode=release" >> "$GITHUB_OUTPUT"
|
||
echo "label=dispatch-run-${{ github.run_number }}" >> "$GITHUB_OUTPUT"
|
||
echo "signing=dispatch-release" >> "$GITHUB_OUTPUT"
|
||
echo "signing_label=Manual dispatch with release keystore (overlay install OK)" >> "$GITHUB_OUTPUT"
|
||
else
|
||
echo "mode=debug" >> "$GITHUB_OUTPUT"
|
||
echo "label=unsigned-run-${{ github.run_number }}" >> "$GITHUB_OUTPUT"
|
||
echo "signing=debug-fallback" >> "$GITHUB_OUTPUT"
|
||
echo "signing_label=Unsigned debug fallback (no keystore secrets)" >> "$GITHUB_OUTPUT"
|
||
echo "::notice title=Unsigned debug APK::ANDROID_KEYSTORE_* secrets not configured. Building debug APK without release signing. Overlay install and user data preservation require the release keystore; uninstall before installing if replacing a signed build."
|
||
if [[ "${INPUT_SIGNED_DEBUG:-false}" == "true" ]]; then
|
||
echo "::warning::signed_debug was requested but Android keystore secrets are incomplete; falling back to an unsigned debug APK."
|
||
fi
|
||
fi
|
||
|
||
- name: Resolve ABI matrix
|
||
id: abis
|
||
shell: bash
|
||
env:
|
||
INPUT_ABIS: ${{ github.event.inputs.abis }}
|
||
INPUT_FAST_PROFILE: ${{ github.event.inputs.fast_profile }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ "${{ steps.mode.outputs.is_tag_release }}" = "true" ]; then
|
||
raw=all
|
||
fast=false
|
||
else
|
||
raw="${INPUT_ABIS:-aarch64}"
|
||
fast="${INPUT_FAST_PROFILE:-false}"
|
||
fi
|
||
matrix="$(node openless-all/app/scripts/android-abi-matrix.mjs parse "$raw")"
|
||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||
abi_list="$(node -e 'const m=JSON.parse(process.argv[1]); process.stdout.write(m.map(x=>x.abi).join(","))' "$matrix")"
|
||
gradle_list="$(node -e 'const m=JSON.parse(process.argv[1]); process.stdout.write(m.map(x=>x.gradle_abi).join(","))' "$matrix")"
|
||
echo "abi_list=$abi_list" >> "$GITHUB_OUTPUT"
|
||
echo "gradle_abi_list=$gradle_list" >> "$GITHUB_OUTPUT"
|
||
echo "fast_profile=$fast" >> "$GITHUB_OUTPUT"
|
||
echo "Resolved ABIs: $abi_list (fast_profile=$fast)"
|
||
|
||
build-android-apk:
|
||
name: Build Android (${{ matrix.abi }})
|
||
needs: plan
|
||
permissions:
|
||
contents: write
|
||
runs-on: ubuntu-latest
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include: ${{ fromJson(needs.plan.outputs.matrix) }}
|
||
env:
|
||
CI: true
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
OPENLESS_RELEASE_CHANNEL: ${{ needs.plan.outputs.release_channel }}
|
||
OPENLESS_ANDROID_TARGETS: ${{ matrix.abi }}
|
||
CARGO_TERM_COLOR: always
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
# Android does not use the local Qwen3/Whisper C submodules.
|
||
submodules: false
|
||
|
||
- name: Disable macOS-only Qwen3 MLX dependency
|
||
run: node openless-all/app/scripts/ci-disable-macos-qwen3.mjs
|
||
|
||
- name: Check updater signing availability (tag release)
|
||
if: needs.plan.outputs.is_tag_release == 'true'
|
||
shell: bash
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
run: |
|
||
if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||
echo "::error::TAURI_SIGNING_PRIVATE_KEY is required for signed Android release artifacts."
|
||
exit 1
|
||
fi
|
||
|
||
- name: Check Android keystore secrets (tag release)
|
||
if: needs.plan.outputs.is_tag_release == 'true'
|
||
shell: bash
|
||
env:
|
||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||
run: |
|
||
missing=()
|
||
for name in ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do
|
||
if [ -z "${!name:-}" ]; then
|
||
missing+=("$name")
|
||
fi
|
||
done
|
||
if [ "${#missing[@]}" -gt 0 ]; then
|
||
echo "::error::Android release signing secrets are required for tag builds: ${missing[*]}"
|
||
echo "::error::Configure ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD in repo secrets."
|
||
exit 1
|
||
fi
|
||
|
||
- name: Apply dispatch fast profile
|
||
if: needs.plan.outputs.fast_profile == 'true' && needs.plan.outputs.is_tag_release != 'true'
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
# Override [profile.release] for this job only; tag builds never take this path.
|
||
echo "CARGO_PROFILE_RELEASE_LTO=false" >> "$GITHUB_ENV"
|
||
echo "CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16" >> "$GITHUB_ENV"
|
||
echo "::notice title=Fast profile::Dispatch fast_profile enabled (LTO off, codegen-units=16)"
|
||
|
||
- name: Setup Java 17 (Zulu)
|
||
uses: actions/setup-java@v4
|
||
with:
|
||
distribution: zulu
|
||
java-version: '17'
|
||
|
||
- name: Setup Android SDK
|
||
uses: android-actions/setup-android@v3
|
||
with:
|
||
packages: platform-tools
|
||
|
||
- name: Install NDK and accept SDK licenses
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
sdkmanager "ndk;26.1.10909125"
|
||
ndk_dir="$ANDROID_HOME/ndk/26.1.10909125"
|
||
if [ ! -d "$ndk_dir" ]; then
|
||
echo "::error::NDK not found at $ndk_dir"
|
||
exit 1
|
||
fi
|
||
echo "ANDROID_NDK_HOME=$ndk_dir" >> "$GITHUB_ENV"
|
||
echo "NDK_HOME=$ndk_dir" >> "$GITHUB_ENV"
|
||
set +o pipefail
|
||
yes | sdkmanager --licenses
|
||
sdkmanager_exit=${PIPESTATUS[1]}
|
||
set -o pipefail
|
||
if [ "$sdkmanager_exit" -ne 0 ]; then
|
||
echo "::error::sdkmanager --licenses failed with exit code $sdkmanager_exit"
|
||
exit "$sdkmanager_exit"
|
||
fi
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "22"
|
||
cache: npm
|
||
cache-dependency-path: openless-all/app/package-lock.json
|
||
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
with:
|
||
targets: ${{ matrix.rust_target }}
|
||
|
||
- name: Cache Cargo
|
||
id: rust-cache
|
||
uses: swatinem/rust-cache@v2
|
||
with:
|
||
# #1103: bump prefix so previously truncated / half-cold caches are not reused.
|
||
prefix-key: v1-rust-android-1103
|
||
shared-key: android-${{ matrix.abi }}
|
||
workspaces: openless-all/app/src-tauri -> target
|
||
cache-on-failure: true
|
||
|
||
- uses: gradle/actions/setup-gradle@v4
|
||
with:
|
||
# Keep Gradle caches for post-job save; do not wipe ~/.gradle before post.
|
||
cache-read-only: false
|
||
|
||
- name: Install npm deps
|
||
working-directory: openless-all/app
|
||
run: npm ci
|
||
|
||
# Frontend is built by tauri beforeBuildCommand; skip a duplicate standalone build (#1103).
|
||
|
||
- name: Initialize Android project
|
||
working-directory: openless-all/app
|
||
run: npm run tauri -- android init --ci
|
||
|
||
- name: Copy Android scaffolding (Kotlin + XML)
|
||
working-directory: openless-all/app
|
||
run: node scripts/copy-android-scaffolding.mjs
|
||
|
||
- name: Merge APK v1 manifest permissions
|
||
working-directory: openless-all/app
|
||
run: node scripts/merge-android-v1-manifest.mjs
|
||
|
||
- name: Merge overlay / accessibility manifest
|
||
working-directory: openless-all/app
|
||
run: node scripts/merge-android-overlay-manifest.mjs
|
||
|
||
- name: Merge Shizuku manifest
|
||
working-directory: openless-all/app
|
||
run: node scripts/merge-android-shizuku-manifest.mjs
|
||
|
||
- name: Patch Shizuku Gradle dependencies
|
||
working-directory: openless-all/app
|
||
run: node scripts/patch-android-shizuku-deps.mjs
|
||
|
||
- name: Merge updater / install manifest
|
||
working-directory: openless-all/app
|
||
run: node scripts/merge-android-updater-manifest.mjs
|
||
|
||
- name: Configure Android release signing
|
||
if: needs.plan.outputs.mode == 'release' || needs.plan.outputs.signing == 'signed-debug'
|
||
working-directory: openless-all/app
|
||
env:
|
||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||
# signed_debug: also wire debug buildType to the release keystore
|
||
OPENLESS_SIGN_DEBUG: ${{ needs.plan.outputs.signing == 'signed-debug' && 'true' || 'false' }}
|
||
run: node scripts/configure-android-release-signing.mjs
|
||
|
||
- name: Prime Gradle wrapper
|
||
working-directory: openless-all/app
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
for attempt in 1 2 3; do
|
||
if src-tauri/gen/android/gradlew --project-dir src-tauri/gen/android --version --no-daemon; then
|
||
exit 0
|
||
fi
|
||
if [ "$attempt" -lt 3 ]; then
|
||
sleep $([ "$attempt" -eq 1 ] && echo 15 || echo 30)
|
||
fi
|
||
done
|
||
exit 1
|
||
|
||
- name: Build Android debug APK
|
||
if: needs.plan.outputs.mode == 'debug'
|
||
working-directory: openless-all/app
|
||
env:
|
||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||
run: npm run tauri:android:build:debug
|
||
|
||
- name: Build Android release APK
|
||
if: needs.plan.outputs.mode == 'release'
|
||
working-directory: openless-all/app
|
||
env:
|
||
# Resolve signing credentials at execution time; compiled Gradle scripts
|
||
# are cached and must never contain secret literals.
|
||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||
run: npm run tauri:android:build:release
|
||
|
||
- name: Record cache / duplicate-compile notes
|
||
if: always()
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
{
|
||
echo "### Cache / compile notes (\`${{ matrix.abi }}\`)"
|
||
echo
|
||
echo "- Rust cache hit: \`${{ steps.rust-cache.outputs.cache-hit }}\`"
|
||
echo "- Tauri runs plugin-init cargo for the first ABI, then \`android-studio-script\` per ABI (first ABI may compile twice)."
|
||
echo "- Do not wipe \`target\` / Cargo registry / Gradle caches before action post-save (#1103)."
|
||
} >> "$GITHUB_STEP_SUMMARY"
|
||
- name: Collect split APKs
|
||
id: apk
|
||
shell: bash
|
||
working-directory: openless-all/app
|
||
env:
|
||
OPENLESS_APK_MODE: ${{ needs.plan.outputs.mode }}
|
||
OPENLESS_APK_LABEL: ${{ needs.plan.outputs.label }}
|
||
OPENLESS_EXPECTED_GRADLE_ABIS: ${{ matrix.gradle_abi }}
|
||
run: node scripts/collect-android-split-apks.mjs
|
||
|
||
- name: Upload Android APK artifact
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: ${{ needs.plan.outputs.mode == 'release' && format('openless-android-release-{0}', matrix.gradle_abi) || format('openless-android-debug-{0}', matrix.gradle_abi) }}
|
||
path: ${{ steps.apk.outputs.apk_path }}
|
||
if-no-files-found: error
|
||
|
||
- name: Write build summary
|
||
if: always()
|
||
run: |
|
||
cat >> "$GITHUB_STEP_SUMMARY" << EOF
|
||
### Android APK build (\`${{ matrix.abi }}\`)
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| Gradle mode | \`${{ needs.plan.outputs.mode }}\` |
|
||
| Signing | ${{ needs.plan.outputs.signing_label }} |
|
||
| Artifact label | \`${{ needs.plan.outputs.label }}\` |
|
||
| Fast profile | \`${{ needs.plan.outputs.fast_profile }}\` |
|
||
| Rust cache hit | \`${{ steps.rust-cache.outputs.cache-hit }}\` |
|
||
|
||
EOF
|
||
if [ "${{ needs.plan.outputs.signing }}" = "debug-fallback" ]; then
|
||
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
|
||
> **Unsigned debug APK.** Debug builds use a CI-only signature. Use `adb install -r` only when replacing the same debug build. To upgrade from a signed release without losing user data, configure `ANDROID_KEYSTORE_*` repo secrets and re-run this workflow.
|
||
EOF
|
||
elif [ "${{ needs.plan.outputs.signing }}" = "dispatch-release" ]; then
|
||
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
|
||
> **Signed release APK** (manual dispatch). Same keystore as tag releases — supports overlay install and preserves user data when upgrading from an existing signed install.
|
||
EOF
|
||
elif [ "${{ needs.plan.outputs.signing }}" = "signed-debug" ]; then
|
||
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
|
||
> **Signed debug APK** (manual dispatch). Debug mode is retained for diagnostics; the release keystore is used so `adb install -r` can upgrade the matching signed app without changing the debug artifact name.
|
||
EOF
|
||
fi
|
||
|
||
publish-android-release:
|
||
name: Publish Android release assets
|
||
if: needs.plan.outputs.is_tag_release == 'true'
|
||
needs: [plan, build-android-apk]
|
||
permissions:
|
||
contents: write
|
||
runs-on: ubuntu-latest
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
OPENLESS_RELEASE_CHANNEL: ${{ needs.plan.outputs.release_channel }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
submodules: false
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "22"
|
||
cache: npm
|
||
cache-dependency-path: openless-all/app/package-lock.json
|
||
|
||
- name: Install npm deps
|
||
working-directory: openless-all/app
|
||
run: npm ci
|
||
|
||
- name: Download all ABI artifacts
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
pattern: openless-android-release-*
|
||
path: ${{ runner.temp }}/android-release-apks
|
||
merge-multiple: true
|
||
|
||
- name: Verify full ABI set and stage files
|
||
id: stage
|
||
shell: bash
|
||
working-directory: openless-all/app
|
||
env:
|
||
OPENLESS_STAGE_DIR: ${{ runner.temp }}/android-release-apks
|
||
run: |
|
||
set -euo pipefail
|
||
version="$(node -p "require('./package.json').version")"
|
||
out_dir="${{ runner.temp }}/openless-android-release-split"
|
||
mkdir -p "$out_dir"
|
||
expected=(arm64-v8a armeabi-v7a x86 x86_64)
|
||
missing=()
|
||
for abi in "${expected[@]}"; do
|
||
src="$OPENLESS_STAGE_DIR/OpenLess_${version}_${abi}.apk"
|
||
if [ ! -f "$src" ]; then
|
||
# download-artifact may nest by artifact name
|
||
src="$(find "$OPENLESS_STAGE_DIR" -type f -name "OpenLess_${version}_${abi}.apk" | head -n 1 || true)"
|
||
fi
|
||
if [ -z "${src:-}" ] || [ ! -f "$src" ]; then
|
||
missing+=("$abi")
|
||
continue
|
||
fi
|
||
cp -f "$src" "$out_dir/OpenLess_${version}_${abi}.apk"
|
||
echo "Staged $abi from $src"
|
||
done
|
||
if [ "${#missing[@]}" -gt 0 ]; then
|
||
echo "::error::Tag release missing ABI APK(s): ${missing[*]}"
|
||
find "$OPENLESS_STAGE_DIR" -type f -name '*.apk' -print || true
|
||
exit 1
|
||
fi
|
||
echo "out_dir=$out_dir" >> "$GITHUB_OUTPUT"
|
||
{
|
||
echo "release_files<<EOF"
|
||
for abi in "${expected[@]}"; do
|
||
echo "$out_dir/OpenLess_${version}_${abi}.apk"
|
||
done
|
||
echo "EOF"
|
||
} >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Sign release APKs (minisign)
|
||
working-directory: openless-all/app
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
run: |
|
||
set -euo pipefail
|
||
out_dir="${{ steps.stage.outputs.out_dir }}"
|
||
version="$(node -p "require('./package.json').version")"
|
||
node scripts/sign-android-apks.mjs \
|
||
"$out_dir/OpenLess_${version}_arm64-v8a.apk" \
|
||
"$out_dir/OpenLess_${version}_armeabi-v7a.apk" \
|
||
"$out_dir/OpenLess_${version}_x86.apk" \
|
||
"$out_dir/OpenLess_${version}_x86_64.apk"
|
||
|
||
- name: Write Android updater manifests
|
||
working-directory: openless-all/app
|
||
env:
|
||
OPENLESS_UPDATE_APK_DIR: ${{ steps.stage.outputs.out_dir }}
|
||
OPENLESS_UPDATE_REPO: Open-Less/openless
|
||
OPENLESS_UPDATE_MIRROR_BASE_URL: https://fastgit.cc/https://github.com
|
||
OPENLESS_RELEASE_CHANNEL: ${{ env.OPENLESS_RELEASE_CHANNEL }}
|
||
OPENLESS_RELEASE_TAG: ${{ github.ref_name }}
|
||
run: |
|
||
set -euo pipefail
|
||
for arch in aarch64 armv7 i686 x86_64; do
|
||
OPENLESS_UPDATE_TARGET=android OPENLESS_UPDATE_ARCH="$arch" \
|
||
node scripts/write-updater-manifest.mjs
|
||
done
|
||
|
||
- name: Append release files (manifests + signatures)
|
||
id: release_assets
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
out_dir="${{ steps.stage.outputs.out_dir }}"
|
||
{
|
||
echo "${{ steps.stage.outputs.release_files }}"
|
||
for f in "$out_dir"/*.apk.sig "$out_dir"/latest-android-*.json; do
|
||
[ -e "$f" ] && echo "$f"
|
||
done
|
||
} > "$RUNNER_TEMP/android-release-files.txt"
|
||
echo "files<<EOF" >> "$GITHUB_OUTPUT"
|
||
cat "$RUNNER_TEMP/android-release-files.txt" >> "$GITHUB_OUTPUT"
|
||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Prepare Android release body
|
||
shell: bash
|
||
run: |
|
||
cat > "$RUNNER_TEMP/android-release-body.md" << 'EOF'
|
||
### Android 安装说明
|
||
|
||
- **推荐**:真机下载 `OpenLess_<version>_arm64-v8a.apk`(arm64 设备)。
|
||
- **模拟器**:x86_64 模拟器用 `OpenLess_<version>_x86_64.apk`。
|
||
- 首次安装需在系统设置中允许「安装未知应用」;应用内更新会拉起系统安装器。
|
||
- adb 调试:`adb install -r OpenLess_<version>_arm64-v8a.apk`
|
||
|
||
### Android 应用内更新
|
||
|
||
- Stable 用户检查 `latest-android-aarch64.json`(及 mirror 变体)。
|
||
- Beta 用户(设置 → 高级 → 加入 Beta)检查带 `-beta` 后缀的 manifest。
|
||
EOF
|
||
|
||
- name: Attach Android assets to GitHub Release
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
tag_name: ${{ github.ref_name }}
|
||
name: OpenLess ${{ github.ref_name }}
|
||
# The desktop and Android jobs share one Beta draft. Publish it only
|
||
# after all jobs and package verification have finished.
|
||
draft: ${{ env.OPENLESS_RELEASE_CHANNEL == 'beta' }}
|
||
prerelease: ${{ env.OPENLESS_RELEASE_CHANNEL == 'beta' }}
|
||
append_body: true
|
||
body_path: ${{ runner.temp }}/android-release-body.md
|
||
files: ${{ steps.release_assets.outputs.files }}
|