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:
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user