forked from boos/BoosAPI
Refresh the recent Docker tag PR onto current main
The release branch had fallen behind current main and only conflicted in the Update Center recent-tag test. Keep the latest main-side deploy-state coverage while preserving the PR's raw-tag filtering and digest visibility assertions. Constraint: PR #495 must merge into the current main without widening the PR diff beyond the update-center candidate behavior. Rejected: Drop the PR-specific digest/raw-tag assertions | would regress the review fixes that prompted the branch update. Confidence: high Scope-risk: narrow Directive: Keep recent Docker tag cards deployable only from real tagName values and keep digest metadata visible before deployment. Tested: npx vitest run --root . src/web/pages/settings/UpdateCenterSection.test.tsx src/server/services/updateCenterVersionService.test.ts src/server/routes/api/updateCenter.test.ts src/server/services/updateCenterStatusService.test.ts; npm run typecheck; npm run repo:drift-check; git diff --cached --check origin/main Not-tested: Full npm test.
This commit is contained in:
@@ -476,13 +476,9 @@ describe('UpdateCenterSection', () => {
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const recentTagCard = root.root.find((node) => (
|
||||
typeof node.props?.style === 'object'
|
||||
&& 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');
|
||||
expect(collectText(root.root)).toContain('dev-20260417-f67ade2 @ sha256:bbbbbbbbbbbb');
|
||||
|
||||
const deployRecentButton = root.root.find((node) => (
|
||||
node.type === 'button'
|
||||
@@ -508,6 +504,111 @@ describe('UpdateCenterSection', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('disables only the recent Docker candidate that already matches the running helper image', async () => {
|
||||
apiMock.getUpdateCenterStatus.mockResolvedValueOnce({
|
||||
currentVersion: '1.2.3',
|
||||
config: {
|
||||
enabled: true,
|
||||
helperBaseUrl: 'http://metapi-deploy-helper.ai.svc.cluster.local:9850',
|
||||
namespace: 'ai',
|
||||
releaseName: 'metapi',
|
||||
chartRef: 'oci://ghcr.io/cita-777/charts/metapi',
|
||||
imageRepository: '1467078763/metapi',
|
||||
githubReleasesEnabled: true,
|
||||
dockerHubTagsEnabled: true,
|
||||
defaultDeploySource: 'docker-hub-tag',
|
||||
},
|
||||
githubRelease: {
|
||||
normalizedVersion: '1.3.0',
|
||||
displayVersion: '1.3.0',
|
||||
},
|
||||
dockerHubTag: {
|
||||
normalizedVersion: 'latest',
|
||||
tagName: 'latest',
|
||||
digest: 'sha256:efb2ee6553866bd3268dcc54c02fa5f9789728c51ed4af63328aaba6da67df35',
|
||||
displayVersion: 'latest @ sha256:efb2ee655386',
|
||||
},
|
||||
dockerHubRecentTags: [
|
||||
{
|
||||
normalizedVersion: 'dev',
|
||||
tagName: 'dev',
|
||||
digest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
displayVersion: 'dev @ sha256:aaaaaaaaaaaa',
|
||||
publishedAt: '2026-03-30T11:54:35.591877Z',
|
||||
},
|
||||
{
|
||||
normalizedVersion: 'dev-20260417-next',
|
||||
tagName: 'dev-20260417-next',
|
||||
digest: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
displayVersion: 'dev-20260417-next @ sha256:bbbbbbbbbbbb',
|
||||
publishedAt: '2026-03-30T10:54:35.591877Z',
|
||||
},
|
||||
],
|
||||
helper: {
|
||||
ok: true,
|
||||
healthy: true,
|
||||
revision: '17',
|
||||
imageTag: 'dev',
|
||||
imageDigest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
history: [],
|
||||
},
|
||||
runtime: {
|
||||
lastCheckedAt: '2026-03-30 20:30:00',
|
||||
lastCheckError: null,
|
||||
lastResolvedSource: 'docker-hub-tag',
|
||||
lastResolvedDisplayVersion: 'dev',
|
||||
lastResolvedCandidateKey: 'docker-hub-tag:dev@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
lastNotifiedCandidateKey: null,
|
||||
lastNotifiedAt: null,
|
||||
},
|
||||
runningTask: null,
|
||||
lastFinishedTask: null,
|
||||
});
|
||||
|
||||
let root!: ReactTestRenderer;
|
||||
try {
|
||||
await act(async () => {
|
||||
root = create(
|
||||
<MemoryRouter>
|
||||
<ToastProvider>
|
||||
<UpdateCenterSection />
|
||||
</ToastProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
const runningCandidateButton = root.root.find((node) => (
|
||||
node.type === 'button'
|
||||
&& typeof node.props.onClick === 'function'
|
||||
&& collectText(node) === '部署 dev'
|
||||
));
|
||||
const nextCandidateButton = root.root.find((node) => (
|
||||
node.type === 'button'
|
||||
&& typeof node.props.onClick === 'function'
|
||||
&& collectText(node) === '部署 dev-20260417-next'
|
||||
));
|
||||
|
||||
expect(runningCandidateButton.props.disabled).toBe(true);
|
||||
expect(runningCandidateButton.props.title).toContain('当前已运行该镜像');
|
||||
expect(nextCandidateButton.props.disabled).toBe(false);
|
||||
|
||||
await act(async () => {
|
||||
await nextCandidateButton.props.onClick();
|
||||
});
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(apiMock.deployUpdateCenter).toHaveBeenCalledWith({
|
||||
source: 'docker-hub-tag',
|
||||
targetTag: 'dev-20260417-next',
|
||||
targetDigest: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
});
|
||||
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 {
|
||||
|
||||
@@ -451,10 +451,6 @@ export default function UpdateCenterSection() {
|
||||
const manualDockerTag = String(manualDockerTarget.tag || '').trim();
|
||||
const manualDockerDigest = String(manualDockerTarget.digest || '').trim();
|
||||
const recentDockerCandidates = normalizeRecentDockerCandidates(status?.dockerHubRecentTags);
|
||||
const canDeployDockerByTag = !deploying
|
||||
&& config.enabled
|
||||
&& config.dockerHubTagsEnabled
|
||||
&& helperHealthy;
|
||||
const canDeployManualDocker = !deploying
|
||||
&& config.enabled
|
||||
&& config.dockerHubTagsEnabled
|
||||
@@ -809,6 +805,15 @@ export default function UpdateCenterSection() {
|
||||
const candidateTag = String(candidate.tagName || '').trim();
|
||||
const candidateDigest = String(candidate.digest || '').trim();
|
||||
const candidateLabel = candidate.displayVersion || candidate.normalizedVersion || candidateTag;
|
||||
const candidateDeployState = describeDockerDeployState({
|
||||
enabled: config.enabled && config.dockerHubTagsEnabled,
|
||||
helperHealthy,
|
||||
helperError: status?.helper?.error,
|
||||
currentVersion: status?.currentVersion,
|
||||
helper: status?.helper,
|
||||
candidate,
|
||||
});
|
||||
const canDeployCandidate = !deploying && candidateDeployState.canDeploy;
|
||||
return (
|
||||
<div
|
||||
key={`${candidateTag}:${candidateDigest || 'no-digest'}`}
|
||||
@@ -836,9 +841,10 @@ export default function UpdateCenterSection() {
|
||||
type="button"
|
||||
className="btn btn-ghost"
|
||||
style={{ border: '1px solid var(--color-border)' }}
|
||||
disabled={!canDeployDockerByTag || !candidateTag}
|
||||
disabled={!canDeployCandidate || !candidateTag}
|
||||
title={candidateDeployState.reason}
|
||||
onClick={() => {
|
||||
if (!canDeployDockerByTag || !candidateTag) return;
|
||||
if (!canDeployCandidate || !candidateTag) return;
|
||||
void runDeploy('docker-hub-tag', {
|
||||
tag: candidateTag,
|
||||
digest: candidateDigest || null,
|
||||
|
||||
Reference in New Issue
Block a user