Files
homarr/packages/request-handler/src/update-checker.ts
T

110 lines
3.1 KiB
TypeScript

import { Octokit } from "octokit";
import { compareSemVer, isValidSemVer, parseSemVer } from "semver-parser";
import { env } from "@homarr/common/env";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { createRequestHandler } from "./lib/request-handler";
import packageJson from "../../../package.json";
const logger = createLogger({ module: "updateCheckerRequestHandler" });
export const updateCheckerRequestHandler = createRequestHandler({
async requestAsync(_) {
const availableUpdates = await getAvailableUpdatesAsync(packageJson.version);
return { availableUpdates };
},
cacheTtlMs: 30 * 60 * 1000,
});
interface Update {
name: string | null;
contentHtml?: string;
url: string;
tagName: string;
isPrerelease: boolean;
}
const isPrereleaseTag = (tagName: string) => {
try {
const parsed = parseSemVer(tagName);
return Boolean(parsed.pre?.length);
} catch {
return false;
}
};
export const getAvailableUpdatesAsync = async (currentVersion: string) => {
if (env.NO_EXTERNAL_CONNECTION) return [];
if (!isValidSemVer(currentVersion)) {
throw new ErrorWithMetadata("Unable to check for updates due to non semantic current version", {
currentVersion,
});
}
const octokit = new Octokit({
request: {
fetch: fetchWithTrustedCertificatesAsync,
},
throttle: { enabled: false },
});
const isCurrentPrerelease = isPrereleaseTag(currentVersion);
const releases = await octokit.rest.repos.listReleases({
owner: "homarr-labs",
repo: "homarr",
});
const { skippedTags, semanticReleases } = releases.data
.map((release) => ({
name: release.name,
contentHtml: release.body_html,
url: release.html_url,
tagName: release.tag_name,
isPrerelease: release.prerelease,
}))
.reduce(
(prev, curr) => {
if (!isValidSemVer(curr.tagName)) {
prev.skippedTags.push(curr.tagName);
return prev;
}
prev.semanticReleases.push(curr);
return prev;
},
{ semanticReleases: [] as Update[], skippedTags: [] as string[] },
);
if (skippedTags.length > 0) {
logger.warn(
"Some releases were skipped during the update check because their tag name is not a valid semantic version",
{
skippedTags: skippedTags.join(","),
},
);
}
const availableUpdates = semanticReleases
.filter((release) => isCurrentPrerelease || !release.isPrerelease)
.filter((release) => compareSemVer(release.tagName, currentVersion) > 0)
.toSorted((releaseA, releaseB) => compareSemVer(releaseB.tagName, releaseA.tagName));
if (availableUpdates.length === 0) {
logger.debug("No available updates found", { currentVersion });
return [];
}
logger.info("Found available updates", {
version: availableUpdates[0]?.tagName,
count: availableUpdates.length,
currentVersion,
});
return availableUpdates;
};