fix: fix upgrade banner to be triggered after package upload

This commit is contained in:
Abhishek Kumar 2026-05-18 15:00:16 +05:30
parent dbc174c867
commit f929a332bb
2 changed files with 85 additions and 14 deletions

View file

@ -0,0 +1,77 @@
import { NextResponse } from "next/server";
const GHCR_IMAGES = ["dograh-hq/dograh-ui", "dograh-hq/dograh-api"] as const;
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
const REVALIDATE_SECONDS = 60 * 60;
type Semver = [number, number, number];
function parseSemver(tag: string): Semver | null {
const m = tag.match(SEMVER_RE);
if (!m) return null;
return [Number(m[1]), Number(m[2]), Number(m[3])];
}
function compareSemver(a: Semver, b: Semver): number {
for (let i = 0; i < 3; i++) {
if (a[i] !== b[i]) return a[i] - b[i];
}
return 0;
}
async function fetchLatestTag(image: string): Promise<string | null> {
const tokenRes = await fetch(
`https://ghcr.io/token?scope=repository:${image}:pull&service=ghcr.io`,
{ next: { revalidate: REVALIDATE_SECONDS } },
);
if (!tokenRes.ok) return null;
const { token } = (await tokenRes.json()) as { token?: string };
if (!token) return null;
const tagsRes = await fetch(`https://ghcr.io/v2/${image}/tags/list`, {
headers: { Authorization: `Bearer ${token}` },
next: { revalidate: REVALIDATE_SECONDS },
});
if (!tagsRes.ok) return null;
const { tags } = (await tagsRes.json()) as { tags?: string[] };
let latest: { tag: string; parsed: Semver } | null = null;
for (const tag of tags ?? []) {
const parsed = parseSemver(tag);
if (!parsed) continue;
if (!latest || compareSemver(parsed, latest.parsed) > 0) {
latest = { tag, parsed };
}
}
return latest?.tag ?? null;
}
export async function GET() {
try {
const results = await Promise.all(GHCR_IMAGES.map(fetchLatestTag));
// Only advertise an update once every image has published a tag at that
// version — otherwise we'd nudge users to upgrade before the matching
// container actually exists.
let minLatest: { tag: string; parsed: Semver } | null = null;
for (const tag of results) {
if (!tag) return NextResponse.json({ latest: null }, { status: 200 });
const parsed = parseSemver(tag);
if (!parsed) return NextResponse.json({ latest: null }, { status: 200 });
if (!minLatest || compareSemver(parsed, minLatest.parsed) < 0) {
minLatest = { tag, parsed };
}
}
return NextResponse.json(
{ latest: minLatest?.tag ?? null },
{
headers: {
"Cache-Control": `public, max-age=${REVALIDATE_SECONDS}, s-maxage=${REVALIDATE_SECONDS}`,
},
},
);
} catch {
return NextResponse.json({ latest: null }, { status: 200 });
}
}

View file

@ -14,7 +14,7 @@ interface Result {
const CACHE_KEY = "dograh-latest-release";
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
const SEMVER_RE = /^(?:[a-z][a-z0-9-]*-)?v?(\d+)\.(\d+)\.(\d+)$/i;
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)$/;
function parseSemver(tag: string): [number, number, number] | null {
const m = tag.match(SEMVER_RE);
@ -56,11 +56,11 @@ export function useLatestReleaseVersion(
}
let cancelled = false;
fetch("https://api.github.com/repos/dograh-hq/dograh/releases/latest")
fetch("/api/config/latest-version")
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (cancelled || !data?.tag_name) return;
const tag = data.tag_name as string;
if (cancelled || !data?.latest) return;
const tag = data.latest as string;
try {
localStorage.setItem(
CACHE_KEY,
@ -72,7 +72,7 @@ export function useLatestReleaseVersion(
setLatest(tag);
})
.catch(() => {
// silent — don't break the sidebar if GitHub is unreachable
// silent — don't break the sidebar if the lookup fails
});
return () => {
@ -80,19 +80,13 @@ export function useLatestReleaseVersion(
};
}, [enabled, currentVersion]);
const normalizedCurrent = currentVersion
? currentVersion.startsWith("v")
? currentVersion
: `v${currentVersion}`
: null;
const currentParsed = normalizedCurrent ? parseSemver(normalizedCurrent) : null;
const currentParsed = currentVersion ? parseSemver(currentVersion) : null;
const latestParsed = latest ? parseSemver(latest) : null;
const isBehind = !!(
normalizedCurrent &&
currentVersion &&
latest &&
isOlder(normalizedCurrent, latest)
isOlder(currentVersion, latest)
);
const isLatest = !!(