feat: add staged release updates
TallyNote release / linux-x64 (push) Failing after 2m51s

This commit is contained in:
Qiufeng
2026-09-01 14:46:32 +08:00
parent 4395317651
commit a268eb5fe9
18 changed files with 452 additions and 92 deletions
+30
View File
@@ -14,6 +14,7 @@ import {
sanitizeAssetName,
selectReleaseAsset,
validateHttpsUrl,
RELEASE_NOTES_MAX_BYTES,
type ReleaseAsset,
type ReleaseMetadata,
} from "./update.js";
@@ -34,7 +35,10 @@ export type CachedRelease = {
metadataUrl: string;
version: string;
tagName?: string;
releaseName?: string;
publishedAt?: string;
notes?: string;
releaseUrl?: string;
platform: string;
signatureVerified?: boolean;
asset?: {
@@ -53,7 +57,10 @@ export type UpdateCheckResult = {
latest: {
version: string;
tagName?: string;
releaseName?: string;
publishedAt?: string;
notes?: string;
releaseUrl?: string;
compatible: boolean;
integrityReady: boolean;
signatureReady: boolean;
@@ -65,6 +72,7 @@ export type UpdateCheckResult = {
export type UpdateRequest = {
jobId: string;
operation?: "download" | "apply";
version: string;
metadataUrl: string;
assetUrl: string;
@@ -76,6 +84,7 @@ export type UpdateRequest = {
currentLink: string;
releasesDir: string;
dataDir: string;
stagedPath?: string;
};
function setting(database: Database.Database, key: string): string | undefined {
@@ -206,7 +215,10 @@ export async function checkForUpdate(database: Database.Database, config: AppCon
metadataUrl,
version: safeVersion,
...(metadata.tagName ? { tagName: metadata.tagName } : {}),
...(metadata.releaseName ? { releaseName: metadata.releaseName } : {}),
...(metadata.publishedAt ? { publishedAt: metadata.publishedAt } : {}),
...(metadata.notes ? { notes: metadata.notes } : {}),
...(metadata.releaseUrl ? { releaseUrl: metadata.releaseUrl } : {}),
platform: platform.target,
signatureVerified,
...(asset ? {
@@ -227,7 +239,10 @@ export async function checkForUpdate(database: Database.Database, config: AppCon
latest: {
version: safeVersion,
...(metadata.tagName ? { tagName: metadata.tagName } : {}),
...(metadata.releaseName ? { releaseName: metadata.releaseName } : {}),
...(metadata.publishedAt ? { publishedAt: metadata.publishedAt } : {}),
...(metadata.notes ? { notes: metadata.notes } : {}),
...(metadata.releaseUrl ? { releaseUrl: metadata.releaseUrl } : {}),
compatible: Boolean(asset),
integrityReady: Boolean(asset?.sha256 && (!config.updateRequireSignature || signatureVerified)),
signatureReady: !config.updateRequireSignature || signatureVerified,
@@ -245,6 +260,9 @@ export function readCachedRelease(database: Database.Database, config: AppConfig
if (!value || typeof value !== "object" || typeof value.version !== "string" || typeof value.metadataUrl !== "string" || typeof value.platform !== "string") return null;
parseSemver(value.version);
const metadataUrl = validateHttpsUrl(value.metadataUrl, policy(config)).toString();
if (value.releaseName !== undefined && (typeof value.releaseName !== "string" || value.releaseName.length > 200 || /[\u0000-\u001f\u007f]/.test(value.releaseName))) return null;
if (value.notes !== undefined && (typeof value.notes !== "string" || Buffer.byteLength(value.notes, "utf8") > RELEASE_NOTES_MAX_BYTES)) return null;
if (value.releaseUrl !== undefined) validateHttpsUrl(value.releaseUrl, policy(config));
if (value.signatureVerified !== undefined && typeof value.signatureVerified !== "boolean") return null;
if (value.asset) {
if (typeof value.asset.name !== "string" || typeof value.asset.url !== "string") return null;
@@ -266,7 +284,10 @@ export function publicCheckFromCache(database: Database.Database, config: AppCon
return { configured: config.updateStrategy !== "disabled", currentVersion: config.appVersion, platform, checkedAt: cached?.checkedAt ?? 0, latest: cached ? {
version: cached.version,
...(cached.tagName ? { tagName: cached.tagName } : {}),
...(cached.releaseName ? { releaseName: cached.releaseName } : {}),
...(cached.publishedAt ? { publishedAt: cached.publishedAt } : {}),
...(cached.notes ? { notes: cached.notes } : {}),
...(cached.releaseUrl ? { releaseUrl: cached.releaseUrl } : {}),
compatible,
integrityReady: compatible && Boolean(cached.asset?.sha256) && (!config.updateRequireSignature || cached.signatureVerified === true),
signatureReady: !config.updateRequireSignature || cached.signatureVerified === true,
@@ -282,7 +303,10 @@ export function publicCheckFromCache(database: Database.Database, config: AppCon
latest: {
version: cached.version,
...(cached.tagName ? { tagName: cached.tagName } : {}),
...(cached.releaseName ? { releaseName: cached.releaseName } : {}),
...(cached.publishedAt ? { publishedAt: cached.publishedAt } : {}),
...(cached.notes ? { notes: cached.notes } : {}),
...(cached.releaseUrl ? { releaseUrl: cached.releaseUrl } : {}),
compatible: Boolean(cached.asset),
integrityReady: Boolean(cached.asset?.sha256) && (!config.updateRequireSignature || cached.signatureVerified === true),
signatureReady: !config.updateRequireSignature || cached.signatureVerified === true,
@@ -309,8 +333,11 @@ export async function writeUpdateRequest(config: AppConfig, request: UpdateReque
export function publicUpdateJob(row: Record<string, unknown> | undefined): Record<string, unknown> | null {
if (!row) return null;
const hasError = typeof row.errorMessage === "string" && row.errorMessage.length > 0;
const updatedAt = typeof row.updatedAt === "number" ? row.updatedAt : null;
const expectedRecoveryAt = row.status === "applying" && updatedAt !== null ? updatedAt + 30_000 : null;
return {
id: row.id,
operation: row.operation ?? "apply",
status: row.status,
version: row.version,
platform: row.platform,
@@ -323,5 +350,8 @@ export function publicUpdateJob(row: Record<string, unknown> | undefined): Recor
createdAt: row.createdAt,
updatedAt: row.updatedAt,
completedAt: row.completedAt ?? null,
...(row.applyQueuedAt ? { applyQueuedAt: row.applyQueuedAt } : {}),
...(expectedRecoveryAt ? { expectedRecoveryAt } : {}),
...(row.status === "applying" ? { restartWindowSeconds: 30 } : {}),
};
}