Files
TRIPandHKLHaoBin 6f9af89583 release: OpenLess 2.0.0-Beta.4+build.20260930 (#1126)
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>
2026-09-30 20:32:53 +08:00

556 lines
24 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }}