从零搭建一个 Tauri NextJS Android 项目
Tauri 简介
Tauri 是一个轻量、高性能的跨平台应用开发框架,允许使用 Web 技术(如 HTML、JavaScript、CSS)构建 UI,同时以 Rust、Swift 或 Kotlin 编写后端逻辑,生成适用于主流桌面和移动平台的本地应用程序。
目前 Tauri 已经发布 V2 版本,支持包括 Android、iOS 在内的移动平台。
Tauri 的主要优势在于构建体积小、技术选型灵活,以及基于 Rust 构建所带来的高性能与可靠性:
- 轻量:Tauri 没有像 Electron 那样将完整的 Chromium 打包进应用中,而是使用系统原生的 WebView,因此生成的应用体积非常小。
- 灵活的前端框架选择:它不依赖特定的前端技术栈,开发者可以自由选择 React、Vue、Svelte、Next.js 等任意框架。
- 高性能与安全性:底层采用 Rust 编写,具备出色的运行性能和更强的内存安全保障。
当然,Tauri 也有一些需要注意的限制:
- 平台兼容性差异:由于依赖系统 WebView,开发者需要手动处理各平台之间在 WebView 实现上的差异,特别是在 Android 和旧版 Windows 上。
- Rust 学习成本:虽然可以仅编写前端,但如果希望更深入地控制应用行为、扩展系统功能或编写后端逻辑,仍然需要具备一定的 Rust 编程能力。
可以参考 Tauri 官方提供的前置条件部分来完成初步的项目环境搭建。
项目初始化
Tauri 官方有提供有 cli 来帮助创建项目,具体可以查看官方的创建项目。
这里以 pnpm
为例。
$ pnpm create tauri-app
✔ Project name · tauri-android-tutorial
✔ Identifier · com.example.app
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, deno, bun)
✔ Choose your package manager · pnpm
✔ Choose your UI template · Vanilla
✔ Choose your UI flavor · TypeScript
运行 pnpm create tauri-app
命令后,会进入交互式初始化流程,依次配置即可。
配置项说明:
- Project name
项目名称,将作为项目目录名和默认包名。这里填写tauri-android-tutorial
。 - Identifier
应用标识符,建议使用反转域名格式(如com.example.app
),未来用于 Android 包名等场景。 - Frontend language
前端开发语言,推荐选择 TypeScript / JavaScript,后续我们会引入 Next.js。 - Package manager
包管理器选择,本文统一使用pnpm
,也可根据个人偏好选择yarn
或npm
。 - UI template
初始前端模板,选择Vanilla
意味着不使用任何框架(如 React/Vue),方便我们后续自行集成 Next.js。 - UI flavor
选择使用的语言风格,推荐使用TypeScript
,更有利于类型安全和开发效率。
配置好后,命令行会输出内容来告诉我们接下来该怎么做。
进入项目文件夹,安装依赖,初始化 Android 配置,如果是桌面开发就使用 pnpm tauri dev
如果是 Android 开发就使用 pnpm tauri android dev
。
Template created! To get started run:
cd tauri-android-tutorial
pnpm install
pnpm tauri android init
For Desktop development, run:
pnpm tauri dev
For Android development, run:
pnpm tauri android dev
Tauri 虽然也支持 iOS,但由于苹果官方政策限制,iOS 应用开发必须在 macOS 上进行,并依赖 Xcode 工具链。因此,如果你当前使用的是 Windows 或 Linux 开发环境,暂时无法进行 iOS 构建和调试。
尝试构建
完成依赖安装和 Android 初始化之后,就可以尝试构建 Android 应用了。
在项目根目录下运行 pnpm tauri android build
。
首次执行构建命令时,系统可能会下载缺失的构建依赖,因此耗时较长是正常的,请耐心等待。
构建完成后,命令行会输出构建产物的路径信息,例如:
...
Finished 1 APK at:
E:\tauri-android-tutorial\src-tauri\gen/android\app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk
Finished 1 AAB at:
E:\tauri-android-tutorial\src-tauri\gen/android\app/build/outputs/bundle/universalRelease/app-universal-release.aab
可以看见,有 APK 和 AAB 两种格式,APK 是 Android 安装包,可直接安装到真机或模拟器上,用于调试与内部测试,AAB 则是 Android App Bundle,适用于发布到 Google Play 商店,由 Google 进行按需拆包与分发。
APK 签名
在完成构建之后,可以尝试将构建的 APK 文件安装到手机上。
然后就会看见安装失败,这是因为没有签名导致的。
这是因为 Android 系统要求所有 APK 必须先使用证书进行数字签名,然后才能安装到设备上或进行更新。
所以我们需要对构建产物进行签名。
需要注意,Google Play 以第一次上传的签名为准,要求后续上传的文件签名一致。
生成签名
keytool
是 Java 数据证书管理工具。
可以在 Android Studio 安装目录下的 /jbr/bin/
目录中找到 keytool.exe
。
keytool -genkey -v -keystore 自定义的数据文件名称 -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias 自定义的证书别名
之后按照提示进行输入即可。
如果是要上架应用商店的,要如实填写。否则可能会导致因信息不对从而上架失败。
输入完毕,确认后,工具会要求设置密码。记住密码,后面会用到。
完成以上操作后,在当前工作目录下会生成一个 自定义的数据文件名称.keystore
文件。
构建时签名
创建 src-tauri/gen/android/keystore.properties
文件。
文件内容如下:
storePassword=数据文件密码
keyPassword=证书密码
keyAlias=自定义的证书别名
storeFile=自定义的数据文件名称.keystore
需要注意,在 windows 下,storeFile 路径需要 C:\\Program Files\\Android
这样的格式。
随后找到 src-tauri/gen/android/app/build.gradle.kts
文件。
增加以下内容:
import java.io.FileInputStream
// ...
signingConfigs {
create("release") {
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = File(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
// ...
// ...
signingConfig = signingConfigs.getByName("release")
配置完毕之后,重新打包,APK 安装成功。
集成 NextJS
本文使用的是 NextJS 的 App Router。
Tauri 要求前端资源必须是可导出的静态 HTML,因此我们需要使用 NextJS 提供的静态导出功能。
执行 pnpm add next@latest react@latest react-dom@latest
安装 NextJS,因为使用了 TS,因此需要还需要执行 pnpm add -D @types/react @types/react-dom
命令来安装 React 的类型定义。
接着修改 package.json
的 scripts
部分。
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"tauri": "tauri"
},
创建并修改 next.config.ts
文件。
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
output: "export",
images: {
unoptimized: true
}
}
export default nextConfig
接着创建 src/app
目录,和 src/app/layout.tsx
和 src/app/page.tsx
两个文件。
// src/app/layout.tsx
export default function rootLayout({ children }: React.PropsWithChildren) {
return (
<html>
<head></head>
<body>
{children}
</body>
</html>
)
}
// src/app/page.tsx
export default function Home() {
return (
<div>
Home
</div>
)
}
修改 tauri.conf.json
文件中的 build
部分。
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../out"
},
随后执行 pnpm tauri dev
来运行项目。
多语言配置
NextJS 官方是有多语言支持的,但要求是通过 NextJS 运行项目,而 Tauri 要求前端资源必须是可导出的静态 HTML,因此需要我们手动实现多语言功能。
安装依赖 pnpm add i18next i18next-resources-to-backend react-i18next
。
多语言配置代码
// i18n.ts
import { createInstance, KeyPrefix } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { initReactI18next } from 'react-i18next/initReactI18next'
/**
* 支持语言列表
*/
export const SUPPORT_LANGUAGES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pt', 'ar', 'ja'] as const satisfies string[]
export type SUPPORT_LANGUAGES = typeof SUPPORT_LANGUAGES
/**
* 支持的语言
*/
export type SUPPORT_LANGUAGE = SUPPORT_LANGUAGES[number]
/**
* 语言对应全称
*/
export const SUPPORT_LANGUAGES_FULL_NAME = {
en: 'English',
de: 'Deutsch',
fr: 'Français',
es: 'Español',
it: 'Italiano',
nl: 'Netherlands',
pt: 'Português',
ar: 'عربى',
ja: 'Japan',
} as const satisfies Record<SUPPORT_LANGUAGE, string>
export type SUPPORT_LANGUAGES_FULL_NAME = typeof SUPPORT_LANGUAGES_FULL_NAME
/**
* 右到左方向排列语言列表
*/
export const RTL_LANGUAGES: Array<SUPPORT_LANGUAGE> = ['ar']
/**
* 语言资源命名空间列表
*/
export const LANGUAGE_NAMESPACES = ['common', 'home'] as const satisfies string[]
export type LANGUAGE_NAMESPACES = typeof LANGUAGE_NAMESPACES
/**
* 语言资源命名空间
*/
export type LANGUAGE_NAMESPACE = LANGUAGE_NAMESPACES[number]
/**
* 默认语言
*/
export const DEFAULT_LANGUAGE: SUPPORT_LANGUAGE = 'en'
/**
* 默认命名空间
*/
export const DEFAULT_NAMESPACE: LANGUAGE_NAMESPACE = 'common'
export type withLocaleProps = { locale: SUPPORT_LANGUAGE }
const initI18next = async (lng: SUPPORT_LANGUAGE, ns: LANGUAGE_NAMESPACE) => {
const i18nInstance = createInstance()
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend<SUPPORT_LANGUAGE, LANGUAGE_NAMESPACE>(
(lng: SUPPORT_LANGUAGE, ns: LANGUAGE_NAMESPACE) => import(`#/locales/${lng}/${ns}.json`),
),
)
.init({
supportedLngs: SUPPORT_LANGUAGES,
fallbackLng: DEFAULT_LANGUAGE,
lng,
fallbackNS: DEFAULT_NAMESPACE,
defaultNS: DEFAULT_NAMESPACE,
ns,
})
return i18nInstance
}
export async function createTranslation(
lng: SUPPORT_LANGUAGE,
ns: LANGUAGE_NAMESPACE,
options?: KeyPrefix<LANGUAGE_NAMESPACE>,
) {
const i18nextInstance = await initI18next(lng, ns)
return {
t: i18nextInstance.getFixedT<LANGUAGE_NAMESPACE, string>(lng, Array.isArray(ns) ? ns[0] : ns, options),
i18n: i18nextInstance,
}
}
在页面中使用
// src/app/[locale]/page.tsx
import style from './page.module.scss'
import { createTranslation, DEFAULT_LANGUAGE, SUPPORT_LANGUAGES, type withLocaleProps } from '@/lib/i18n'
export async function generateStaticParams() {
return SUPPORT_LANGUAGES.map((locale) => ({ locale }))
}
export default async function Home({ params }: { params: Promise<withLocaleProps> }) {
const locale = (await params).locale ?? DEFAULT_LANGUAGE
const { t } = await createTranslation(locale, 'home')
return (
<div className={style.container}>
<h1>{t('home-title')}</h1>
</div>
)
}
使用 github Action 进行自动化构建
完成以上步骤后,一个简单的 Tauri Android 项目就搭建好了。
在开发完毕后手动构建就行。
但每次开发后手动构建有点影响效率。
因此我们需要自动化构建。
我们可以使用 github 提供的 Action。
首先创建以下环境变量:
- BASE64_JKS
- Base64 编码后的签名文件
- KEY_ALIAS
keystore.properties
文件中的 keyAlias
- KEY_PASSWORD
keystore.properties
文件中的 keyPassword
- NDK_VERSION
- NDK 版本,最新的 LTS 版本是 r27c,最新稳定版是 r28b。
- STORE_FILE
keystore.properties
文件中的 storeFile- 需要注意 linux 环境和 windows 环境中路径的差异。
- STORE_PASSWORD
keystore.properties
文件中的 storePassword
以下是配置:
# .github/workflows/build.yml
name: Build APK
description: Build APK File
on:
workflow_dispatch:
push:
tags:
- v*
jobs:
build-APK:
permissions:
contents: write
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install deps (ubuntu only)
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: "aarch64-linux-android x86_64-linux-android armv7-linux-androideabi i686-linux-android"
- name: Install Android NDK
id: ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: ${{ secrets.NDK_VERSION }}
- name: Install front deps
run: pnpm install
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: Cache node_modules
uses: actions/cache@v4
with:
path: '~/.pnpm-store'
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/pnpm-lock.json') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Set environment variables for Android NDK
run: |
echo "NDK_HOME=${{steps.ndk.outputs.ndk-path}}" >> $GITHUB_ENV
echo "SDK_ROOT=$HOME/android-sdk" >> $GITHUB_ENV
- name: Create keystore.properties and jks file
env:
storeFile: ${{ secrets.STORE_FILE }}
storePassword: ${{ secrets.STORE_PASSWORD }}
keyAlias: ${{ secrets.KEY_ALIAS }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
jksBase64: ${{ secrets.BASE64_JKS }}
run: |
resolvedStoreFile="${storeFile/#\~/$HOME}"
mkdir -p "$(dirname $"$resolvedStoreFile")"
echo "storeFile=$resolvedStoreFile" > ${{github.workspace}}/src-tauri/gen/android/keystore.properties
echo "storePassword=$storePassword" >> ${{github.workspace}}/src-tauri/gen/android/keystore.properties
echo "keyAlias=$keyAlias" >> ${{github.workspace}}/src-tauri/gen/android/keystore.properties
echo "keyPassword=$keyPassword" >> ${{github.workspace}}/src-tauri/gen/android/keystore.properties
echo "$jksBase64" | base64 --decode > "$resolvedStoreFile"
- name: Build App Bundle
run: pnpm tauri android build
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: App Bundle ${{ github.ref_name }}
path: |
${{ github.workspace }}/src-tauri/gen/android/app/build/outputs/bundle/universalRelease/app-universal-release.aab
${{ github.workspace }}/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
当发布一个 v 开头的 tag,如 v1.2.3 时,这个 Action会自动触发,并进行构建。
访问该 action 界面,在下方可以下载构建产物(前提是构建成功)。
使用 github Action 进行自动管理版本
每次都需要手动修改版本并打 tag 还是有点繁琐了。
所以需要将手动的版本管理改为自动版本管理。
所以需要再次编写一个Action。
# .github/workflows/release.yml
name: Release App Bundle
description: Release App Bundle
on:
workflow_dispatch:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
issues: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Release Please
id: release
uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
// .release-please-manifest.json
{
".": "0.0.1"
}
// release-please-config.json
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"sequential-calls": true,
"plugins": [
"cargo-workspace"
],
"pull-request-title-pattern": "release version: v1.9.0",
"include-component-in-tag": false,
"packages": {
".": {
"changelog-path": "CHANGELOG.md",
"release-type": "node",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"draft": false,
"prerelease": false,
"extra-files": [
{
"type": "json",
"path": "src-tauri/tauri.conf.json",
"jsonpath": "$.version"
}
]
}
}
}
配置完毕后,当我们按照对应规范进行 commit 后,就会触发 Action 帮助我们进行版本管理。
具体 commit 规范可以参考这个网址。
引入 Git Commit 规范
完成以上步骤后,当开发完毕进行 commit 时,Action 会根据 commit 规范来提取 commit 信息构建 CHANGELOG,但 commit 内容这依赖于开发者的自觉。
因此我们需要引入限制,保证 commit 规范。
安装对应依赖 pnpm add commitizen cz-custom izablecz-conventional-changelog husky -D
。
修改 package.json
,在 scripts
部分新增 "prepare": "husky"
。
// .cz-config.json
{
"types": [
{ "value": "feat", "name": "特性: 一个新的特性" },
{ "value": "fix", "name": "修复: 修复一个Bug" },
{ "value": "docs", "name": "文档: 变更的只有文档" },
{ "value": "style", "name": "格式: 空格, 分号等格式修复" },
{ "value": "refactor", "name": "重构: 代码重构,注意和特性、修复区分开" },
{ "value": "perf", "name": "性能: 提升性能" },
{ "value": "test", "name": "测试: 添加一个测试" },
{ "value": "revert", "name": "回滚: 代码回退" },
{ "value": "chore", "name": "工具:开发工具变动(构建、脚手架工具等)" },
{ "value": "merge", "name": "合并:合并代码" },
{ "value": "build", "name": "打包: 打包发布" },
{ "value": "ci", "name": "集成: 持续集成" },
{ "value": "release", "name": "发布: 发布新版本" },
{ "value": "other", "name": "其他: 其他改动,比如构建流程, 依赖管理" },
],
"messages": {
"type": "选择提交类型:",
"customScope": "修改范围(可选):",
"subject": "短说明:",
"body": "长说明,使用\"|\"换行(可选):",
"footer": "关联关闭的issue,例如:#31, #34(可选):",
"confirmCommit": "确定提交说明?"
},
"allowCustomScopes": true,
"allowBreakingChanges": ["feat"],
"subjectLimit": 100
}
// commitlint.config.cjs
const czConfig = require("./.cz-config.json")
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
// type 类型定义
"type-enum": [2, "always", czConfig.types.map((item) => item.value)],
// subject 大小写不做校验
// 自动部署的BUILD ROBOT的commit信息大写,以作区别
"subject-case": [0],
},
}
// .czrc
{
"path": "cz-customizable",
"config": ".cz-config.cjs"
}
# .husky/commit-msg
#!/bin/sh
# 定义颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # 无色(重置颜色)
# 执行 commitlint 校验
if ! npx commitlint -e "$GIT_PARAMS"; then
echo ""
echo -e "${RED}🚫 提交信息校验失败!${NC}"
echo -e "${YELLOW}请按照规范书写提交信息,例如:${NC}"
echo -e " ${CYAN}feat(front): 新增用户登录功能${NC}"
echo -e " ${CYAN}fix(styles): 修复登录页面样式问题${NC}"
echo ""
echo -e "${GREEN}👉 建议使用:npm run commit 以规范提交${NC}"
exit 1
fi
修改 package.json
的 scripts
部分,新增 "commit": "git-cz"
部分。
执行 pnpm commit
即可进入交互式 commit 流程。
需要 git 暂存区有暂存文件!
使用 github Action 进行自动化发布版本
将构建 APK 和版本管理两个 Action 合并。
当提交到主分支时,会触发版本管理步骤,会创建一个版本更改的 PR。
当合并版本更改 PR 后,会触发构建步骤,自动将构建产物上传到版本发布 release 中。
以下是具体配置:
name: Release App Bundle
description: Build App Bundle And Release
on:
workflow_dispatch:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
issues: write
jobs:
release:
environment: prerelease
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- name: Release Please
id: release
uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
build-APK:
environment: prerelease
needs: release
if: ${{ needs.release.outputs.release_created == 'true' }}
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install deps (ubuntu only)
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: "aarch64-linux-android x86_64-linux-android armv7-linux-androideabi i686-linux-android"
- name: Install Android NDK
id: ndk
uses: nttld/setup-ndk@v1
with:
ndk-version: ${{ secrets.NDK_VERSION }}
- name: Install front deps
run: pnpm install
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'
- name: Cache node_modules
uses: actions/cache@v4
with:
path: '~/.pnpm-store'
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/pnpm-lock.json') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Set environment variables for Android NDK
run: |
echo "NDK_HOME=${{steps.ndk.outputs.ndk-path}}" >> $GITHUB_ENV
echo "SDK_ROOT=$HOME/android-sdk" >> $GITHUB_ENV
- name: Create keystore.properties and jks file
env:
storeFile: ${{ secrets.STORE_FILE }}
storePassword: ${{ secrets.STORE_PASSWORD }}
keyAlias: ${{ secrets.KEY_ALIAS }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
jksBase64: ${{ secrets.BASE64_JKS }}
run: |
resolvedStoreFile="${storeFile/#\~/$HOME}"
mkdir -p "$(dirname $"$resolvedStoreFile")"
echo "storeFile=$resolvedStoreFile" > ${{github.workspace}}/src-tauri/gen/android/keystore.properties
echo "storePassword=$storePassword" >> ${{github.workspace}}/src-tauri/gen/android/keystore.properties
echo "keyAlias=$keyAlias" >> ${{github.workspace}}/src-tauri/gen/android/keystore.properties
echo "keyPassword=$keyPassword" >> ${{github.workspace}}/src-tauri/gen/android/keystore.properties
echo "$jksBase64" | base64 --decode > "$resolvedStoreFile"
- name: Build APK
run: pnpm tauri android build --apk
- name: Build AAB
run: pnpm tauri android build --aab
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: App Bundle ${{ github.ref_name }}
path: |
${{ github.workspace }}/src-tauri/gen/android/app/build/outputs/bundle/universalRelease/app-universal-release.aab
${{ github.workspace }}/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
- name: Upload Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ needs.release.outputs.tag_name }} \
${{ github.workspace }}/src-tauri/gen/android/app/build/outputs/bundle/universalRelease/app-universal-release.aab \
${{ github.workspace }}/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk \
--clobber \
--repo "$GITHUB_REPOSITORY"