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:
@@ -207,6 +207,27 @@ export const integrationDefs = {
|
|||||||
category: ["releasesProvider"],
|
category: ["releasesProvider"],
|
||||||
defaultUrl: "https://codeberg.org",
|
defaultUrl: "https://codeberg.org",
|
||||||
},
|
},
|
||||||
|
linuxServerIO: {
|
||||||
|
name: "LinuxServer.io",
|
||||||
|
secretKinds: [[]],
|
||||||
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg",
|
||||||
|
category: ["releasesProvider"],
|
||||||
|
defaultUrl: "https://api.linuxserver.io",
|
||||||
|
},
|
||||||
|
githubPackages: {
|
||||||
|
name: "Github Packages",
|
||||||
|
secretKinds: [[], ["personalAccessToken"]],
|
||||||
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
|
||||||
|
category: ["releasesProvider"],
|
||||||
|
defaultUrl: "https://api.github.com",
|
||||||
|
},
|
||||||
|
quay: {
|
||||||
|
name: "Quay",
|
||||||
|
secretKinds: [[], ["personalAccessToken"]],
|
||||||
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png",
|
||||||
|
category: ["releasesProvider"],
|
||||||
|
defaultUrl: "https://quay.io",
|
||||||
|
},
|
||||||
ntfy: {
|
ntfy: {
|
||||||
name: "ntfy",
|
name: "ntfy",
|
||||||
secretKinds: [["topic"], ["topic", "apiKey"]],
|
secretKinds: [["topic"], ["topic", "apiKey"]],
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorre
|
|||||||
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
|
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
|
||||||
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
|
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
|
||||||
import { EmbyIntegration } from "../emby/emby-integration";
|
import { EmbyIntegration } from "../emby/emby-integration";
|
||||||
|
import { GithubPackagesIntegration } from "../github-packages/github-packages-integration";
|
||||||
import { GithubIntegration } from "../github/github-integration";
|
import { GithubIntegration } from "../github/github-integration";
|
||||||
import { GitlabIntegration } from "../gitlab/gitlab-integration";
|
import { GitlabIntegration } from "../gitlab/gitlab-integration";
|
||||||
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
|
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
|
||||||
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
|
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
|
||||||
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
|
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
|
||||||
|
import { LinuxServerIOIntegration } from "../linuxserverio/linuxserverio-integration";
|
||||||
import { LidarrIntegration } from "../media-organizer/lidarr/lidarr-integration";
|
import { LidarrIntegration } from "../media-organizer/lidarr/lidarr-integration";
|
||||||
import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration";
|
import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration";
|
||||||
import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration";
|
import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration";
|
||||||
@@ -34,6 +36,7 @@ import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-fac
|
|||||||
import { PlexIntegration } from "../plex/plex-integration";
|
import { PlexIntegration } from "../plex/plex-integration";
|
||||||
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
||||||
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
||||||
|
import { QuayIntegration } from "../quay/quay-integration";
|
||||||
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
|
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
|
||||||
import type { Integration, IntegrationInput } from "./integration";
|
import type { Integration, IntegrationInput } from "./integration";
|
||||||
|
|
||||||
@@ -104,6 +107,9 @@ export const integrationCreators = {
|
|||||||
gitlab: GitlabIntegration,
|
gitlab: GitlabIntegration,
|
||||||
npm: NPMIntegration,
|
npm: NPMIntegration,
|
||||||
codeberg: CodebergIntegration,
|
codeberg: CodebergIntegration,
|
||||||
|
linuxServerIO: LinuxServerIOIntegration,
|
||||||
|
githubPackages: GithubPackagesIntegration,
|
||||||
|
quay: QuayIntegration,
|
||||||
ntfy: NTFYIntegration,
|
ntfy: NTFYIntegration,
|
||||||
mock: MockIntegration,
|
mock: MockIntegration,
|
||||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const localLogger = logger.child({ module: "CodebergIntegration" });
|
|||||||
|
|
||||||
export class CodebergIntegration extends Integration implements ReleasesProviderIntegration {
|
export class CodebergIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
||||||
if (!this.hasSecretValue("personalAccessToken")) return await callback({});
|
if (!this.hasSecretValue("personalAccessToken")) return await callback(undefined);
|
||||||
|
|
||||||
return await callback({
|
return await callback({
|
||||||
Authorization: `token ${this.getSecretValue("personalAccessToken")}`,
|
Authorization: `token ${this.getSecretValue("personalAccessToken")}`,
|
||||||
@@ -61,7 +61,7 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
|
|||||||
const details = await this.getDetailsAsync(owner, name);
|
const details = await this.getDetailsAsync(owner, name);
|
||||||
|
|
||||||
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
||||||
return fetchWithTrustedCertificatesAsync(
|
return await fetchWithTrustedCertificatesAsync(
|
||||||
this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`),
|
this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`),
|
||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
||||||
if (!this.hasSecretValue("username") || !this.hasSecretValue("personalAccessToken")) return await callback({});
|
if (!this.hasSecretValue("username") || !this.hasSecretValue("personalAccessToken"))
|
||||||
|
return await callback(undefined);
|
||||||
|
|
||||||
const storedSession = await this.sessionStore.getAsync();
|
const storedSession = await this.sessionStore.getAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { Octokit, RequestError } from "octokit";
|
||||||
|
|
||||||
|
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 { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||||
|
import type {
|
||||||
|
DetailsProviderResponse,
|
||||||
|
ReleaseProviderResponse,
|
||||||
|
ReleasesRepository,
|
||||||
|
ReleasesResponse,
|
||||||
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
|
|
||||||
|
const localLogger = logger.child({ module: "GithubPackagesIntegration" });
|
||||||
|
|
||||||
|
export class GithubPackagesIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
|
private static readonly userAgent = "Homarr-Lab/Homarr:GithubPackagesIntegration";
|
||||||
|
|
||||||
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
|
const headers: RequestInit["headers"] = {
|
||||||
|
"User-Agent": GithubPackagesIntegration.userAgent,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.hasSecretValue("personalAccessToken"))
|
||||||
|
headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`;
|
||||||
|
|
||||||
|
const response = await input.fetchAsync(this.url("/octocat"), {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 Github Packages integration`,
|
||||||
|
{
|
||||||
|
identifier: repository.identifier,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: repository.id,
|
||||||
|
error: { code: "invalidIdentifier" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = this.getApi();
|
||||||
|
const details = await this.getDetailsAsync(api, owner, name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const releasesResponse = await api.rest.packages.getAllPackageVersionsForPackageOwnedByUser({
|
||||||
|
username: owner,
|
||||||
|
package_type: "container",
|
||||||
|
package_name: name,
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const releasesProviderResponse = releasesResponse.data.reduce<ReleaseProviderResponse[]>((acc, release) => {
|
||||||
|
if (!release.metadata?.container?.tags || !(release.metadata.container.tags.length > 0)) return acc;
|
||||||
|
|
||||||
|
release.metadata.container.tags.forEach((tag) => {
|
||||||
|
acc.push({
|
||||||
|
latestRelease: tag,
|
||||||
|
latestReleaseAt: new Date(release.updated_at),
|
||||||
|
releaseUrl: release.html_url,
|
||||||
|
releaseDescription: release.description ?? undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return getLatestRelease(releasesProviderResponse, repository, details);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof RequestError ? error.message : String(error);
|
||||||
|
|
||||||
|
localLogger.warn(`Failed to get releases for ${owner}\\${name} with Github Packages integration`, {
|
||||||
|
owner,
|
||||||
|
name,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: repository.id,
|
||||||
|
error: { message: errorMessage },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getDetailsAsync(
|
||||||
|
api: Octokit,
|
||||||
|
owner: string,
|
||||||
|
name: string,
|
||||||
|
): Promise<DetailsProviderResponse | undefined> {
|
||||||
|
try {
|
||||||
|
const response = await api.rest.packages.getPackageForUser({
|
||||||
|
username: owner,
|
||||||
|
package_type: "container",
|
||||||
|
package_name: name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectUrl: response.data.repository?.html_url ?? response.data.html_url,
|
||||||
|
projectDescription: response.data.repository?.description ?? undefined,
|
||||||
|
isFork: response.data.repository?.fork,
|
||||||
|
isArchived: response.data.repository?.archived,
|
||||||
|
createdAt: new Date(response.data.created_at),
|
||||||
|
starsCount: response.data.repository?.stargazers_count,
|
||||||
|
openIssues: response.data.repository?.open_issues_count,
|
||||||
|
forksCount: response.data.repository?.forks_count,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
localLogger.warn(`Failed to get details for ${owner}\\${name} with Github Packages integration`, {
|
||||||
|
owner,
|
||||||
|
name,
|
||||||
|
error: error instanceof RequestError ? error.message : String(error),
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getApi() {
|
||||||
|
return new Octokit({
|
||||||
|
baseUrl: this.url("/").origin,
|
||||||
|
request: {
|
||||||
|
fetch: fetchWithTrustedCertificatesAsync,
|
||||||
|
},
|
||||||
|
userAgent: GithubPackagesIntegration.userAgent,
|
||||||
|
throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||||
|
...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,6 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
const api = this.getApi();
|
const api = this.getApi();
|
||||||
|
|
||||||
const details = await this.getDetailsAsync(api, owner, name);
|
const details = await this.getDetailsAsync(api, owner, name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
109
packages/integrations/src/quay/quay-integration.ts
Normal file
109
packages/integrations/src/quay/quay-integration.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import type { RequestInit, Response } from "undici";
|
||||||
|
|
||||||
|
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 { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||||
|
import type {
|
||||||
|
ReleaseProviderResponse,
|
||||||
|
ReleasesRepository,
|
||||||
|
ReleasesResponse,
|
||||||
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
|
import { releasesResponseSchema } from "./quay-schemas";
|
||||||
|
|
||||||
|
const localLogger = logger.child({ module: "QuayIntegration" });
|
||||||
|
|
||||||
|
export class QuayIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
|
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
||||||
|
if (!this.hasSecretValue("personalAccessToken")) return await callback(undefined);
|
||||||
|
|
||||||
|
return await callback({
|
||||||
|
Authorization: `token ${this.getSecretValue("personalAccessToken")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
|
const response = await this.withHeadersAsync(async (headers) => {
|
||||||
|
return await input.fetchAsync(this.url("/api/v1/discovery"), {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 this.withHeadersAsync(async (headers) => {
|
||||||
|
return await fetchWithTrustedCertificatesAsync(
|
||||||
|
this.url(
|
||||||
|
`/api/v1/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}?includeTags=true&includeStats=true`,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 details = {
|
||||||
|
projectDescription: data.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
const releasesProviderResponse = Object.entries(data.tags).reduce<ReleaseProviderResponse[]>((acc, [_, tag]) => {
|
||||||
|
if (!tag.name || !tag.last_modified) return acc;
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
latestRelease: tag.name,
|
||||||
|
latestReleaseAt: new Date(tag.last_modified),
|
||||||
|
releaseUrl: `https://quay.io/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/tag/${encodeURIComponent(tag.name)}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return getLatestRelease(releasesProviderResponse, repository, details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/integrations/src/quay/quay-schemas.ts
Normal file
11
packages/integrations/src/quay/quay-schemas.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const releasesResponseSchema = z.object({
|
||||||
|
description: z.string().optional(),
|
||||||
|
tags: z.record(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
last_modified: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
@@ -570,7 +570,7 @@ const ImportRepositorySelect = ({
|
|||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Tooltip label={tRepository("noProvider.tooltip")} disabled={!integration} withArrow>
|
<Tooltip label={tRepository("noProvider.tooltip")} disabled={integration !== undefined} withArrow>
|
||||||
<Group>
|
<Group>
|
||||||
{integration ? (
|
{integration ? (
|
||||||
<MaskedImage
|
<MaskedImage
|
||||||
@@ -618,29 +618,19 @@ const RepositoryImportModal = createModal<RepositoryImportProps>(({ innerProps,
|
|||||||
() =>
|
() =>
|
||||||
docker.data?.containers.reduce<ReleasesRepositoryImport[]>((acc, container) => {
|
docker.data?.containers.reduce<ReleasesRepositoryImport[]>((acc, container) => {
|
||||||
const [maybeSource, maybeIdentifierAndVersion] = container.image.split(/\/(.*)/);
|
const [maybeSource, maybeIdentifierAndVersion] = container.image.split(/\/(.*)/);
|
||||||
const hasSource = maybeSource && maybeSource in sourceToProviderKind;
|
const hasSource = maybeSource && maybeSource in containerImageToProviderKind;
|
||||||
const source = hasSource ? maybeSource : "docker.io";
|
const source = hasSource ? maybeSource : "docker.io";
|
||||||
const identifierAndVersion = hasSource ? maybeIdentifierAndVersion : container.image;
|
const [identifier, version] =
|
||||||
|
hasSource && maybeIdentifierAndVersion ? maybeIdentifierAndVersion.split(":") : container.image.split(":");
|
||||||
|
|
||||||
if (!identifierAndVersion) return acc;
|
if (!identifier) return acc;
|
||||||
|
|
||||||
const providerKey = sourceToProviderKind[source];
|
const providerKind = containerImageToProviderKind[source] ?? "dockerHub";
|
||||||
const integrationId = Object.values(innerProps.integrations).find(
|
const integrationId = Object.values(innerProps.integrations).find(
|
||||||
(integration) => integration.kind === providerKey,
|
(integration) => integration.kind === providerKind,
|
||||||
)?.id;
|
)?.id;
|
||||||
|
|
||||||
const [identifier, version] = identifierAndVersion.split(":");
|
if (acc.some((item) => item.providerIntegrationId === integrationId && item.identifier === identifier))
|
||||||
|
|
||||||
if (!identifier || !integrationId) return acc;
|
|
||||||
|
|
||||||
if (
|
|
||||||
acc.some(
|
|
||||||
(item) =>
|
|
||||||
item.providerIntegrationId !== undefined &&
|
|
||||||
innerProps.integrations[item.providerIntegrationId]?.kind === providerKey &&
|
|
||||||
item.identifier === identifier,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|
||||||
acc.push({
|
acc.push({
|
||||||
@@ -651,10 +641,7 @@ const RepositoryImportModal = createModal<RepositoryImportProps>(({ innerProps,
|
|||||||
name: formatIdentifierName(identifier),
|
name: formatIdentifierName(identifier),
|
||||||
versionFilter: version ? parseImageVersionToVersionFilter(version) : undefined,
|
versionFilter: version ? parseImageVersionToVersionFilter(version) : undefined,
|
||||||
alreadyImported: innerProps.repositories.some(
|
alreadyImported: innerProps.repositories.some(
|
||||||
(item) =>
|
(item) => item.providerIntegrationId === integrationId && item.identifier === identifier,
|
||||||
item.providerIntegrationId !== undefined &&
|
|
||||||
innerProps.integrations[item.providerIntegrationId]?.kind === providerKey &&
|
|
||||||
item.identifier === identifier,
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
return acc;
|
return acc;
|
||||||
@@ -811,9 +798,11 @@ const RepositoryImportModal = createModal<RepositoryImportProps>(({ innerProps,
|
|||||||
size: "xl",
|
size: "xl",
|
||||||
});
|
});
|
||||||
|
|
||||||
const sourceToProviderKind: Record<string, IntegrationKind> = {
|
const containerImageToProviderKind: Record<string, IntegrationKind> = {
|
||||||
"ghcr.io": "github",
|
"ghcr.io": "github",
|
||||||
"docker.io": "dockerHub",
|
"docker.io": "dockerHub",
|
||||||
|
"lscr.io": "linuxServerIO",
|
||||||
|
"quay.io": "quay",
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseImageVersionToVersionFilter = (imageVersion: string): ReleasesVersionFilter | undefined => {
|
const parseImageVersionToVersionFilter = (imageVersion: string): ReleasesVersionFilter | undefined => {
|
||||||
|
|||||||
Reference in New Issue
Block a user