Tauri 踩坑实录之基于 Github Action 实现自动更新

前言

根据官方文档 Tauri Updater: Static JSON File ,我们可以通过 Github Action 和 Github Release 实现项目的自动编译及更新。

参考 浮之静: Tauri 应用篇 - 自动通知应用升级GyDi: clash-verge 实现,希望对你有所帮助。

跨平台编译

根据 Tauri: Cross-Platform Compilation 实现,使用 pnpm 作为包管理器。

添加 workflow 脚本

1
2
3
$ cd ${Your project path}
$ mkdir .github/workflows
$ touch .github/workflows/release.yml

编写 workflow,当提交的 tag 信息为 v* 的格式即自动触发,或在 Github Actions 页面手动触发,默认全平台编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
workflow_dispatch:

jobs:
release:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
# 选择编译平台
platform: [macos-latest, ubuntu-20.04, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
# You can remove libayatana-appindicator3-dev if you don't use the system tray feature.
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev

- name: Rust setup
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './src-tauri -> target'

- name: Sync node version and insatll nodejs
uses: actions/setup-node@v3
with:
node-version: 'lts/*'

# 使用 pnpm 作为包管理器
- name: Install pnpm
uses: pnpm/action-setup@v2
id: pnpm-install
with:
version: 7
run_install: false

- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT

- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install frontend dependencies
# If you don't have `beforeBuildCommand` configured you may want to build your frontend here too.
run: pnpm install # Change this to npm, yarn or pnpm.

- name: Build the app
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:
tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags.
releaseName: 'App Name v__VERSION__' # 自定义 release 名称,__VERSION__ 将自动填写为版本信息
releaseBody: 'See the assets to download and install this version.'
releaseDraft: true
prerelease: false

添加编译流程

添加环境变量

在项目的 github 仓库添加所需环境变量。

Name Value
TAURI_PRIVATE_KEY ${Your project private key}
TAURI_KEY_PASSWORD ${Your project password}

添加仓库环境变量

查看使用情况

访问 Github Account: billing,看到 Usage this month 第一项即是 Github Actions 当前使用情况。

查看账户账单使用

查看 Github Actions 使用

自动更新

根据 Tauri Updater:Static JSON File 实现,通过 github release 实现分发。

添加项目签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ cd ${Your project path}

# 在 linux 或 macOS 为项目签名
$ pnpm tauri signer generate -w ~/.tauri/${Custom sign name}.key

pnpm tauri signer generate -w ~/.tauri/tauri-app.key

# 在 windows 为项目签名
$ pnpm tauri signer generate -w $HOME/.tauri/${Custom sign name}.key

# 输入密码,密码不会显示但输入都是有效的,类似 linux
# 此处假设设置的 Custom sign name 为 tauri-app
> tauri-app@0.0.0 tauri /home/user/Desktop/tauri-app
> tauri "signer" "generate" "-w" "/home/user/.tauri/tauri-app.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: /home/user/.tauri/tauri-app.key (Keep it secret!)
Public: /home/user/.tauri/tauri-app.key.pub
---------------------------

Environment variables 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 work.
---------------------------

创建 updater release

通过在 github release 中创建 updater tag 并添加 update.json 文件,我们可以在 tauri.conf.jsonupdater 属性里 endpoionts 中添加该文件地址。

这样只要程序能够访问 github release,就能够获取更新信息,我们只需要在发布新版本后更新文件即可。

1
2
3
$ cd ${Your project path}
$ git tag updater
$ git push --tags

添加 updater 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"version": "v1.0.0",
"notes": "Test version",
"pub_date": "2020-06-22T19:25:57Z",
"platforms": {
"darwin-x86_64": {
"signature": "Content of app.tar.gz.sig",
"url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x86_64.app.tar.gz"
},
"darwin-aarch64": {
"signature": "Content of app.tar.gz.sig",
"url": "https://github.com/username/reponame/releases/download/v1.0.0/app-aarch64.app.tar.gz"
},
"linux-x86_64": {
"signature": "Content of app.AppImage.tar.gz.sig",
"url": "https://github.com/username/reponame/releases/download/v1.0.0/app-amd64.AppImage.tar.gz"
},
"windows-x86_64": {
"signature": "Content of app.msi.sig",
"url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x64.msi.zip"
}
}
}

创建 updater release

修改配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
...
"tauri": {
...
"updater": {
"active": true,
"endpoints": [
"https://github.com/${{Your github username}}/${{Your repo name}}/releases/download/updater/update.json"
],
"dialog": false,
"pubkey": "${{Your signature pub key}}",
"windows": {
"installMode": "passive"
}
}
...
}
...
}

本地推送完成更新

通过本地脚本和 github actions 的结合,实现本地推送完成 update.json 的更新。

添加依赖及相关文件

1
2
3
4
5
$ cd ${Your project path}
$ pnpm install -D node-fetch fs-extra @actions/github
$ touch UPDATELOG.md
$ mkdir script && cd ./script
$ touch updatelog.mjs publish.mjs update.mjs

更新 package.json

1
2
3
4
5
6
7
8
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri",
"updater": "node scripts/updater.mjs",
"publish": "node scripts/publish.mjs"
},

编写更新信息脚本 updatelog.mjs

负责从 UPDATELOG.md 文件获取更新描述信息,对应 update.jsonnotes 项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import fs from "fs-extra";
import path from "path";

const UPDATE_LOG = "UPDATELOG.md";

// 解析 UPDATELOG.md 文件,获取更新信息
export async function resolveUpdateLog(tag) {
const cwd = process.cwd();

const reTitle = /^## v[\d\.]+/;
const reEnd = /^---/;

const file = path.join(cwd, UPDATE_LOG);

if (!(await fs.pathExists(file))) {
throw new Error("could not found UPDATELOG.md");
}

const data = await fs.readFile(file).then((d) => d.toString("utf8"));

const map = {};
let p = "";

data.split("\n").forEach((line) => {
if (reTitle.test(line)) {
p = line.slice(3).trim();
if (!map[p]) {
map[p] = [];
} else {
throw new Error(`Tag ${p} dup`);
}
} else if (reEnd.test(line)) {
p = "";
} else if (p) {
map[p].push(line);
}
});

if (!map[tag]) {
throw new Error(`could not found "${tag}" in UPDATELOG.md`);
}

return map[tag].join("\n").trim();
}

UPDATELOG.md 文件中更新信息格式如下。

1
2
3
4
5
6
7
8
9
10
11
## v0.0.1

### Features

- some feature
- another feature

### Bug Fixes

- some bugs
- another bugs

编写版本更新及发布脚本 publish.mjs

负责自动更新版本信息并提交 git,注意须先写好更新日志 UPDATELOG.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import fs from "fs-extra";
import { createRequire } from "module";
import { execSync } from "child_process";
import { resolveUpdateLog } from "./updatelog.mjs";

const require = createRequire(import.meta.url);

// 自动更新版本,添加对应 tag 并推送至仓库
async function resolvePublish() {
const flag = process.argv[2] ?? "patch";
const packageJson = require("../package.json");
const tauriJson = require("../src-tauri/tauri.conf.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 throw new Error(`invalid flag "${flag}"`);

const nextVersion = `${a}.${b}.${c}`;
packageJson.version = nextVersion;
tauriJson.package.version = nextVersion;

// 发布更新前先写更新日志
const nextTag = `v${nextVersion}`;
await resolveUpdateLog(nextTag);

await fs.writeFile(
"./package.json",
JSON.stringify(packageJson, undefined, 2)
);
await fs.writeFile(
"./src-tauri/tauri.conf.json",
JSON.stringify(tauriJson, undefined, 2)
);

execSync("git add ./package.json");
execSync("git add ./src-tauri/tauri.conf.json");
execSync(`git commit -m "release v${nextVersion}"`);
execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
execSync(`git push`);
execSync(`git push origin v${nextVersion}`);
console.log(`Publish Successfully...`);
}

resolvePublish();

添加静态文件更新脚本 update.mjs

负责更新文件 update.json 的生成与发布,在 github actions 中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import { resolveUpdateLog } from "./updatelog.mjs";

const UPDATE_TAG_NAME = "updater";
const UPDATE_JSON_FILE = "update.json";

/// 生成 update.json 文件并更新 github updater release 中的文件
async function resolveUpdater() {
if (process.env.GITHUB_TOKEN === undefined) {
throw new Error("GITHUB_TOKEN is required");
}

const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);

const { data: tags } = await github.rest.repos.listTags({
...options,
per_page: 10,
page: 1,
});

const tag = tags.find((t) => t.name.startsWith("v"));

console.log(tag);

const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: tag.name,
});

// 根据需要选择需更新的平台,应与编译脚本平台选择对应
const updateData = {
version: tag.name,
notes: await resolveUpdateLog(tag.name),
pub_date: new Date().toISOString(),
platforms: {
// comment out as needed
"windows-x86_64": { signature: "", url: "" },
// "darwin-aarch64": { signature: "", url: "" },
"darwin-x86_64": { signature: "", url: "" },
"linux-x86_64": { signature: "", url: "" },
},
};

const promises = latestRelease.assets.map(async (asset) => {
const { name, browser_download_url } = asset;

// windows-x86_64 url
if (name.endsWith(".msi.zip")) {
updateData.platforms["windows-x86_64"].url = browser_download_url;
}

// windows-x86_64 signature
if (name.endsWith(".msi.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86_64"].signature = sig;
}

// darwin-x86_64 url (macos intel)
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
updateData.platforms["darwin-x86_64"].url = browser_download_url;
}
// darwin-x86_64 signature (macos intel)
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["darwin-x86_64"].signature = sig;
}

// darwin-aarch64 url (macos silicon)
if (name.endsWith("aarch64.app.tar.gz")) {
updateData.platforms["darwin-aarch64"].url = browser_download_url;
}

// darwin-aarch64 signature (macos silicon)
if (name.endsWith("aarch64.app.tar.gz.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["darwin-aarch64"].signature = sig;
}

// linux-x86_64 url
if (name.endsWith(".AppImage.tar.gz")) {
updateData.platforms["linux-x86_64"].url = browser_download_url;
}
// linux-x86_64 signature
if (name.endsWith(".AppImage.tar.gz.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["linux-x86_64"].signature = sig;
}
});

await Promise.allSettled(promises);
console.log(updateData);

Object.entries(updateData.platforms).forEach(([key, value]) => {
if (!value.url) {
console.log(`[Error]: failed to parse release for "${key}"`);
delete updateData.platforms[key];
}
});

// 更新 update.json 文件
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: UPDATE_TAG_NAME,
});

for (let asset of updateRelease.assets) {
if (asset.name === UPDATE_JSON_FILE) {
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: asset.id,
});
}
}

await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: UPDATE_JSON_FILE,
data: JSON.stringify(updateData, null, 2),
});
}

async function getSignature(url) {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/octet-stream" },
});

return response.text();
}

resolveUpdater().catch(console.error);

编写发布更新 workflow

当项目新版本 release 发布时自动触发,或手动触发,使用 pnpm 作为包管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ cd ${Your project path}
$ touch .github/workflows/updater.yml

BASH
name: Updater CI
run-name: Release update.json

on:
release:
types: [published]
workflow_dispatch:

jobs:
release-update:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Install pnpm
uses: pnpm/action-setup@v2
id: pnpm-install
with:
version: 7
run_install: false

- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT

- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install frontend dependencies
run: pnpm install

- name: Release updater file
run: pnpm run updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

添加更新流程

更新流程

  1. 完成新版本程序编写并提交
  2. UPDATELOG.md 中添加新版本描述
  3. 执行 pnpm run publish, 发布新版本,由于提交信息中带有版本 tag,触发 Release CI
  4. 编译完成,repo release 中出现新版本的 draft,根据需要修改相关信息并发布
  5. 发布完成自动触发 Updater CI,更新 updater release 中的 update.json
  6. 程序此时可以给通过访问 github release 中的 update.json 获得新版本更新信息并实现自动更新

Tauri 踩坑实录之基于 Github Action 实现自动更新
http://example.com/2023/04/13/Tauri-踩坑实录之基于-Github-Action-实现自动更新/
作者
Steins Gu
发布于
2023年4月13日
许可协议