1
0
forked from boos/BoosAPI

Keep recent Docker tag deploys explicit and tag-safe

Review feedback pointed out two small but important operator-facing gaps in the
recent-tag card: one-click deploy should only be available when a real raw
Docker tag exists, and the digest that will be deployed should be visible on
the card instead of remaining implicit. This patch tightens the candidate
filter to require tagName and renders the digest metadata before the deploy
actions, with regression coverage for both behaviors.

Constraint: The deploy API still requires a raw Docker tag, not a normalized display value
Constraint: The recent-tag card should surface the exact digest operators are about to deploy
Rejected: Keep accepting normalizedVersion-only candidates | could enable one-click deploy with a non-tag display value
Rejected: Rely on displayVersion alone for digest visibility | digest presence should stay explicit even if display text changes later
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Any future recent-tag UI should treat tagName as the deployable identity and digest as separately visible metadata
Tested: src/web/pages/settings/UpdateCenterSection.test.tsx, npm run typecheck, git diff --check
Not-tested: Full PR checks rerun pending after push
This commit is contained in:
cita-777
2026-04-18 16:09:24 +08:00
parent 93912bb330
commit fa6edec83a
2 changed files with 22 additions and 3 deletions
@@ -88,6 +88,12 @@ describe('UpdateCenterSection', () => {
displayVersion: 'dev-20260417-f67ade2 @ sha256:bbbbbbbbbbbb',
publishedAt: '2026-03-30T10:54:35.591877Z',
},
{
normalizedVersion: 'feature-without-raw-tag',
digest: 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
displayVersion: 'feature-without-raw-tag @ sha256:cccccccccccc',
publishedAt: '2026-03-30T09:54:35.591877Z',
},
],
helper: {
ok: true,
@@ -475,6 +481,8 @@ describe('UpdateCenterSection', () => {
&& collectText(node).includes('dev-20260417-f67ade2 @ sha256:bbbbbbbbbbbb')
));
expect(recentTagCard).toBeTruthy();
expect(collectText(root.root)).toContain('Digest:sha256:bbbbbbbbbbbb');
expect(collectText(root.root)).not.toContain('feature-without-raw-tag');
const deployRecentButton = root.root.find((node) => (
node.type === 'button'
+14 -3
View File
@@ -221,9 +221,15 @@ function formatImageTarget(tag?: string | null, digest?: string | null) {
return '';
}
function normalizeRecentDockerCandidates(input?: UpdateCenterStatus['dockerHubRecentTags'] | null) {
type RecentDockerCandidate = NonNullable<NonNullable<UpdateCenterStatus['dockerHubRecentTags']>[number]>;
function normalizeRecentDockerCandidates(
input?: UpdateCenterStatus['dockerHubRecentTags'] | null,
): Array<RecentDockerCandidate & { tagName: string }> {
if (!Array.isArray(input)) return [];
return input.filter((entry) => String(entry?.tagName || entry?.normalizedVersion || '').trim());
return input.filter(
(entry): entry is RecentDockerCandidate & { tagName: string } => !!String(entry?.tagName || '').trim(),
);
}
export default function UpdateCenterSection() {
@@ -800,7 +806,7 @@ export default function UpdateCenterSection() {
{recentDockerCandidates.length ? (
<div style={{ display: 'grid', gap: 8 }}>
{recentDockerCandidates.map((candidate) => {
const candidateTag = String(candidate.tagName || candidate.normalizedVersion || '').trim();
const candidateTag = String(candidate.tagName || '').trim();
const candidateDigest = String(candidate.digest || '').trim();
const candidateLabel = candidate.displayVersion || candidate.normalizedVersion || candidateTag;
return (
@@ -820,6 +826,11 @@ export default function UpdateCenterSection() {
<div style={fieldHintStyle}>
最近推送:{formatTaskTime(candidate.publishedAt)}
</div>
{candidateDigest ? (
<div style={{ ...fieldHintStyle, fontFamily: 'var(--font-mono)' }} title={candidateDigest}>
Digest:{formatShortDigest(candidateDigest)}
</div>
) : null}
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button
type="button"