feat(releases-widget): add new providers, Github Packages, linuxserver.io and Quay (#3607)

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Andre Silva
2025-08-01 10:13:20 +01:00
committed by GitHub
parent 03adf538b0
commit c92bbd2da0
11 changed files with 427 additions and 27 deletions

View File

@@ -0,0 +1,88 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
import type { ReleasesRepository, ReleasesResponse } from "../interfaces/releases-providers/releases-providers-types";
import { releasesResponseSchema } from "./linuxserverio-schemas";
const localLogger = logger.child({ module: "LinuxServerIOsIntegration" });
export class LinuxServerIOIntegration extends Integration implements ReleasesProviderIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/health"));
if (!response.ok) {
return TestConnectionError.StatusResult(response);
}
return {
success: true,
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
if (!owner || !name) {
localLogger.warn(
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with LinuxServerIO integration`,
{
identifier: repository.identifier,
},
);
return {
id: repository.id,
error: { code: "invalidIdentifier" },
};
}
const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/images"));
if (!releasesResponse.ok) {
return {
id: repository.id,
error: { message: releasesResponse.statusText },
};
}
const releasesResponseJson: unknown = await releasesResponse.json();
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
if (!success) {
return {
id: repository.id,
error: {
message: error.message,
},
};
} else {
const release = data.data.repositories.linuxserver.find((repo) => repo.name === name);
if (!release) {
localLogger.warn(`Repository ${name} not found on provider, with LinuxServerIO integration`, {
owner,
name,
});
return {
id: repository.id,
error: { code: "noReleasesFound" },
};
}
return {
id: repository.id,
latestRelease: release.version,
latestReleaseAt: release.version_timestamp,
releaseDescription: release.changelog?.shift()?.desc,
projectUrl: release.github_url,
projectDescription: release.description,
isArchived: release.deprecated,
createdAt: release.initial_date ? new Date(release.initial_date) : undefined,
starsCount: release.stars,
};
}
}
}

View File

@@ -0,0 +1,31 @@
import { z } from "zod";
export const releasesResponseSchema = z.object({
data: z.object({
repositories: z.object({
linuxserver: z.array(
z.object({
name: z.string(),
initial_date: z
.string()
.transform((value) => new Date(value))
.optional(),
github_url: z.string(),
description: z.string(),
version: z.string(),
version_timestamp: z.string().transform((value) => new Date(value)),
stars: z.number(),
deprecated: z.boolean(),
changelog: z
.array(
z.object({
date: z.string().transform((value) => new Date(value)),
desc: z.string(),
}),
)
.optional(),
}),
),
}),
}),
});