From 87fb5149e69e69497fefe8ad30c04c6e5165a2da Mon Sep 17 00:00:00 2001 From: cita-777 Date: Fri, 17 Apr 2026 22:43:47 +0800 Subject: [PATCH] Let operators deploy dev Docker Hub tags from the update center The update center previously surfaced stable Docker Hub candidates but left dev and branch-tag rollouts stranded in SSH and Helm commands. This adds a repo-native manual Docker tag entrypoint in Settings so operators can deploy dev, branch, temporary, or sha tags through the existing helper flow without changing the backend deploy contract. Constraint: Keep the existing helper API and digest-aware Helm contract unchanged Constraint: Preserve stable Docker Hub candidate discovery for default reminders Rejected: Expand automatic Docker Hub discovery to all branch tags | would blur stable release reminders with operator-only rollout targets Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep automatic candidate detection stable-first; manual tag deploy is the escape hatch for non-stable tags Tested: update-center focused vitest suite; typecheck; build:web; docs:build; repo:drift-check Not-tested: Full npm test remains red due to unrelated tmp/sub2api frontend suites and an existing firstByteTimeout unhandled error --- docs/k3s-update-center.md | 7 +- .../settings/UpdateCenterSection.test.tsx | 56 +++++++++ .../pages/settings/UpdateCenterSection.tsx | 111 +++++++++++++++--- 3 files changed, 156 insertions(+), 18 deletions(-) diff --git a/docs/k3s-update-center.md b/docs/k3s-update-center.md index 35177d4..17bc9be 100644 --- a/docs/k3s-update-center.md +++ b/docs/k3s-update-center.md @@ -308,6 +308,11 @@ helper 端使用: - `GitHub Releases` - `Docker Hub` +补充一点: + +- `Docker Hub` 里的**自动发现**仍然优先 `latest` / `main` / 稳定 SemVer 标签 +- 如果你要部署 `dev`、分支名、临时标签或 `sha-*` 这类非稳定标签,直接在 Docker Hub 卡片底部用“手动部署 Docker Hub 标签”填写即可 + 对已经跑起来的 K3s / Helm 用户来说,更新中心的日常配置主要就在这一页: ```text @@ -325,7 +330,7 @@ helper 端使用: - 版本来源发现了可部署版本 - 如果 Docker Hub 显示的是 `latest @ sha256:...`,说明页面已经识别到 alias tag 当前指向的具体镜像 digest - Deploy Helper 显示健康 -4. 再点部署按钮 +4. 如果你要跟随稳定候选,直接点部署按钮;如果你要切到 `dev` / 分支 / 临时标签,就在 Docker Hub 卡片底部手动填写 tag,必要时连 digest 一起填 5. 在页面下方看部署日志 6. 如果升级后发现问题,可以直接在“回退历史”里点旧 revision 回滚;只要该 revision 当时记录了 digest,就会跟着一起回到对应镜像 diff --git a/src/web/pages/settings/UpdateCenterSection.test.tsx b/src/web/pages/settings/UpdateCenterSection.test.tsx index a3e524a..50dc477 100644 --- a/src/web/pages/settings/UpdateCenterSection.test.tsx +++ b/src/web/pages/settings/UpdateCenterSection.test.tsx @@ -375,6 +375,62 @@ describe('UpdateCenterSection', () => { } }); + it('deploys manual Docker Hub tags so dev and branch images are reachable from the UI', async () => { + let root!: ReactTestRenderer; + try { + await act(async () => { + root = create( + + + + + , + ); + }); + await flushMicrotasks(); + + const manualTagInput = root.root.find((node) => ( + node.type === 'input' + && node.props.placeholder === 'dev / dev-20260417-f67ade2 / sha-f67ade2' + )); + const manualDigestInput = root.root.find((node) => ( + node.type === 'input' + && node.props.placeholder === '可选 digest:sha256:...' + )); + + await act(async () => { + manualTagInput.props.onChange({ target: { value: 'dev-20260417-f67ade2' } }); + manualDigestInput.props.onChange({ + target: { + value: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }, + }); + }); + + const customDeployButton = root.root.find((node) => ( + node.type === 'button' + && typeof node.props.onClick === 'function' + && collectText(node).includes('部署自定义 Docker 标签') + )); + + expect(customDeployButton.props.disabled).toBe(false); + + await act(async () => { + await customDeployButton.props.onClick(); + }); + await flushMicrotasks(); + + expect(apiMock.deployUpdateCenter).toHaveBeenCalledWith({ + source: 'docker-hub-tag', + targetTag: 'dev-20260417-f67ade2', + targetDigest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }); + expect(apiMock.streamUpdateCenterTaskLogs).toHaveBeenCalledWith('task-1', expect.any(Object)); + } finally { + root?.unmount(); + } + }); + it('keeps rollback history compact and opens the full revision list in a centered modal', async () => { let root!: ReactTestRenderer; try { diff --git a/src/web/pages/settings/UpdateCenterSection.tsx b/src/web/pages/settings/UpdateCenterSection.tsx index 91f09ea..d9187a7 100644 --- a/src/web/pages/settings/UpdateCenterSection.tsx +++ b/src/web/pages/settings/UpdateCenterSection.tsx @@ -223,6 +223,10 @@ export default function UpdateCenterSection() { const [deploying, setDeploying] = useState(false); const [status, setStatus] = useState(null); const [config, setConfig] = useState(DEFAULT_CONFIG); + const [manualDockerTarget, setManualDockerTarget] = useState({ + tag: '', + digest: '', + }); const [logs, setLogs] = useState([]); const [taskStatus, setTaskStatus] = useState(''); const [historyModalOpen, setHistoryModalOpen] = useState(false); @@ -426,6 +430,13 @@ export default function UpdateCenterSection() { }); const canDeployGithub = !deploying && githubDeployState.canDeploy; const canDeployDocker = !deploying && dockerDeployState.canDeploy; + const manualDockerTag = String(manualDockerTarget.tag || '').trim(); + const manualDockerDigest = String(manualDockerTarget.digest || '').trim(); + const canDeployManualDocker = !deploying + && config.enabled + && config.dockerHubTagsEnabled + && helperHealthy + && !!manualDockerTag; const helperHistory = Array.isArray(status?.helper?.history) ? status.helper.history : []; const historyPreview = helperHistory.slice(0, 2); const currentRevision = String(status?.helper?.revision || '').trim(); @@ -559,7 +570,7 @@ export default function UpdateCenterSection() { /> Docker Hub - 从镜像标签中找出最新稳定版本,适合镜像优先的发布链路。 + 自动发现优先稳定标签;dev / 分支 / 临时标签可在下方手动部署。 @@ -720,7 +731,7 @@ export default function UpdateCenterSection() { ) : null} -
适合镜像标签领先于 release 的场景,直接跟随容器分发节奏。
+
自动候选优先 latest / main / 稳定 SemVer;dev / 分支 / sha 标签可手动填写部署。
{status?.dockerHubTag?.displayVersion || status?.dockerHubTag?.normalizedVersion || '未发现'}
@@ -730,21 +741,87 @@ export default function UpdateCenterSection() {
最近推送:{formatTaskTime(status?.dockerHubTag?.publishedAt)}
- +
+ + {status?.dockerHubTag?.tagName ? ( + + ) : null} +
+
+
+ 手动部署 Docker Hub 标签 +
+
+ 自动候选不会覆盖所有分支标签。若要切到 dev、feature、临时或 sha 标签,直接在这里填写。 +
+
+ setManualDockerTarget((prev) => ({ ...prev, tag: e.target.value }))} + style={{ ...inputStyle, fontFamily: 'var(--font-mono)' }} + placeholder="dev / dev-20260417-f67ade2 / sha-f67ade2" + /> + setManualDockerTarget((prev) => ({ ...prev, digest: e.target.value }))} + style={{ ...inputStyle, fontFamily: 'var(--font-mono)' }} + placeholder="可选 digest:sha256:..." + /> +
+
+ + + digest 选填;如果 chart 当前锁定了旧 digest,建议把 tag 和 digest 一起填写。 + +
+