Tauri应用,通过github自动通知应用升级流程详解
来实现,充分发挥 github 的能力(简单来说,就是不需要借助其他第三方平台或服务就可以实现整个应用的自动化发布更新)。如果不想借助 github 打包和静态资源存放,也可以参考上面的步骤,自行部署。Action 配置请参考之前的文章【Tauri 入门篇 - 跨平台编译】,此处新增环境设置签名和静态资源推送。文件,通知用户更新注意事项,格式如下(使用版本号作为标题,具体请查看。的工作流来实现,所
签名
Tauri 通过签名来保证安全更新应用。 签名更新应用需要做两件事:
- 私钥 (privkey) 用于签署应用的更新,必须严密保存。此外,如果丢失了此密钥,将无法向当前用户群发布新的更新,将其保存在安全的地方至关重要。
- 在
tauri.conf.json
中添加公钥 (pubkey),以在安装前验证更新存档。
生成签名
使用 Tauri CLI 提供的命令可以生成密钥(.pub
后缀的文件为公钥):
tauri signer generate -w ~/.tauri/omb.key
$ tauri signer generate -w /Users/lencx/.tauri/omb.key
Generating new private key without password.
Please enter a password to protect the secret key.
Password:
Password (one more time):
Deriving a key from the password in order to encrypt the secret key... done
Your keypair was generated successfully
Private: /Users/lencx/.tauri/omb.key (Keep it secret!)
Public: /Users/lencx/.tauri/omb.key.pub
---------------------------
Environment variabled used to sign:
`TAURI_PRIVATE_KEY` Path or String of your private key
`TAURI_KEY_PASSWORD` Your private key password (optional)
ATTENTION: If you lose your private key OR password, you'll not be able to sign your update package and updates will not works.
---------------------------
✨ Done in 39.09s.
⚠️ 注意:如果丢失了私钥或密码,将无法签署更新包并且更新将无法正常工作(请妥善保管)。
tauri.conf.json 配置
{
"updater": {
"active": true,
"dialog": true,
"endpoints": ["https://releases.myapp.com/{{target}}/{{current_version}}"],
"pubkey": "YOUR_UPDATER_PUBKEY"
},
}
- active - 布尔值,是否启用,默认值为 false
- dialog - 布尔值,是否启用内置新版本提示框,如果不启用,则需要在 JS 中自行监听事件并进行提醒
- endpoints - 数组,通过地址列表来确定服务器端是否有可用更新,字符串 {{target}} 和 {{current_version}} 会在 URL 中自动替换。如果指定了多个地址,服务器在预期时间内未响应,更新程序将依次尝试。endpoints 支持两种格式:
- 动态接口 - 服务器根据客户端的更新请求确定是否需要更新。 如果需要更新,服务器应以状态代码
200 OK
进行响应,并在正文中包含更新 JSON。 如果不需要更新,服务器必须响应状态代码204 No Content
。 - 静态文件 - 备用更新技术使用纯 JSON 文件,将更新元数据存储在 gist,github-pages 或其他静态文件存储中。
- 动态接口 - 服务器根据客户端的更新请求确定是否需要更新。 如果需要更新,服务器应以状态代码
- pubkey - 签名的公钥
实现步骤
拆解问题
要实现自动升级应用主要分为以下几个步骤:
- 生成签名(公私钥):
- 私钥用于设置打包(
tauri build
)的环境变量 - 公钥用于配置
tauri.conf.json -> updater.pubkey
- 私钥用于设置打包(
- 向客户端推送包含签名及下载链接的更新请求,有两种形式:
- 动态接口返回 json 数据
- 静态资源返回 json 文件
- 将 2 中的更新请求地址配置在
tauri.conf.json -> updater.endpoints
- 通过将
tauri.conf.json -> updater.dialog
配置为true
,启用内置通知更新应用的弹窗。设置为 false 则需要自行通过 js 事件来处理(暂不推荐,喜欢折腾的朋友可以自行尝试)
因为应用的跨平台打包借助了 github action
的工作流来实现,所以更新也同样使用 github action
来实现,充分发挥 github 的能力(简单来说,就是不需要借助其他第三方平台或服务就可以实现整个应用的自动化发布更新)。
梳理流程
- 在本地生成公私钥
- 加签名构建跨平台应用(通过 github action 设置签名环境变量)
- 对构建出的安装包解析,生成静态资源文件(通过脚本实现安装包信息获取)
- 推送更新请求采用静态资源的方式(可以将 json 文件存储在 github pages)
- 将 github pages 的资源地址配置到
tauri.conf.json -> updater.endpoints
代码实现
Step1
生成公私钥
tauri signer generate -w ~/.tauri/omb.key
配置公钥 pubkey
(~/.tauri/omb.key.pub
)及资源地址 endpoints
(github pages 地址):
{
"package": {
"productName": "OhMyBox",
"version": "../package.json"
},
"tauri": {
"updater": {
"active": true,
"dialog": true,
"endpoints": ["https://lencx.github.io/OhMyBox/install.json"],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU5MEIwREEzNDlBNzdDN0MKUldSOGZLZEpvdzBMNmFOZ2cyY2NPeTdwK2hsV3gwcWxoZHdUWXRZWFBpQTh1dWhqWXhBdkl0cW8K"
}
}
}
Step2
在项目根路径下创建 scripts
目录,然后在 scripts
下依次创建 release.mjs
,updatelog.mjs
,updater.mjs
三个 .mjs 文件:
scripts/release.mjs
- 版本发布,因发布需涉及多处改动(如版本,版本日志,打 tag 标签等等),故将其写成脚本,减少记忆成本scripts/updatelog.mjs
- 版本更新日志处理,供scripts/updater.mjs
脚本使用scripts/updater.mjs
- 生成应用更新需要的静态文件
# 安装开发依赖
yarn add -D node-fetch @actions/github
// scripts/release.mjs
import { createRequire } from 'module';
import { execSync } from 'child_process';
import fs from 'fs';
import updatelog from './updatelog.mjs';
const require = createRequire(import.meta.url);
async function release() {
const flag = process.argv[2] ?? 'patch';
const packageJson = require('../package.json');
let [a, b, c] = packageJson.version.split('.').map(Number);
if (flag === 'major') { // 主版本
a += 1;
b = 0;
c = 0;
} else if (flag === 'minor') { // 次版本
b += 1;
c = 0;
} else if (flag === 'patch') { // 补丁版本
c += 1;
} else {
console.log(`Invalid flag "${flag}"`);
process.exit(1);
}
const nextVersion = `${a}.${b}.${c}`;
packageJson.version = nextVersion;
const nextTag = `v${nextVersion}`;
await updatelog(nextTag, 'release');
// 将新版本写入 package.json 文件
fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 2));
// 提交修改的文件,打 tag 标签(tag 标签是为了触发 github action 工作流)并推送到远程
execSync('git add ./package.json ./UPDATE_LOG.md');
execSync(`git commit -m "v${nextVersion}"`);
execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
execSync(`git push`);
execSync(`git push origin v${nextVersion}`);
console.log(`Publish Successfully...`);
}
release().catch(console.error);
// scripts/updatelog.mjs
import fs from 'fs';
import path from 'path';
const UPDATE_LOG = 'UPDATE_LOG.md';
export default function updatelog(tag, type = 'updater') {
const reTag = /## v[\d\.]+/;
const file = path.join(process.cwd(), UPDATE_LOG);
if (!fs.existsSync(file)) {
console.log('Could not found UPDATE_LOG.md');
process.exit(1);
}
let _tag;
const tagMap = {};
const content = fs.readFileSync(file, { encoding: 'utf8' }).split('\n');
content.forEach((line, index) => {
if (reTag.test(line)) {
_tag = line.slice(3).trim();
if (!tagMap[_tag]) {
tagMap[_tag] = [];
return;
}
}
if (_tag) {
tagMap[_tag].push(line);
}
if (reTag.test(content[index + 1])) {
_tag = null;
}
});
if (!tagMap?.[tag]) {
console.log(
`${type === 'release' ? '[UPDATE_LOG.md] ' : ''}Tag ${tag} does not exist`
);
process.exit(1);
}
return tagMap[tag].join('\n').trim() || '';
}
// scripts/updater.mjs
import fetch from 'node-fetch';
import { getOctokit, context } from '@actions/github';
import fs from 'fs';
import updatelog from './updatelog.mjs';
const token = process.env.GITHUB_TOKEN;
async function updater() {
if (!token) {
console.log('GITHUB_TOKEN is required');
process.exit(1);
}
// 用户名,仓库名
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(token);
// 获取 tag
const { data: tags } = await github.rest.repos.listTags({
...options,
per_page: 10,
page: 1,
});
// 过滤包含 `v` 版本信息的 tag
const tag = tags.find((t) => t.name.startsWith('v'));
// console.log(`${JSON.stringify(tag, null, 2)}`);
if (!tag) return;
// 获取此 tag 的详细信息
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: tag.name,
});
// 需要生成的静态 json 文件数据,根据自己的需要进行调整
const updateData = {
version: tag.name,
// 使用 UPDATE_LOG.md,如果不需要版本更新日志,则将此字段置空
notes: updatelog(tag.name),
pub_date: new Date().toISOString(),
platforms: {
win64: { signature: '', url: '' }, // compatible with older formats
linux: { signature: '', url: '' }, // compatible with older formats
darwin: { signature: '', url: '' }, // compatible with older formats
'darwin-aarch64': { signature: '', url: '' },
'darwin-x86_64': { signature: '', url: '' },
'linux-x86_64': { signature: '', url: '' },
'windows-x86_64': { signature: '', url: '' },
// 'windows-i686': { signature: '', url: '' }, // no supported
},
};
const setAsset = async (asset, reg, platforms) => {
let sig = '';
if (/.sig$/.test(asset.name)) {
sig = await getSignature(asset.browser_download_url);
}
platforms.forEach((platform) => {
if (reg.test(asset.name)) {
// 设置平台签名,检测应用更新需要验证签名
if (sig) {
updateData.platforms[platform].signature = sig;
return;
}
// 设置下载链接
updateData.platforms[platform].url = asset.browser_download_url;
}
});
};
const promises = latestRelease.assets.map(async (asset) => {
// windows
await setAsset(asset, /.msi.zip/, ['win64', 'windows-x86_64']);
// darwin
await setAsset(asset, /.app.tar.gz/, [
'darwin',
'darwin-x86_64',
'darwin-aarch64',
]);
// linux
await setAsset(asset, /.AppImage.tar.gz/, ['linux', 'linux-x86_64']);
});
await Promise.allSettled(promises);
if (!fs.existsSync('updater')) {
fs.mkdirSync('updater');
}
// 将数据写入文件
fs.writeFileSync(
'./updater/install.json',
JSON.stringify(updateData, null, 2)
);
console.log('Generate updater/install.json');
}
updater().catch(console.error);
// 获取签名内容
async function getSignature(url) {
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/octet-stream' },
});
return response.text();
} catch (_) {
return '';
}
}
在根路径下创建 UPDATE_LOG.md
文件,通知用户更新注意事项,格式如下(使用版本号作为标题,具体请查看 scripts/updatelog.mjs
):
# Updater Log
## v0.1.7
- feat: xxx
- fix: xxx
## v0.1.6
test
修改 package.json
,在 "scripts" 中加入 updater
和 release
命令:
"scripts": {
"dev": "vite --port=4096",
"build": "rsw build && tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"rsw": "rsw",
"updater": "node scripts/updater.mjs", // ✅ 新增
"release": "node scripts/release.mjs" // ✅ 新增
},
Step3
Action 配置请参考之前的文章【Tauri 入门篇 - 跨平台编译】,此处新增环境设置签名和静态资源推送。
设置 Secret
配置变量 Repo -> Settings -> Secrets -> Actions -> New repository secret
:
- TAURI_PRIVATE_KEY - 私钥,value 为
~/.tauri/omb.key.pub
内容- Name:
TAURI_PRIVATE_KEY
- Value:
******
- Name:
- TAURI_KEY_PASSWORD - 密码,value 为生成签名时的密码
- Name:
TAURI_KEY_PASSWORD
- Value:
******
- Name:
设置 .github/workflows/release.yml
name: Release CI
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
jobs:
create-release:
runs-on: ubuntu-latest
outputs:
RELEASE_UPLOAD_ID: ${{ steps.create_release.outputs.id }}
steps:
- uses: actions/checkout@v2
- name: Query version number
id: get_version
shell: bash
run: |
echo "using version tag ${GITHUB_REF:10}"
echo ::set-output name=version::"${GITHUB_REF:10}"
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: '${{ steps.get_version.outputs.VERSION }}'
release_name: 'OhMyBox ${{ steps.get_version.outputs.VERSION }}'
body: 'See the assets to download this version and install.'
build-tauri:
needs: create-release
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
# Rust cache
- uses: Swatinem/rust-cache@v1
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
# Install wasm-pack
- uses: jetli/wasm-pack-action@v0.3.0
with:
# Optional version of wasm-pack to install(eg. 'v0.9.1', 'latest')
version: v0.9.1
- name: Install rsw
run: cargo install rsw
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
- name: Yarn cache
uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install app dependencies and build it
run: yarn && yarn build
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
releaseId: ${{ needs.create-release.outputs.RELEASE_UPLOAD_ID }}
# 生成静态资源并将其推送到 github pages
updater:
runs-on: ubuntu-latest
needs: [create-release, build-tauri]
steps:
- uses: actions/checkout@v2
- run: yarn
- run: yarn updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy install.json
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./updater
force_orphan: true
发布应用
功能开发完成,提交代码后,只需执行 yarn release
命令就可以自动进行应用发布了。如果不想借助 github 打包和静态资源存放,也可以参考上面的步骤,自行部署。
# 发布主版本,v1.x.x -> v2.x.x
yarn release major
# 发布次版本,v1.0.x -> v1.1.x
yarn release minor
# 发布补丁版本,patch 参数可省略,v1.0.0 -> v1.0.1
yarn release [patch]
注意:每次执行 yarn release
发布版本,主版本
,次版本
,补丁版本
都是自增 1。
常见问题
Error A public key has been found, but no private key
如果在 tauri.conf.json
中配置了 pubkey
,但未设置环境变量会出现以下错误:
tauri build
# ...
Compiling omb v0.1.0 (/Users/lencx/github/lencx/OhMyBox/src-tauri)
Finished release [optimized] target(s) in 21.27s
Bundling OhMyBox.app (/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app)
Bundling OhMyBox_0.1.1_x64.dmg (/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/dmg/OhMyBox_0.1.1_x64.dmg)
Running bundle_dmg.sh
Bundling /Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app.tar.gz (/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app.tar.gz)
Finished 3 bundles at:
/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app
/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/dmg/OhMyBox_0.1.1_x64.dmg
/Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app.tar.gz (updater)
Error A public key has been found, but no private key. Make sure to set `TAURI_PRIVATE_KEY` environment variable.
error Command failed with exit code 1.
解决方案:
# macOS 设置环境变量:
export TAURI_PRIVATE_KEY="********" # omb.key
export TAURI_KEY_PASSWORD="********" # 生成公私钥时在终端输入的密码,如果未设置密码则无需设置此变量
# Windows 设置环境变量:
set TAURI_PRIVATE_KEY="********"
set TAURI_KEY_PASSWORD="********"
# 如果签名打包成功会看到以下信息(以 macOS 为例)
Info 1 updater archive at:
Info /Users/lencx/github/lencx/OhMyBox/src-tauri/target/release/bundle/macos/OhMyBox.app.tar.gz.sig
✨ Done in 58.55s.
版本信息错误
发布的应用版本以 tauri.conf.json
中的 package.version
为准,在发布新版本时注意更新 version
。
可能造成更新失败的原因
- 使用 github pages 作为更新文件静态资源存储在国内会因网络限制导致更新失败,无法看到更新弹窗提示,或者下载不响应等问题,可以通过配置多个
endpoints
地址来解决,安装包也可以放在自建服务器来提高下载的稳定性 - 静态 json 文件中的平台签名(
platforms[platform].signature
)是否完整,签名内容可以在tauri build
产生的target/release/bundle/<platform>/*.sig
文件中查看
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)