Let manual update checks bypass rollout

Automatic desktop update checks still respect rollout admission. Manual checks carry an explicit intent through the app and desktop updater service, and the up-to-date state now shows when the last check completed.
This commit is contained in:
Mohamed Boudra
2026-06-09 15:47:30 +07:00
parent e72b0773e6
commit eca0a5bf67
14 changed files with 809 additions and 284 deletions
@@ -0,0 +1,42 @@
import { UUID } from "builder-util-runtime";
import { z } from "zod";
export type AppReleaseChannel = "stable" | "beta";
export type AppUpdateCheckIntent = "automatic" | "manual";
export const rolloutManifestSchema = z.object({
rolloutHours: z
.union([z.number(), z.string().transform(Number)])
.pipe(z.number().finite().nonnegative())
.optional()
.catch(undefined),
releaseDate: z.string().optional().catch(undefined),
});
export function shouldAdmitAppUpdate(args: {
channel: AppReleaseChannel;
intent: AppUpdateCheckIntent;
rolloutHours: number | undefined;
releaseDate: string | undefined;
now: number;
bucket: number;
}): boolean {
if (args.intent === "manual") return true;
if (args.channel !== "stable") return true;
if (args.rolloutHours == null) return true;
if (args.rolloutHours === 0) return true;
if (!args.releaseDate) return true;
const releaseTime = new Date(args.releaseDate).getTime();
if (Number.isNaN(releaseTime)) return true;
const ageHours = (args.now - releaseTime) / 3_600_000;
if (ageHours < 0) return false;
const pct = Math.min(100, (ageHours / args.rolloutHours) * 100);
return args.bucket * 100 < pct;
}
export function bucketFromStagingUserId(stagingUserId: string): number {
return UUID.parse(stagingUserId).readUInt32BE(12) / 0x100000000;
}