mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
fix: fix upgrade banner to be triggered after package upload
This commit is contained in:
parent
dbc174c867
commit
f929a332bb
2 changed files with 85 additions and 14 deletions
77
ui/src/app/api/config/latest-version/route.ts
Normal file
77
ui/src/app/api/config/latest-version/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = !!(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue