Tauri 2 项目 Gitee 源码 + GitHub Actions 三平台 Release 发布

背景

项目 ZImgTools 是基于 Tauri 2 + Vue 3 + Vite 的图片压缩与格式转换桌面工具,源码放在 Gitee 私有仓库

安装包需要发布到 GitHub Release,供用户下载。

约束与目标:

  • GitHub 侧不存放源码,只放安装包(独立仓库 psvmc/z-img-tools-releases
  • 需要 Windows + Linux + macOS 三平台产物
  • 本机是 Windows,没有 macOS;Linux 也不想依赖本机 WSL / Docker

Tip

  • Gitee Go 收费。
  • Github Actions 公开仓库免费。
  • 本地构建 Linux 环境需要用 WSL 或 Docker 并安装编译环境,Mac 环境无法在 Windows 下创建。

方案演进

阶段 做法 结果
1 Gitee Go CI 构建 + GitHub API 上传 Gitee Go 收费,放弃
2 本机 Win 构建 + WSL 构建 Linux + GHA 构建 macOS 环境维护成本高
3(最终) 三平台全部 GitHub Actions 本机只负责触发

最终方案:源码在 Gitee,构建在 GitHub Actions,发布到 GitHub Releases 空壳仓

架构

image-20260629145453505

要点:

  1. 源码仓(Gitee):正常开发、git taggit push
  2. 发布仓(GitHub psvmc/z-img-tools-releases):只有 README + workflow,建议 Public(Actions 免费)
  3. workflow 不在 Gitee 跑,而是通过脚本同步到发布仓后触发
  4. Actions 用 GITEE_TOKEN 从 Gitee 克隆私有仓库对应 tag

一次性配置

准备 GitHub Releases 仓库

在 GitHub 创建 psvmc/z-img-tools-releasesmain 分支至少有一个 commit(例如 README)。

GitHub Token

Token 创建入口(登录 GitHub 后打开):

Classic:https://github.com/settings/tokens/new

勾选下面的权限:

image-20260629110311650

安装 GitHub CLI

1
winget install GitHub.cli

如果安装不成功,执行下面的命令

1
2
3
winget source reset --force
winget source update
winget install GitHub.cli --accept-source-agreements --accept-package-agreements

安装完要重启终端

1
gh auth login

配置 Gitee 私人令牌

Gitee 私人令牌 创建令牌,勾选 projects 读权限。

私人令牌 - Gitee.com

写入发布仓 Secret(须带 --body,不要用不带 --body 的交互命令,否则 Windows 下容易写入空值):

1
gh secret set GITEE_TOKEN -R psvmc/z-img-tools-releases --body "你的Gitee令牌"

验证:触发 workflow 后,日志中 GITEE_TOKEN: 应显示 ***,而不是空白。

注意每个发布仓都要单独配置一次 Secret。

项目内脚本

源码仓库 scripts/ 目录下保留以下文件:

1
2
3
4
5
6
7
8
scripts/
├── publish-release.bat # 发版入口
├── set-version.bat # 版本号一键同步(交互式)
├── set-version.ps1 # 版本脚本实现
└── release/
├── ensure-release-gha.bat # 同步 workflow 到发布仓
├── ensure-release-gha.ps1 # 通过 GitHub Contents API 上传 workflow
└── github-workflow-release-all.yml # 三平台构建 workflow 模板

publish-release.batcall scripts\release\ensure-release-gha.bat,上述 release/ 目录必须完整,否则报「系统找不到指定的路径」。

版本号与打包配置

升级版本脚本

发版前用 scripts\set-version.bat 同步各文件版本号:

1
2
3
scripts\set-version.bat          REM 显示当前版本并交互输入
scripts\set-version.bat 1.2.0 REM 指定版本
scripts\set-version.bat patch REM 1.2.0 -> 1.2.1

自动更新:package.jsonpackage-lock.jsonsrc-tauri/Cargo.tomlsrc-tauri/Cargo.locksrc-tauri/tauri.conf.json(含窗口标题中的 vX.Y.Z)。

程序命名

tauri.conf.json 中配置:

1
2
3
4
{
"productName": "ZImgTools",
"mainBinaryName": "ZImgTools"
}
  • productName:安装包、开始菜单等显示名称
  • mainBinaryName:可执行文件名(打包后为 ZImgTools.exe,不再是 Cargo 包名 z-img-tools

图标配置

图标文件放在 src-tauri/icons/,可在项目根 icons/ 维护设计稿后复制过去:

文件 用途
32x32.png / 128x128.png / 128x128@2x.png Tauri bundle 多尺寸 PNG
icon.ico Windows 主程序 + NSIS 安装包图标
icon.icns macOS

tauri.conf.json 关键配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"bundle": {
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"nsis": {
"installerIcon": "icons/icon.ico"
}
}
}
}

说明:

  • bundle.icon 只作用于主程序 exe 嵌入图标
  • NSIS 安装包需单独配置 installerIcon,否则安装程序会使用 NSIS 默认图标
  • 当前 tauri-build 尚不支持 uninstallerIcon,不要写入配置,否则构建报错

本地 Windows 打包(NSIS)

按平台拆分的配置文件,开发时主配置 bundle.active 仍为 false,Windows 打包时自动合并:

src-tauri/tauri.windows.conf.json

1
2
3
4
5
6
{
"bundle": {
"active": true,
"targets": "nsis"
}
}

本地打包命令:

1
2
npm run tauri build      REM Windows 下自动生成 NSIS
npm run build:app REM 显式指定 NSIS

产物:src-tauri/target/release/ZImgTools.exesrc-tauri/target/release/bundle/nsis/ZImgTools_<version>_x64-setup.exe

Linux / macOS 对应 tauri.linux.conf.json(deb)、tauri.macos.conf.json(dmg),CI 构建时同样生效。

发版流程

流程

1
2
3
4
5
scripts\set-version.bat 1.2.0
git add -A && git commit -m "chore: bump version to 1.2.0"
git tag 1.2.0
git push origin master --tags
scripts\publish-release.bat

或指定版本触发(tag 须已推到 Gitee):

1
scripts\publish-release.bat 1.2.0

注意:Gitee tag 与 src-tauri/Cargo.tomlsrc-tauri/tauri.conf.jsonpackage.json 中的版本号保持一致,不加 v 前缀(如 1.0.0,不是 v1.0.0)。

发版前务必先 git push origin <tag>,否则 Actions 克隆不到对应 tag。

脚本会:

  1. src-tauri/Cargo.toml 读取版本号(未传参时)
  2. github-workflow-release-all.yml 同步到 psvmc/z-img-tools-releases.github/workflows/release-all.yml
  3. 触发 release-all.yml workflow

约 10~30 分钟后在 Release 页查看:

产物

文件 平台
ZImgTools_*_x64-setup.exe(NSIS 安装包) Windows
ZImgTools_*_amd64.deb Linux
ZImgTools_*_universal.dmg macOS(Universal 二进制)

Actions 中只打 NSIS / deb / dmg 三种格式。本地 Windows 默认打 NSIS(见上文 tauri.windows.conf.json)。

项目开发时 tauri.conf.jsonbundle.activefalse;CI 与各平台配置文件(tauri.windows.conf.json 等)在构建时合并为 active: true,无需 workflow 内再 patch 配置。

Release 更新日志

GitHub Release 的说明文字不使用 generate_release_notes: true(那是 GitHub 按 PR 自动生成的),而是从 Gitee 源码仓库 取两个 tag 之间的 git 提交记录 生成。

publish job 在汇总 artifact 后、发布 Release 前会:

  1. GITEE_TOKEN 克隆 Gitee 仓库并 git fetch --tags
  2. 解析当前发版 tag(兼容 1.0.0 / v1.0.0
  3. 按版本号排序 tag 列表,找到上一个 tag 作为对比基准
  4. 执行 git log PREV_TAG..TAG --pretty=format:'- %s (%h)' --no-merges
  5. 将结果写入 Release 的 body

生成示例:

1
2
3
4
## Changes since 1.0.0

- 修复批量压缩进度显示 (a1b2c3d)
- 新增 WebP 转换支持 (e4f5g6h)

若是首个版本(没有更早 tag),标题为 ## Initial release,内容为该 tag 可达的全部提交。

应用内检查更新也会读取 Release 的 body 字段展示给用户,因此 commit message 写清楚有助于用户了解变更内容。

应用内更新实现见 Tauri 2 项目 GitHub Release 应用版本更新:从本 Release 仓下载对应平台安装包,Windows 为 *-setup.exe,下载时弹窗显示进度,完成后 detached 启动 NSIS(/UPDATE 覆盖安装)。

脚本代码

发版入口

路径:scripts/publish-release.bat

读版本号、同步 workflow、触发 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
@echo off
setlocal EnableDelayedExpansion
cd /d "%~dp0.."

rem --- Release config (edit below) ---
set "GITHUB_REPO=psvmc/z-img-tools-releases"
set "GITEE_REPO=https://gitee.com/psvmc/z-img-tools-tauri.git"
rem ---------------------------------

set "TAG="

:parse_args
if "%~1"=="" goto args_done
if not defined TAG set "TAG=%~1"
shift
goto parse_args
:args_done

if not defined TAG (
for /f "usebackq tokens=2 delims== " %%a in (`findstr /R /C:"^version " src-tauri\Cargo.toml`) do set "VER=%%a"
set "VER=!VER:"=!"
for /f "tokens=* delims= " %%a in ("!VER!") do set "VER=%%a"
set "TAG=!VER!"
)

where gh >nul 2>&1 || (
echo [error] Install GitHub CLI and run: gh auth login
exit /b 1
)

echo.
echo === ZImgTools release (GitHub Actions: Windows + Linux + macOS) ===
echo Tag: %TAG%
echo GitHub repo: %GITHUB_REPO%
echo Gitee repo: %GITEE_REPO%
echo.
echo Prerequisites:
echo 1. Gitee token in GitHub secret GITEE_TOKEN
echo gh secret set GITEE_TOKEN -R %GITHUB_REPO% --body "YOUR_GITEE_TOKEN"
echo 2. Push tag to Gitee: git push origin %TAG%
echo.

set "GITHUB_REPO=%GITHUB_REPO%"
call "%~dp0release\ensure-release-gha.bat"
if errorlevel 1 exit /b 1

echo Triggering release-all workflow...
gh workflow run release-all.yml -R "%GITHUB_REPO%" -f "tag=%TAG%" -f "gitee_repo=%GITEE_REPO%"
if errorlevel 1 (
echo [error] Failed to start workflow
exit /b 1
)

echo.
echo Build started. Wait 10-30 min, then check:
echo https://github.com/%GITHUB_REPO%/releases/tag/%TAG%
echo https://github.com/%GITHUB_REPO%/actions
echo.
echo Watch: gh run watch -R %GITHUB_REPO%
exit /b 0

升级版本脚本

路径:scripts/set-version.bat(调用同目录 set-version.ps1

发版前同步各文件版本号。无参数时显示当前版本并交互输入;也支持 1.2.0patchminormajor

1
2
3
4
5
6
@echo off
setlocal
cd /d "%~dp0.."

powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0set-version.ps1" %*
exit /b %errorlevel%

用法示例:

1
2
3
4
5
scripts\set-version.bat
scripts\set-version.bat 1.2.0
scripts\set-version.bat patch
scripts\set-version.bat minor
scripts\set-version.bat major

版本脚本实现

路径:scripts/set-version.ps1

读取 src-tauri/Cargo.toml 当前版本,通过 npm version --no-git-tag-version 更新 package.json / package-lock.json,并同步 Rust 与 Tauri 配置(UTF-8 读写,避免中文乱码)。

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
135
136
137
138
139
140
141
142
143
144
145
146
param(
[Parameter(Position = 0)]
[string]$Target
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

$Root = Split-Path -Parent $PSScriptRoot
Set-Location $Root

$Utf8NoBom = New-Object System.Text.UTF8Encoding $false

function Read-Utf8File([string]$Path) {
return [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
}

function Write-Utf8NoBomFile([string]$Path, [string]$Content) {
[System.IO.File]::WriteAllText($Path, $Content, $Utf8NoBom)
}

function Get-CurrentVersion {
$cargo = Join-Path $Root 'src-tauri\Cargo.toml'
foreach ($line in ((Read-Utf8File $cargo) -split "`r?`n")) {
if ($line -match '^\s*version\s*=\s*"([^"]+)"\s*$') {
return $Matches[1]
}
}
throw 'Could not read version from src-tauri\Cargo.toml'
}

function Test-SemVer([string]$Version) {
return $Version -match '^\d+\.\d+\.\d+$'
}

function Bump-SemVer([string]$Version, [string]$Kind) {
$parts = $Version -split '\.'
if ($parts.Count -ne 3) {
throw "Invalid version format: $Version"
}

$major = [int]$parts[0]
$minor = [int]$parts[1]
$patch = [int]$parts[2]

switch ($Kind.ToLowerInvariant()) {
'major' { return '{0}.0.0' -f ($major + 1) }
'minor' { return '{0}.{1}.0' -f $major, ($minor + 1) }
'patch' { return '{0}.{1}.{2}' -f $major, $minor, ($patch + 1) }
default { throw "Unknown bump kind: $Kind" }
}
}

function Update-TauriConf([string]$NewVersion) {
$path = Join-Path $Root 'src-tauri\tauri.conf.json'
$content = Read-Utf8File $path
$content = $content -replace '"version"\s*:\s*"\d+\.\d+\.\d+"', ('"version": "{0}"' -f $NewVersion)
$content = [regex]::Replace(
$content,
'("title"\s*:\s*"[^"]*v)\d+\.\d+\.\d+(")',
{ param($match) $match.Groups[1].Value + $NewVersion + $match.Groups[2].Value }
)
Write-Utf8NoBomFile $path $content
}

function Update-CargoToml([string]$NewVersion) {
$path = Join-Path $Root 'src-tauri\Cargo.toml'
$content = Read-Utf8File $path
$content = $content -replace '(?m)^version\s*=\s*"\d+\.\d+\.\d+"', ('version = "{0}"' -f $NewVersion)
Write-Utf8NoBomFile $path $content
}

function Update-CargoLock([string]$NewVersion) {
$path = Join-Path $Root 'src-tauri\Cargo.lock'
$content = Read-Utf8File $path
$content = [regex]::Replace(
$content,
'(?ms)(name = "z-img-tools"\r?\nversion = ")\d+\.\d+\.\d+(")',
{ param($match) $match.Groups[1].Value + $NewVersion + $match.Groups[2].Value }
)
Write-Utf8NoBomFile $path $content
}

$current = Get-CurrentVersion

if (-not $Target) {
Write-Host ''
Write-Host "Current version: $current"
Write-Host 'Enter new version (e.g. 1.2.0, or patch / minor / major):'
$Target = Read-Host 'New version'
Write-Host ''
}

if (-not $Target -or -not $Target.Trim()) {
Write-Host 'Cancelled: no version entered.'
exit 1
}

$Target = $Target.Trim()
$kind = $Target.ToLowerInvariant()

if ($kind -in @('patch', 'minor', 'major')) {
$newVersion = Bump-SemVer $current $kind
} elseif (Test-SemVer $Target) {
$newVersion = $Target
} else {
Write-Error "Invalid target: $Target. Use semver (e.g. 1.2.0) or patch/minor/major."
exit 1
}

if ($newVersion -eq $current) {
Write-Host "Version unchanged: $current"
exit 0
}

Write-Host "Updating version: $current -> $newVersion"
Write-Host ''

$npm = Get-Command npm -ErrorAction SilentlyContinue
if (-not $npm) {
Write-Error 'npm not found. Install Node.js to update package.json and package-lock.json.'
exit 1
}

Push-Location $Root
try {
npm version $newVersion --no-git-tag-version --allow-same-version | Out-Null
if ($LASTEXITCODE -ne 0) {
throw 'npm version failed'
}
} finally {
Pop-Location
}

Update-CargoToml $newVersion
Update-CargoLock $newVersion
Update-TauriConf $newVersion

Write-Host 'Updated files:'
Write-Host ' - package.json'
Write-Host ' - package-lock.json'
Write-Host ' - src-tauri\Cargo.toml'
Write-Host ' - src-tauri\Cargo.lock'
Write-Host ' - src-tauri\tauri.conf.json'
Write-Host ''
Write-Host "Done. Current version: $newVersion"

同步的文件:

文件 更新内容
package.json / package-lock.json versionnpm version
src-tauri/Cargo.toml [package]version
src-tauri/Cargo.lock z-img-tools 包版本
src-tauri/tauri.conf.json version、窗口 title 中的 vX.Y.Z

同步 workflow

路径:scripts/release/ensure-release-gha.bat

调用 PowerShell,将 workflow 上传到发布仓。

1
2
3
4
5
6
7
8
9
10
11
12
13
@echo off
setlocal
cd /d "%~dp0..\.."

if not defined GITHUB_REPO set "GITHUB_REPO=psvmc/z-img-tools-releases"

where gh >nul 2>&1 || (
echo [error] gh not found. Run: gh auth login
exit /b 1
)

powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ensure-release-gha.ps1" -Repo "%GITHUB_REPO%"
exit /b %errorlevel%

上传 workflow

路径:scripts/release/ensure-release-gha.ps1

通过 GitHub Contents API 创建或更新 .github/workflows/release-all.yml

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
# Upload release-all.yml to the GitHub releases repository
param(
[string]$Repo = $env:GITHUB_REPO
)

$ErrorActionPreference = 'Stop'

if (-not $Repo) { $Repo = 'psvmc/z-img-tools-releases' }

$workflowName = 'release-all.yml'
$workflowPath = ".github/workflows/$workflowName"
$sourceFile = Join-Path $PSScriptRoot 'github-workflow-release-all.yml'

if (-not (Test-Path $sourceFile)) {
Write-Error "Missing $sourceFile"
}

$content = [IO.File]::ReadAllText($sourceFile)
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($content))

$apiArgs = @(
"repos/$Repo/contents/$workflowPath",
'-X', 'PUT',
'-f', "message=Add or update release-all workflow",
'-f', "content=$b64",
'-f', 'branch=main'
)

$prevEap = $ErrorActionPreference
$ErrorActionPreference = 'SilentlyContinue'
$existingJson = gh api "repos/$Repo/contents/$workflowPath" 2>$null
$checkCode = $LASTEXITCODE
$ErrorActionPreference = $prevEap

if ($checkCode -eq 0 -and $existingJson) {
$existing = $existingJson | ConvertFrom-Json
if ($existing.sha) {
$apiArgs += '-f', "sha=$($existing.sha)"
Write-Host "Updating workflow on $Repo..."
}
} else {
Write-Host "Creating workflow on $Repo..."
}

gh api @apiArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

Write-Host "Workflow ready: https://github.com/$Repo/blob/main/$workflowPath"

三平台构建 workflow

路径:scripts/release/github-workflow-release-all.yml(同步到发布仓后为 .github/workflows/release-all.yml

matrix 并行构建 Win / Linux / macOS,汇总 artifact 后发布 Release。构建使用 Rust + npm + Tauri CLI,与 Wails 方案不同。

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
name: Release All Platforms

on:
workflow_dispatch:
inputs:
tag:
description: Release tag (e.g. 1.0.0)
required: true
gitee_repo:
description: Gitee source URL (no credentials)
required: false
default: https://gitee.com/psvmc/z-img-tools-tauri.git

permissions:
contents: write

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
platform: windows
- os: ubuntu-22.04
platform: linux
- os: macos-latest
platform: macos

runs-on: ${{ matrix.os }}
steps:
- name: Clone from Gitee
env:
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
GITEE_REPO: ${{ inputs.gitee_repo }}
RELEASE_TAG: ${{ inputs.tag }}
shell: bash
run: |
set -euo pipefail
if [ -z "${GITEE_TOKEN:-}" ]; then
echo "::error::Set secret GITEE_TOKEN on this repository (Gitee private token, scope: projects)"
exit 1
fi
REPO_PATH="${GITEE_REPO#https://}"
AUTH_URL="https://oauth2:${GITEE_TOKEN}@${REPO_PATH}"

git config --global http.postBuffer 524288000
git config --global http.version HTTP/1.1

TAG_CANDIDATES=("$RELEASE_TAG")
if [[ "$RELEASE_TAG" == v* ]]; then
TAG_CANDIDATES+=("${RELEASE_TAG#v}")
else
TAG_CANDIDATES+=("v${RELEASE_TAG}")
fi

clone_ok=false
for TAG in "${TAG_CANDIDATES[@]}"; do
for attempt in 1 2 3; do
echo "Cloning tag ${TAG} (attempt ${attempt})..."
rm -rf repo
if git clone --depth 1 --branch "$TAG" "$AUTH_URL" repo; then
echo "Cloned tag ${TAG}"
clone_ok=true
break 2
fi
sleep $((attempt * 5))
done
done

if [ "$clone_ok" != true ]; then
echo "::error::Failed to clone tag ${RELEASE_TAG} from Gitee. Push the tag first: git tag ${RELEASE_TAG#v} && git push origin ${RELEASE_TAG#v}"
exit 1
fi

- name: Install NSIS (Windows)
if: matrix.platform == 'windows'
shell: pwsh
run: choco install nsis -y --no-progress

- name: Install Linux deps
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: repo/package-lock.json

- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}

- uses: swatinem/rust-cache@v2
with:
workspaces: repo/src-tauri -> target

- name: Build
working-directory: repo
shell: bash
run: |
set -euo pipefail
npm ci
mkdir -p release

if [ "${{ matrix.platform }}" = "windows" ]; then
npm run tauri build -- --bundles nsis
find src-tauri/target -path '*/release/bundle/nsis/*-setup.exe' -exec cp -f {} release/ \;
elif [ "${{ matrix.platform }}" = "linux" ]; then
npm run tauri build -- --bundles deb
find src-tauri/target -path '*/release/bundle/deb/*.deb' -exec cp -f {} release/ \;
else
npm run tauri build -- --target universal-apple-darwin --bundles dmg
find src-tauri/target -path '*/release/bundle/dmg/*.dmg' -exec cp -f {} release/ \;
fi

if [ -z "$(ls -A release 2>/dev/null)" ]; then
echo "::error::No release artifacts found"
find src-tauri/target -path '*/release/bundle/*' -type f | head -50
exit 1
fi
ls -la release/

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}
path: repo/release/*

publish:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true

- name: List release files
run: find dist -type f -ls

- name: Generate release notes from git log
id: changelog
env:
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
GITEE_REPO: ${{ inputs.gitee_repo }}
RELEASE_TAG: ${{ inputs.tag }}
shell: bash
run: |
set -euo pipefail
if [ -z "${GITEE_TOKEN:-}" ]; then
echo "::error::Set secret GITEE_TOKEN on this repository"
exit 1
fi

REPO_PATH="${GITEE_REPO#https://}"
AUTH_URL="https://oauth2:${GITEE_TOKEN}@${REPO_PATH}"

git clone --filter=blob:none "$AUTH_URL" repo
cd repo
git fetch --tags origin

resolve_tag() {
local input="$1"
local candidates=("$input")
if [[ "$input" == v* ]]; then
candidates+=("${input#v}")
else
candidates+=("v${input}")
fi
for candidate in "${candidates[@]}"; do
if git rev-parse -q --verify "refs/tags/${candidate}^{commit}" >/dev/null; then
echo "$candidate"
return 0
fi
done
echo "::error::Tag ${input} not found in Gitee repository"
exit 1
}

TAG="$(resolve_tag "$RELEASE_TAG")"

mapfile -t SORTED_TAGS < <(git tag -l --sort=-version:refname)
PREV_TAG=""
for i in "${!SORTED_TAGS[@]}"; do
if [ "${SORTED_TAGS[$i]}" = "$TAG" ]; then
if [ $((i + 1)) -lt ${#SORTED_TAGS[@]} ]; then
PREV_TAG="${SORTED_TAGS[$((i + 1))]}"
fi
break
fi
done

if [ -n "$PREV_TAG" ]; then
RANGE="${PREV_TAG}..${TAG}"
HEADER="## Changes since ${PREV_TAG}"
else
RANGE="$TAG"
HEADER="## Initial release"
fi

NOTES="$(git log "$RANGE" --pretty=format:'- %s (%h)' --no-merges || true)"
if [ -z "$NOTES" ]; then
NOTES="- No commits in this range"
fi

BODY="${HEADER}

${NOTES}"

{
echo 'body<<CHANGELOG_EOF'
echo "$BODY"
echo 'CHANGELOG_EOF'
} >> "$GITHUB_OUTPUT"

- name: Publish to GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.tag }}
target_commitish: main
name: ZImgTools ${{ inputs.tag }}
body: ${{ steps.changelog.outputs.body }}
files: dist/**
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

小结

这套方案的核心思路:

  1. 源码与发布分离 — Gitee 管代码,GitHub 管安装包
  2. 构建全部上云 — 三平台 matrix,本机零构建环境依赖
  3. workflow 随源码版本管理 — 发版时 ensure-release-gha 同步到发布仓,改 workflow 不用去 GitHub 网页手改
  4. 更新日志来自 git log — 自动取上一 tag 到当前 tag 之间的 commit,写入 Release 说明与应用内更新提示
  5. 与应用内更新联动 — Release 资源命名需与 updater 后缀规则一致(ZImgTools_*-setup.exe 等);发版后客户端通过 GitHub API 拉取本仓 latest Release

本地开发构建仍可用 npm run tauri build(Windows 下自动出 NSIS),与发版流程无关。

发版检查清单

  1. 执行 scripts\set-version.bat 升级版本并提交(含 src-tauri/icons/ 图标若有更新)
  2. 代码已 push 到 Gitee,git taggit push origin <tag>
  3. 发布仓 Secret GITEE_TOKEN 有效(日志中为 ***
  4. 执行 scripts\publish-release.bat,用 gh run watch -R psvmc/z-img-tools-releases 查看进度