[codex] add automatic update-center reminders (#333)

* add automatic update-center reminders

* fix update-center reminder review issues
This commit is contained in:
Cita
2026-03-30 22:37:24 +08:00
committed by GitHub
parent 26f9717ad7
commit 9eb33df6d4
21 changed files with 1009 additions and 176 deletions
@@ -76,6 +76,12 @@ describe('navigationFocus helpers', () => {
type: 'proxy',
})).toBe('/routes');
expect(buildEventNavigationPath({
relatedType: 'update_center',
relatedId: null,
type: 'status',
})).toBe('/settings');
expect(buildEventNavigationPath({ type: 'proxy' })).toBe('/logs');
expect(buildEventNavigationPath({ type: 'checkin' })).toBe('/checkin');
expect(buildEventNavigationPath({ type: 'site_notice' })).toBe('/site-announcements');
+3
View File
@@ -98,6 +98,9 @@ export function buildEventNavigationPath(event: {
if (relatedType === 'site_announcement' && relatedId) {
return buildAnnouncementFocusPath(relatedId);
}
if (relatedType === 'update_center') {
return '/settings';
}
if (relatedType === 'route') {
return '/routes';
}
@@ -40,4 +40,44 @@ describe('updateCenterPresentation', () => {
expect(state.canDeploy).toBe(true);
expect(state.badgeLabel).toBe('发现新 digest');
});
it('treats semver tags with and without a v prefix as the same Docker digest target', () => {
const state = describeDockerDeployState({
enabled: true,
helperHealthy: true,
currentVersion: '1.2.3',
helper: {
imageTag: 'v1.2.3',
imageDigest: 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
},
candidate: {
normalizedVersion: '1.2.3',
tagName: '1.2.3',
digest: 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
},
});
expect(state.kind).toBe('new-digest');
expect(state.canDeploy).toBe(true);
});
it('does not advertise an older GitHub reminder when the helper is already ahead', () => {
expect(buildUpdateReminder({
currentVersion: '1.2.3',
helper: {
imageTag: '1.4.0',
},
githubRelease: {
normalizedVersion: '1.3.0',
displayVersion: '1.3.0',
tagName: 'v1.3.0',
},
dockerHubTag: null,
})).toEqual({
label: '已是最新',
badgeClassName: 'badge badge-muted',
detail: '当前运行版本与已发现的部署目标没有明显差异。',
highlight: false,
});
});
});
@@ -1,14 +1,10 @@
export type UpdateVersionCandidateLike = {
normalizedVersion?: string | null;
displayVersion?: string | null;
tagName?: string | null;
digest?: string | null;
};
export type UpdateHelperRuntimeLike = {
imageTag?: string | null;
imageDigest?: string | null;
};
import {
compareStableVersions,
isSameImageTarget,
resolveUpdateReminderCandidate,
type UpdateHelperRuntimeLike,
type UpdateVersionCandidateLike,
} from '../../../shared/updateCenterReminder.js';
export type UpdateDeployState = {
kind: 'disabled' | 'missing' | 'helper-unhealthy' | 'same-version' | 'same-image' | 'new-version' | 'new-digest' | 'available';
@@ -26,8 +22,6 @@ export type UpdateReminder = {
highlight: boolean;
};
const STABLE_SEMVER_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:\+[\w.-]+)?$/i;
function normalizeString(value?: string | null): string {
return String(value || '').trim();
}
@@ -37,53 +31,6 @@ function normalizeDigest(value?: string | null): string {
return /^sha256:[a-f0-9]{64}$/i.test(digest) ? digest.toLowerCase() : '';
}
export function normalizeStableVersion(value?: string | null): string {
const raw = normalizeString(value);
if (!raw) return '';
const match = raw.match(STABLE_SEMVER_PATTERN);
if (!match) return '';
return [
Number.parseInt(match[1], 10),
Number.parseInt(match[2], 10),
Number.parseInt(match[3], 10),
].join('.');
}
export function compareStableVersions(left?: string | null, right?: string | null): number | null {
const normalizedLeft = normalizeStableVersion(left);
const normalizedRight = normalizeStableVersion(right);
if (!normalizedLeft || !normalizedRight) return null;
const leftParts = normalizedLeft.split('.').map((item) => Number.parseInt(item, 10));
const rightParts = normalizedRight.split('.').map((item) => Number.parseInt(item, 10));
for (let index = 0; index < 3; index += 1) {
if (leftParts[index] === rightParts[index]) continue;
return leftParts[index] < rightParts[index] ? -1 : 1;
}
return 0;
}
export function isSameImageTarget(
current: UpdateHelperRuntimeLike | null | undefined,
target: { tag?: string | null; digest?: string | null },
): boolean {
const currentDigest = normalizeDigest(current?.imageDigest);
const targetDigest = normalizeDigest(target.digest);
if (currentDigest && targetDigest) {
return currentDigest === targetDigest;
}
const currentTag = normalizeString(current?.imageTag);
const targetTag = normalizeString(target.tag);
if (!currentTag || !targetTag || currentTag !== targetTag) {
return false;
}
const currentVersion = normalizeStableVersion(currentTag);
const targetVersion = normalizeStableVersion(targetTag);
return !!currentVersion && currentVersion === targetVersion;
}
export function describeGitHubDeployState(input: {
enabled: boolean;
helperHealthy: boolean;
@@ -127,14 +74,17 @@ export function describeGitHubDeployState(input: {
};
}
const versionCompare = compareStableVersions(input.currentVersion, candidateVersion || candidateTag);
const helperVersionCompare = compareStableVersions(input.helperImageTag, candidateTag);
if (versionCompare === 0 || helperVersionCompare === 0) {
const candidateTargetVersion = candidateVersion || candidateTag;
const versionCompare = compareStableVersions(input.currentVersion, candidateTargetVersion);
const helperVersionCompare = compareStableVersions(input.helperImageTag, candidateTargetVersion);
if (versionCompare === 0 || helperVersionCompare === 0 || helperVersionCompare === 1) {
return {
kind: 'same-version',
badgeClassName: 'badge badge-muted',
badgeLabel: '当前运行',
reason: '当前已运行该版本,无需重复部署。',
reason: helperVersionCompare === 1
? 'Deploy Helper 已指向更高版本,无需回退到较旧的 GitHub 稳定版。'
: '当前已运行该版本,无需重复部署。',
canDeploy: false,
highlight: false,
};
@@ -216,8 +166,21 @@ export function describeDockerDeployState(input: {
};
}
const versionCompare = compareStableVersions(input.currentVersion, candidateVersion || candidateTag);
if (versionCompare === -1) {
const candidateTargetVersion = candidateVersion || candidateTag;
const helperVersionCompare = compareStableVersions(input.helper?.imageTag, candidateTargetVersion);
if (helperVersionCompare === 1) {
return {
kind: 'same-version',
badgeClassName: 'badge badge-muted',
badgeLabel: '当前运行',
reason: 'Deploy Helper 已指向更高版本,无需回退到较旧镜像。',
canDeploy: false,
highlight: false,
};
}
const versionCompare = compareStableVersions(input.currentVersion, candidateTargetVersion);
if (versionCompare === -1 && (helperVersionCompare == null || helperVersionCompare === -1)) {
return {
kind: 'new-version',
badgeClassName: 'badge badge-success',
@@ -228,9 +191,18 @@ export function describeDockerDeployState(input: {
};
}
const helperTag = normalizeString(input.helper?.imageTag);
const helperDigest = normalizeDigest(input.helper?.imageDigest);
if (candidateDigest && helperTag && helperTag === candidateTag && helperDigest && helperDigest !== candidateDigest) {
const hasSameStableTag = isSameImageTarget(
{
imageTag: input.helper?.imageTag,
imageDigest: null,
},
{
tag: candidateTag,
digest: null,
},
);
if (candidateDigest && helperDigest && hasSameStableTag && helperDigest !== candidateDigest) {
return {
kind: 'new-digest',
badgeClassName: 'badge badge-success',
@@ -277,37 +249,22 @@ export function buildUpdateReminder(input: {
};
}
const githubState = describeGitHubDeployState({
enabled: true,
helperHealthy: true,
currentVersion: input.currentVersion,
helperImageTag: input.helper?.imageTag,
candidate: input.githubRelease,
});
if (githubState.kind === 'new-version') {
return {
label: '发现新版本',
badgeClassName: githubState.badgeClassName,
detail: `GitHub 稳定版 ${normalizeString(input.githubRelease?.displayVersion || input.githubRelease?.normalizedVersion)} 已可部署。`,
highlight: true,
};
}
const dockerState = describeDockerDeployState({
enabled: true,
helperHealthy: true,
const candidate = resolveUpdateReminderCandidate({
currentVersion: input.currentVersion,
helper: input.helper,
candidate: input.dockerHubTag,
githubRelease: input.githubRelease,
dockerHubTag: input.dockerHubTag,
});
if (dockerState.kind === 'new-version' || dockerState.kind === 'new-digest') {
if (candidate) {
return {
label: dockerState.badgeLabel,
badgeClassName: dockerState.badgeClassName,
detail: dockerState.kind === 'new-digest'
label: candidate.kind === 'new-digest' ? '发现新 digest' : '发现新版本',
badgeClassName: 'badge badge-success',
detail: candidate.kind === 'new-digest'
? 'Docker Hub 的 alias tag 已指向新 digest,可按需部署。'
: `Docker Hub ${normalizeString(input.dockerHubTag?.displayVersion || input.dockerHubTag?.normalizedVersion)} 已可部署。`,
highlight: dockerState.highlight,
: candidate.source === 'github-release'
? `GitHub 稳定版 ${normalizeString(input.githubRelease?.displayVersion || input.githubRelease?.normalizedVersion)} 已可部署。`
: `Docker Hub ${normalizeString(input.dockerHubTag?.displayVersion || input.dockerHubTag?.normalizedVersion)} 已可部署。`,
highlight: true,
};
}