110 lines
3.1 KiB
TypeScript
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;
|
|
};
|