feat: ship verified platform upgrades with channel bundle releases

Package production dependencies per architecture for online and offline upgrades.
Validate dependencies and isolated backend startup before readiness, repair once
when permitted, and preserve service/data through failed preparation or rollback.

Publish release bundles with master images and expose an opt-in develop build
sharing the development index across Alpine and Debian images.
This commit is contained in:
whyour
2026-10-18 22:19:49 +08:00
parent a36a77086a
commit e8b6a30bd9
53 changed files with 5271 additions and 122 deletions
+7 -5
View File
@@ -2,7 +2,10 @@ import { CloudDownloadOutlined } from '@ant-design/icons';
import { Modal, Tooltip } from 'antd';
import { useState } from 'react';
import intl from 'react-intl-universal';
import useUpgrade, { activeStatuses, phaseLabels } from '@/hooks/useUpgrade';
import useUpgrade, {
activeStatuses,
upgradeStatusLabel,
} from '@/hooks/useUpgrade';
import CheckUpdate from '@/pages/setting/checkUpdate';
export default function UpgradeNotice({
@@ -21,16 +24,15 @@ export default function UpgradeNotice({
if (!showNotice && !open) return null;
const text = !connected
? intl.get('连接已断开,正在重新连接')
: job.status === 'failed'
? intl.get('更新失败')
: intl.get(phaseLabels[job.phase || ''] || '检查更新');
: intl.get(upgradeStatusLabel(job));
const label =
activeStatuses.includes(job.status) &&
job.phase === 'downloading' && job.percent != null
? `${text} ${job.percent}%`
: text;
return (
<>
{showNotice && (
{showNotice && !open && (
<Tooltip title={label} placement="right">
<button
type="button"
+15
View File
@@ -25,11 +25,26 @@ export const phaseLabels: Record<string, string> = {
extracting: '正在解压更新包',
verifying: '正在校验更新包',
dependencies: '正在准备依赖',
prestarting: '正在预启动验证',
repairing: '正在修复运行依赖',
ready: '更新已就绪',
restarting: '正在重启并更新',
completed: '更新完成',
};
// Status is authoritative; old progress fields must not claim completion.
export function upgradeStatusLabel(job: UpgradeJob): string {
if (job.status === 'ready') return '下载完成,等待重启';
if (job.status === 'completed') return '升级成功';
if (job.status === 'failed') return '更新失败';
if (activeStatuses.includes(job.status)) {
return !['ready', 'completed'].includes(job.phase || '')
? phaseLabels[job.phase || ''] || '正在准备更新'
: '正在准备更新';
}
return '检查更新';
}
// The sidebar and settings share one poller and one mutation state.
export function createUpgradeStore() {
let snapshot = {
+5
View File
@@ -346,6 +346,11 @@ select:-webkit-autofill:focus {
.ant-pro-global-header {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
.ant-pro-global-header-collapsed-button {
min-width: 0;
max-width: 100%;
}
}
.ant-menu-item.ant-pro-sider-collapsed-button {
+3
View File
@@ -734,6 +734,8 @@
"从开始至load总耗时": "Total load time: {duration} ms",
"正在解压更新包": "Extracting update",
"正在校验更新包": "Verifying update",
"正在预启动验证": "Verifying backend startup",
"正在修复运行依赖": "Repairing runtime dependencies",
"正在准备依赖": "Preparing dependencies",
"更新已就绪": "Update ready",
"正在重启并更新": "Restarting and updating",
@@ -744,6 +746,7 @@
"更新已就绪,重启后应用新版本": "Update ready. Restart to apply the new version.",
"更新在后台进行,可以继续使用面板": "The update runs in the background. You can keep using the panel.",
"下载构建产物": "Downloading application files",
"下载更新包": "Downloading update package",
"下载源码": "Downloading source files",
"正在获取下载大小": "Total size unavailable",
"查看更新日志": "View update log",
+3
View File
@@ -734,6 +734,8 @@
"从开始至load总耗时": "从开始至 load 总耗时: {duration} ms",
"正在解压更新包": "正在解压更新包",
"正在校验更新包": "正在校验更新包",
"正在预启动验证": "正在预启动验证",
"正在修复运行依赖": "正在修复运行依赖",
"正在准备依赖": "正在准备依赖",
"更新已就绪": "更新已就绪",
"正在重启并更新": "正在重启并更新",
@@ -744,6 +746,7 @@
"更新已就绪,重启后应用新版本": "更新已就绪,重启后应用新版本",
"更新在后台进行,可以继续使用面板": "更新在后台进行,可以继续使用面板",
"下载构建产物": "下载构建产物",
"下载更新包": "下载更新包",
"下载源码": "下载源码",
"正在获取下载大小": "正在获取下载大小",
"查看更新日志": "查看更新日志",
+5 -10
View File
@@ -5,7 +5,7 @@ import { Button, Modal, Progress, Space, Typography, message } from 'antd';
import { useState } from 'react';
import useUpgrade, {
activeStatuses,
phaseLabels,
upgradeStatusLabel,
reloadKey,
upgradeStore,
} from '@/hooks/useUpgrade';
@@ -100,14 +100,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
onOk: () => reloadSystem(true),
});
const label =
job.status === 'failed'
? intl.get('更新失败')
: ready
? intl.get('下载完成,等待重启')
: completed
? intl.get('升级成功')
: intl.get(phaseLabels[job.phase || ''] || '正在准备更新');
const label = intl.get(upgradeStatusLabel(job));
const hasJob = job.status !== 'idle' && job.status !== 'loading';
const actions = !active && (
<Space wrap>
@@ -167,7 +160,9 @@ const CheckUpdate = ({ systemInfo }: any) => {
<div className="upgrade-progress-meta">
<Typography.Text type="secondary">
{intl.get(
job.artifact === 'qinglong-static'
job.artifact === 'qinglong-bundle'
? '下载更新包'
: job.artifact === 'qinglong-static'
? '下载构建产物'
: '下载源码',
)}
+24 -22
View File
@@ -1,27 +1,29 @@
import { useState, useEffect, useMemo } from 'react';
import browserType from './index';
import { useState, useEffect } from 'react';
export const useCtx = () => {
const [width, setWidth] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-48);
const [isPhone, setIsPhone] = useState(false);
const { platform } = useMemo(() => browserType(), []);
// Match ProLayout's xs/sm breakpoint, including narrow desktop windows.
const [isPhone, setIsPhone] = useState(
() =>
typeof window !== 'undefined' &&
window.matchMedia('(max-width: 767.99px)').matches,
);
useEffect(() => {
if (platform === 'mobile' && document.body.offsetWidth < 768) {
setWidth('auto');
setMarginLeft(0);
setMarginTop(0);
setIsPhone(true);
document.body.setAttribute('data-mode', 'phone');
} else {
setWidth('100%');
setMarginLeft(0);
setMarginTop(-48);
setIsPhone(false);
document.body.setAttribute('data-mode', 'desktop');
const media = window.matchMedia('(max-width: 767.99px)');
const update = () => {
setIsPhone(media.matches);
document.body.setAttribute(
'data-mode',
media.matches ? 'phone' : 'desktop',
);
};
update();
if (typeof media.addEventListener === 'function') {
media.addEventListener('change', update);
return () => media.removeEventListener('change', update);
}
media.addListener(update);
return () => media.removeListener(update);
}, []);
return {
@@ -31,9 +33,9 @@ export const useCtx = () => {
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
marginTop: isPhone ? 0 : -48,
width: isPhone ? 'auto' : '100%',
marginLeft: 0,
} as any,
isPhone,
};