Skip to content

Commit 66368ca

Browse files
committed
feat(updater): add stable and preview release channels
Publish signed preview packages from successful main CI runs and keep stable updates isolated to formal releases. Reject unsupported architectures and installer formats before checking the preview endpoint.
1 parent 9d01c8c commit 66368ca

25 files changed

Lines changed: 1522 additions & 86 deletions

.github/workflows/ci.yml

Lines changed: 349 additions & 8 deletions
Large diffs are not rendered by default.

.github/workflows/release.yml

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,14 @@ jobs:
9494
shell: bash
9595
run: |
9696
set -euo pipefail
97-
previous_tag="$(git describe --tags --abbrev=0 "${GITHUB_SHA}^" 2>/dev/null || true)"
98-
arguments=(
99-
--to "${GITHUB_SHA}"
100-
--version "${GITHUB_REF_NAME}"
101-
--repository "${GITHUB_REPOSITORY}"
102-
--output "${NOTES_PATH}"
97+
node scripts/generate-changelog.mjs \
98+
--auto-from \
99+
--auto-from-ref "${GITHUB_SHA}^" \
100+
--to "${GITHUB_SHA}" \
101+
--version "${GITHUB_REF_NAME}" \
102+
--repository "${GITHUB_REPOSITORY}" \
103+
--output "${NOTES_PATH}" \
103104
--strict
104-
)
105-
if [[ -n "${previous_tag}" ]]; then
106-
arguments+=(--from "${previous_tag}")
107-
fi
108-
node scripts/generate-changelog.mjs "${arguments[@]}"
109105
110106
- name: Create or reuse one draft
111107
id: release

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ chore(release): bump version to 1.4.10
135135

136136
版本标签使用对应的 `v<version>`,例如 `v1.4.10`。标签本身不替代符合规范的提交标题。
137137

138+
`main` 的成功 CI 会自动创建下一补丁版本的测试标签,例如 `v1.4.10-beta.123.1`。这些标签和 `ci-latest` 滚动通道由工作流维护,不应手动创建、移动或用于正式发布;正式 Changelog 查找起点时只识别 `v<major>.<minor>.<patch>` 稳定标签,避免测试标签截断正式版本的变更记录。
139+
138140
## 本地检查
139141

140142
格式化前端、配置、文档和 Rust 代码:

README.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
- 房间独立设置:自动录制、录制模式、分段、弹幕、画质、封面和标题过滤
2525
- 房间详情:分别读取房间信息、录制统计和 IO 统计,并显示 objectId
2626
- 文件名模板工具:提供标准、按月、按分区和后期友好预设,并使用真实房间上下文预览路径
27-
- 应用内更新:自动或手动检查 GitHub Releases,显示版本说明与下载进度,签名验证通过后安装并重启
27+
- 应用内更新:可选择稳定版或测试版通道,显示版本说明与下载进度,签名验证通过后安装并重启
2828

2929
## 通讯边界
3030

@@ -50,7 +50,7 @@ Rust 后端命令
5050

5151
## 下载与平台支持
5252

53-
Pull Request 会自动执行格式化、Changelog、前端构建、测试和 Rust Clippy 检查。每次推送 `main` 在完整检查通过后,都会构建并上传 Windows x64、Linux x64 和 macOS Universal 三组临时 CI 包;也可在 GitHub Actions 中手动运行。带 `v` 前缀的版本标签仍会使用完整平台矩阵,自动发布带 Tauri 更新签名的正式 Release。
53+
Pull Request 会自动执行格式化、Changelog、前端构建、测试和 Rust Clippy 检查。每次推送 `main` 在完整检查通过后,都会构建 Windows x64(NSIS)、Linux x64(AppImage、DEB)和 macOS Universal 三组 CI 包,同时使用 Tauri 更新密钥签名并发布经过匿名下载验证的测试版。带 `v` 前缀的正式版本标签仍会使用完整平台矩阵,自动发布稳定 Release。
5454

5555
| 系统 | 架构 | 安装包 | 便携包 |
5656
| ------- | ------------------------------ | ------------------ | ------------ |
@@ -60,7 +60,7 @@ Pull Request 会自动执行格式化、Changelog、前端构建、测试和 Rus
6060
| macOS | Intel x64、Apple Silicon ARM64 | APP、DMG | APP ZIP |
6161
| macOS | Universal | APP、DMG | APP ZIP |
6262

63-
CI 的临时产物保留 7 天;正式版本可从 [GitHub Releases](https://github.com/demogest/BiliRecControl/releases) 长期下载。便携包不等于完全不写本机数据,限制与运行要求见 [PORTABLE.md](PORTABLE.md)
63+
Actions 中的临时产物保留 7 天;签名测试版保留 14 天(当前滚动通道指向的版本不会被清理),正式版本可从 [GitHub Releases](https://github.com/demogest/BiliRecControl/releases) 长期下载。便携包不等于完全不写本机数据,限制与运行要求见 [PORTABLE.md](PORTABLE.md)
6464

6565
Windows 和 macOS 的操作系统级可信代码签名需要购买相应开发者证书,目前 CI 生成的是未做系统证书签名的包,首次运行可能出现安全提示。应用内更新使用独立的 Tauri 签名机制,下载内容被篡改时客户端会拒绝安装。
6666

@@ -119,15 +119,26 @@ npm run changelog
119119

120120
## 应用内更新
121121

122-
桌面程序启动 4 秒后会静默检查稳定更新通道,也可点击顶栏的下载图标手动检查。发现新版本后,先由用户下载并完成签名校验,再由用户确认“安装并重启”,避免下载完成后立即打断当前操作。检查、下载、校验、安装和重启错误会分别提示;下载失败时也可直接前往 Release 页面手动安装。
122+
桌面程序启动 4 秒后会静默检查用户选择的更新通道,也可点击顶栏的下载图标手动检查。更新窗口提供两个持久化选项:
123123

124-
更新源固定为:
124+
- 稳定版:只读取正式 Release,适合日常使用。
125+
- 测试版:读取 `main` 最新成功 CI 生成的签名测试包,版本采用下一补丁的 `beta.<run>.<attempt>` SemVer。
126+
127+
测试版仅支持 Windows x64 的 NSIS 安装、Linux x64 的 AppImage/DEB 安装,以及 Intel/Apple Silicon 均可运行的 macOS Universal 包。客户端会在 Rust 原生层同时检查编译目标和当前安装格式;Windows ARM64、Linux ARM64、musl、Windows GNU、i686,以及 Windows MSI、Linux RPM 等没有兼容测试包的客户端无法选择或请求测试版,但仍可正常使用稳定版。
128+
129+
发现新版本后,先由用户下载并完成签名校验,再由用户确认“安装并重启”,避免下载完成后立即打断当前操作。切换通道会释放旧更新资源并重新检查;下载或安装期间会锁定通道,避免混用不同来源。
130+
131+
更新清单分别为:
125132

126133
```text
134+
稳定版
127135
https://github.com/demogest/BiliRecControl/releases/latest/download/latest.json
136+
137+
测试版
138+
https://github.com/demogest/BiliRecControl/releases/download/ci-latest/latest.json
128139
```
129140

130-
Tauri 会校验更新元数据和安装包签名。私钥一旦丢失,现有客户端将无法验证以后使用新密钥发布的版本,因此必须另行做好安全备份,且不能提交到 Git。
141+
测试包不会直接使用需要登录且会过期的 GitHub Actions artifact;CI 会先将签名安装包发布为公开 prerelease,验证匿名下载后才更新滚动测试清单。稳定版和测试版都使用同一 Tauri 公钥校验,私钥一旦丢失,现有客户端将无法验证以后使用新密钥发布的版本,因此必须另行做好安全备份,且不能提交到 Git。
131142

132143
如果已发布版本的 `latest.json` 出现元数据错误,可手动运行 [Repair updater manifest](.github/workflows/repair-updater-manifest.yml) 并填写版本标签。该工作流只会依据现有签名资产重建、替换和匿名验证更新清单,不会重新构建安装包;只有明确勾选时才会将该版本设为最新版本。
133144

app/globals.css

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2203,8 +2203,141 @@ main {
22032203
overflow-y: auto;
22042204
}
22052205

2206+
.update-channel-selector {
2207+
display: flex;
2208+
flex-direction: column;
2209+
gap: 9px;
2210+
padding: 11px;
2211+
border: 1px solid var(--line);
2212+
border-radius: 12px;
2213+
background: rgba(5, 10, 16, 0.48);
2214+
}
2215+
2216+
.update-channel-selector > header {
2217+
display: flex;
2218+
align-items: center;
2219+
justify-content: space-between;
2220+
gap: 12px;
2221+
}
2222+
2223+
.update-channel-selector > header > div {
2224+
display: flex;
2225+
min-width: 0;
2226+
flex-direction: column;
2227+
gap: 2px;
2228+
}
2229+
2230+
.update-channel-selector > header strong {
2231+
color: var(--text-soft);
2232+
font-size: 9px;
2233+
}
2234+
2235+
.update-channel-selector > header span {
2236+
color: #657489;
2237+
font-size: 8px;
2238+
}
2239+
2240+
.update-channel-badge {
2241+
padding: 3px 7px;
2242+
flex: 0 0 auto;
2243+
border: 1px solid rgba(62, 231, 211, 0.16);
2244+
border-radius: 100px;
2245+
color: #7bded2;
2246+
background: rgba(62, 231, 211, 0.05);
2247+
font-family: 'Space Grotesk', sans-serif;
2248+
font-size: 7px;
2249+
letter-spacing: 0.08em;
2250+
}
2251+
2252+
.update-channel-badge.is-preview {
2253+
color: #b6aaff;
2254+
border-color: rgba(155, 134, 255, 0.2);
2255+
background: rgba(155, 134, 255, 0.06);
2256+
}
2257+
2258+
.update-channel-selector > div {
2259+
display: grid;
2260+
grid-template-columns: 1fr 1fr;
2261+
gap: 8px;
2262+
}
2263+
2264+
.update-channel-selector > div > button {
2265+
display: flex;
2266+
min-width: 0;
2267+
align-items: center;
2268+
gap: 9px;
2269+
min-height: 48px;
2270+
padding: 8px 10px;
2271+
border: 1px solid rgba(157, 176, 202, 0.1);
2272+
border-radius: 9px;
2273+
color: #6d7c90;
2274+
background: rgba(255, 255, 255, 0.018);
2275+
cursor: pointer;
2276+
text-align: left;
2277+
transition: 150ms ease;
2278+
}
2279+
2280+
.update-channel-selector > div > button:hover:not(:disabled) {
2281+
color: #a7b7ca;
2282+
border-color: rgba(157, 176, 202, 0.2);
2283+
background: rgba(255, 255, 255, 0.035);
2284+
}
2285+
2286+
.update-channel-selector > div > button.is-active {
2287+
color: #72ded2;
2288+
border-color: rgba(62, 231, 211, 0.25);
2289+
background: rgba(62, 231, 211, 0.055);
2290+
box-shadow: inset 0 0 0 1px rgba(62, 231, 211, 0.035);
2291+
}
2292+
2293+
.update-channel-selector > div > button.is-active.is-preview {
2294+
color: #b6aaff;
2295+
border-color: rgba(155, 134, 255, 0.27);
2296+
background: rgba(155, 134, 255, 0.065);
2297+
box-shadow: inset 0 0 0 1px rgba(155, 134, 255, 0.04);
2298+
}
2299+
2300+
.update-channel-selector > div > button:disabled {
2301+
cursor: not-allowed;
2302+
opacity: 0.42;
2303+
}
2304+
2305+
.update-channel-selector > div > button > svg {
2306+
flex: 0 0 auto;
2307+
}
2308+
2309+
.update-channel-selector > div > button > span {
2310+
display: flex;
2311+
min-width: 0;
2312+
flex-direction: column;
2313+
gap: 3px;
2314+
}
2315+
2316+
.update-channel-selector > div > button strong {
2317+
color: currentColor;
2318+
font-size: 9px;
2319+
}
2320+
2321+
.update-channel-selector > div > button small {
2322+
overflow: hidden;
2323+
color: #5d6b7f;
2324+
font-size: 7px;
2325+
text-overflow: ellipsis;
2326+
white-space: nowrap;
2327+
}
2328+
2329+
.update-channel-selector > p {
2330+
margin: 0;
2331+
padding: 7px 9px;
2332+
border-radius: 7px;
2333+
color: #98757d;
2334+
background: rgba(255, 84, 113, 0.035);
2335+
font-size: 8px;
2336+
line-height: 1.55;
2337+
}
2338+
22062339
.update-modal .update-state {
2207-
min-height: 230px;
2340+
min-height: 180px;
22082341
}
22092342

22102343
.update-state {

components/AboutCenter.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { Code2, ExternalLink, Info, MonitorCog, ShieldCheck, Sparkles, X } from
44
import { useCallback, useEffect, useState } from 'react';
55
import { createPortal } from 'react-dom';
66
import { openExternalUrl } from '@/lib/api';
7+
import { updateChannelLabel, type UpdateChannel } from '@/lib/update-channel';
78
import type { ToastItem } from '@/lib/types';
89

910
type Props = {
11+
updateChannel: UpdateChannel;
1012
notify: (message: string, tone?: ToastItem['tone']) => void;
1113
};
1214

@@ -17,7 +19,7 @@ function runningInTauri() {
1719
return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
1820
}
1921

20-
export default function AboutCenter({ notify }: Props) {
22+
export default function AboutCenter({ updateChannel, notify }: Props) {
2123
const [portalReady, setPortalReady] = useState(false);
2224
const [open, setOpen] = useState(false);
2325
const [appName, setAppName] = useState('BiliRec Control');
@@ -156,7 +158,7 @@ export default function AboutCenter({ notify }: Props) {
156158
<section className="about-meta">
157159
<div>
158160
<span>更新通道</span>
159-
<strong>正式版</strong>
161+
<strong>{updateChannelLabel(updateChannel)}</strong>
160162
</div>
161163
<div>
162164
<span>开源许可</span>

components/Dashboard.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ import StreamerAvatar from '@/components/StreamerAvatar';
4343
import UpdateCenter from '@/components/UpdateCenter';
4444
import { useRoomAvatars } from '@/lib/use-room-avatars';
4545
import { parseBilibiliRoomId } from '@/lib/room-input';
46+
import {
47+
readStoredUpdateChannel,
48+
storeUpdateChannel,
49+
type UpdateChannel
50+
} from '@/lib/update-channel';
4651
import {
4752
finiteNumber,
4853
formatBytes,
@@ -414,6 +419,7 @@ export default function Dashboard() {
414419
const [confirmBusy, setConfirmBusy] = useState(false);
415420
const [toasts, setToasts] = useState<ToastItem[]>([]);
416421
const [clock, setClock] = useState<Date | null>(null);
422+
const [updateChannel, setUpdateChannel] = useState<UpdateChannel>('stable');
417423
const logCursor = useRef(0);
418424
const toastSequence = useRef(0);
419425
const connectionAttempted = useRef(false);
@@ -432,6 +438,11 @@ export default function Dashboard() {
432438
}, 3600);
433439
}, []);
434440

441+
const changeUpdateChannel = useCallback((channel: UpdateChannel) => {
442+
setUpdateChannel(channel);
443+
storeUpdateChannel(channel);
444+
}, []);
445+
435446
useEffect(() => {
436447
const saved = localStorage.getItem(STORAGE_KEY);
437448
let restored = DEFAULT_SETTINGS;
@@ -444,6 +455,7 @@ export default function Dashboard() {
444455
}
445456
setSettings(restored);
446457
setDraftSettings(restored);
458+
setUpdateChannel(readStoredUpdateChannel());
447459
setSettingsOpen(!isConfigured(restored));
448460
setHydrated(true);
449461
}, []);
@@ -814,8 +826,12 @@ export default function Dashboard() {
814826
<button className="top-icon-button" type="button" onClick={toggleFullscreen} title="全屏">
815827
<Maximize2 size={18} />
816828
</button>
817-
<UpdateCenter notify={pushToast} />
818-
<AboutCenter notify={pushToast} />
829+
<UpdateCenter
830+
channel={updateChannel}
831+
onChannelChange={changeUpdateChannel}
832+
notify={pushToast}
833+
/>
834+
<AboutCenter updateChannel={updateChannel} notify={pushToast} />
819835
<button className="connection-button" type="button" onClick={openSettings}>
820836
<span className={connected ? 'online' : ''} />
821837
<div>

0 commit comments

Comments
 (0)