fix(deploy): 系统更新 - 非正式镜像标签可在线更新到正式版

sha-* 等非正式标签不再被字典序判成比 vX.Y.Z 更新;健康检查对非正式目标版本只确认服务就绪;管理后台更新页并排展示当前运行与最新发布版本并标注非正式版本
This commit is contained in:
ddCat
2026-09-18 08:50:18 +08:00
parent a7a1d5e6e7
commit 52913939d0
8 changed files with 99 additions and 11 deletions

View File

@@ -156,7 +156,11 @@ func (m *Manager) Check(ctx context.Context) (Status, error) {
}
if CompareVersions(current, release.Version) < 0 {
m.state.Operation.Phase = PhaseReady
m.appendLogLocked(PhaseReady, fmt.Sprintf("发现新版本 %s", release.Version))
if IsReleaseVersion(current) {
m.appendLogLocked(PhaseReady, fmt.Sprintf("发现新版本 %s", release.Version))
} else {
m.appendLogLocked(PhaseReady, fmt.Sprintf("当前部署为 %s,可更新到正式版 %s", current, release.Version))
}
} else {
m.state.Operation.Phase = PhaseNoUpdate
m.appendLogLocked(PhaseNoUpdate, "当前已是最新版本")
@@ -430,6 +434,8 @@ func (m *Manager) checks(current string, currentErr error) []Check {
if currentErr != nil {
items[1].Status = "failed"
items[1].Detail = currentErr.Error()
} else if current != "" && !IsReleaseVersion(current) {
items[1].Detail = current + " · 非正式镜像标签,可切换到正式 Release"
}
if m.state.LastBackup != nil {
items[2].Status = "passed"

View File

@@ -518,7 +518,7 @@ func (m *Manager) checkHealthOnce(healthURL, targetVersion string) error {
if payload.Code != 0 {
return errors.New("健康接口业务状态异常")
}
if payload.Data.Build.Version != "" && CompareVersions(payload.Data.Build.Version, targetVersion) != 0 {
if IsReleaseVersion(targetVersion) && payload.Data.Build.Version != "" && CompareVersions(payload.Data.Build.Version, targetVersion) != 0 {
return fmt.Errorf("运行版本仍为 %s,期望 %s", payload.Data.Build.Version, targetVersion)
}
return nil

View File

@@ -10,12 +10,28 @@ type versionPart struct {
pre []string
}
func IsReleaseVersion(raw string) bool {
_, ok := parseVersion(raw)
return ok
}
func CompareVersions(left, right string) int {
a, aok := parseVersion(left)
b, bok := parseVersion(right)
if !aok || !bok {
return strings.Compare(strings.TrimSpace(left), strings.TrimSpace(right))
if aok && bok {
return compareParsedVersions(a, b)
}
// sha-5509b17、latest 等非正式标签不能按字典序和 v1.5.0 比,否则会被当成更新而不让升级。
if !aok && bok {
return -1
}
if aok && !bok {
return 1
}
return strings.Compare(strings.TrimSpace(left), strings.TrimSpace(right))
}
func compareParsedVersions(a, b versionPart) int {
for _, pair := range [][2]int{{a.major, b.major}, {a.minor, b.minor}, {a.patch, b.patch}} {
if pair[0] < pair[1] {
return -1

View File

@@ -12,6 +12,10 @@ func TestCompareVersions(t *testing.T) {
{"v1.2.0-preview.3", "v1.2.0", -1},
{"v1.2.1", "v1.2.0", 1},
{"v1.2.0", "v1.2.0", 0},
{"vsha-5509b17", "v1.5.0", -1},
{"sha-5509b17", "v1.5.0", -1},
{"v1.5.0", "vsha-5509b17", 1},
{"v1.2.9", "v1.5.0", -1},
}
for _, test := range tests {
got := CompareVersions(test.left, test.right)

View File

@@ -13,8 +13,8 @@ description: Host Updater、Release 检查、数据库备份迁移、健康验
## 安全更新顺序
1. 从 GitHub Releases 读取包含预览版在内的最高语义版本,并与 `.env` 中固定的 `CANVAS_IMAGE_TAG` 比较。
2. 下载目标版本的 `docker-compose.deploy.yml`,执行 `docker compose config --quiet`,检查部署文件、磁盘空间和当前版本。
1. 从 GitHub Releases 读取包含预览版在内的最高语义版本,并与 `.env` 中固定的 `CANVAS_IMAGE_TAG` 比较。`sha-*` 这类非正式镜像标签一律视为低于正式 Release,避免把 `vsha-5509b17` 字典序比较成比 `v1.5.0` 更新。
2. 下载目标版本的 `docker-compose.deploy.yml`,执行 `docker compose config --quiet`,检查部署文件、磁盘空间和当前版本。当前标签不是 `vX.Y.Z` 时,健康检查只确认服务就绪,不要求构建号和镜像标签逐字相同。
3. 创建 `backup-YYYYMMDD-HHMMSS.zip.partial`,其中包含 PostgreSQL custom-format dump、后端数据目录 tar 和元数据;写入完成后 `fsync`、原子改名,并复核 ZIP 结构和 SHA-256。
4. 拉取目标 backend/web 镜像并要求存在仓库 `sha256` 摘要。任何失败都在停服前中止。
5. 停止 web/backend,等待 Backend 的 SIGTERM drain 完成;PostgreSQL 和 Redis 保持运行。

View File

@@ -5,6 +5,13 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
# 待测试
## 开发镜像标签可在线更新到正式版(2026-09-18)
`CANVAS_IMAGE_TAG=sha-5509b17` 这类非正式标签不再被字典序判成比 `v1.5.0` 更新。待登录管理后台「系统配置 / 系统更新」:
- [ ] 当前运行显示 `vsha-...`,最新发布显示 `v1.5.0`(或当前正式版),状态是可更新而不是「已是最新」。
- [ ] 「开始更新」可点;更新完成后 `CANVAS_IMAGE_TAG` 变成正式版本号,页面两边版本一致。
## 创作参数弹窗表面色(2026-09-17)
画幅 / 清晰度 / 时长弹窗改用工作台 `--user-surface-raised`,选中块用 `--user-control-pressed`,不再画 1px 描边。待登录后打开视频生成设置:

View File

@@ -178,7 +178,7 @@ export default function SystemUpdatePage() {
layout="stacked"
icon={<ServerCog className="size-4" />}
title="可用版本"
description={status?.updateAvailable ? "发现比当前运行版本更新的发布构建。" : "检查 GitHub Release 与当前运行版本。"}
description={status?.updateAvailable ? `当前运行 ${status.currentVersion},可更新到 ${status.latestRelease?.version}。` : "检查 GitHub Release 与当前运行的镜像标签。"}
status={<PhaseBadge phase={status?.operation.phase ?? "idle"} />}
footer={
<>
@@ -190,12 +190,27 @@ export default function SystemUpdatePage() {
}
>
<div className="admin-system-update-release-body">
<div className="admin-system-update-version-row">
<strong>{status?.latestRelease?.version || "尚未检查"}</strong>
{status?.latestRelease?.prerelease ? <AdminStatusBadge label="预览版" tone="warning" /> : null}
<div className="admin-system-update-versions">
<div>
<span>当前运行</span>
<strong>{status?.currentVersion || "未识别"}</strong>
{status?.currentVersion && !isReleaseVersion(status.currentVersion) ? <AdminStatusBadge label="非正式版本" tone="warning" /> : null}
</div>
<div>
<span>最新发布</span>
<strong>{status?.latestRelease?.version || "尚未检查"}</strong>
{status?.latestRelease?.prerelease ? <AdminStatusBadge label="预览版" tone="warning" /> : null}
{status?.updateAvailable ? <AdminStatusBadge label="可更新" tone="success" /> : null}
</div>
</div>
{status?.updateAvailable && status.latestRelease ? (
<p className="admin-system-update-hint">
{isReleaseVersion(status.currentVersion)
? `可以从 ${status.currentVersion} 更新到 ${status.latestRelease.version}。`
: `当前镜像标签不是正式版本号,所以会显示成 ${status.currentVersion}。点开始更新后会切换到 ${status.latestRelease.version}。`}
</p>
) : null}
<dl className="admin-system-update-facts">
<dt>当前版本</dt><dd>{status?.currentVersion || "未识别"}</dd>
<dt>部署方式</dt><dd>{status?.deployment || "未知"}</dd>
<dt>发布时间</dt><dd>{formatDate(status?.latestRelease?.publishedAt)}</dd>
<dt>代码仓库</dt><dd>{status?.repository || "ddcat-ai/open-ai-canvas"}</dd>
@@ -282,3 +297,7 @@ function formatBytes(value: number) {
const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
return `${(value / 1024 ** index).toFixed(index > 1 ? 2 : 0)} ${units[index]}`;
}
function isReleaseVersion(value?: string) {
return Boolean(value && /^v?\d+\.\d+\.\d+/.test(value));
}

View File

@@ -9980,6 +9980,42 @@ html[data-magicui-theme-vt="active"] .admin-shell * {
padding: 16px;
}
.admin-system-update-versions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 18px;
padding-bottom: 14px;
}
.admin-system-update-versions > div {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-items: baseline;
gap: 8px;
}
.admin-system-update-versions span {
width: 100%;
color: var(--admin-text-muted);
font-size: 11px;
font-weight: 600;
}
.admin-system-update-versions strong {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
font-size: clamp(18px, 2.4vw, 26px);
line-height: 1.1;
letter-spacing: -0.04em;
}
.admin-system-update-hint {
margin: 0 0 14px;
color: var(--admin-text-secondary, var(--admin-text-muted));
font-size: 12px;
line-height: 1.65;
}
.admin-system-update-version-row {
display: flex;
align-items: center;