fix(releases-widget): Search multiple docker hub pages for releases (#4158)
This commit is contained in:
@@ -40,15 +40,22 @@ export const releasesRouter = createTRPCRouter({
|
|||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
input.repositories.map(async (repository) => {
|
input.repositories.map(async (repository) => {
|
||||||
const innerHandler = releasesRequestHandler.handler(ctx.integration, {
|
const response = await releasesRequestHandler
|
||||||
id: repository.id,
|
.handler(ctx.integration, {
|
||||||
identifier: repository.identifier,
|
id: repository.id,
|
||||||
versionRegex: formatVersionFilterRegex(repository.versionFilter),
|
identifier: repository.identifier,
|
||||||
});
|
versionRegex: formatVersionFilterRegex(repository.versionFilter),
|
||||||
|
})
|
||||||
|
.getCachedOrUpdatedDataAsync({
|
||||||
|
forceUpdate: false,
|
||||||
|
});
|
||||||
|
|
||||||
return await innerHandler.getCachedOrUpdatedDataAsync({
|
return {
|
||||||
forceUpdate: false,
|
id: repository.id,
|
||||||
});
|
integration: { name: ctx.integration.name, kind: ctx.integration.kind },
|
||||||
|
timestamp: response.timestamp,
|
||||||
|
...response.data,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import type { ReleasesProviderIntegration } from "../interfaces/releases-provide
|
|||||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||||
import type {
|
import type {
|
||||||
DetailsProviderResponse,
|
DetailsProviderResponse,
|
||||||
ReleasesRepository,
|
ReleaseResponse,
|
||||||
ReleasesResponse,
|
|
||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas";
|
import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas";
|
||||||
|
|
||||||
@@ -43,22 +42,23 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
private parseIdentifier(identifier: string) {
|
||||||
const [owner, name] = repository.identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(
|
localLogger.warn(
|
||||||
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with Codeberg integration`,
|
`Invalid identifier format. Expected 'owner/name', for ${identifier} with Codeberg integration`,
|
||||||
{
|
{ identifier },
|
||||||
identifier: repository.identifier,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return {
|
return null;
|
||||||
id: repository.id,
|
|
||||||
error: { code: "invalidIdentifier" },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
|
|
||||||
const details = await this.getDetailsAsync(owner, name);
|
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
|
||||||
|
const parsedIdentifier = this.parseIdentifier(identifier);
|
||||||
|
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
|
||||||
|
|
||||||
|
const { owner, name } = parsedIdentifier;
|
||||||
|
|
||||||
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
||||||
return await fetchWithTrustedCertificatesAsync(
|
return await fetchWithTrustedCertificatesAsync(
|
||||||
@@ -66,34 +66,36 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
|
|||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!releasesResponse.ok) {
|
if (!releasesResponse.ok) {
|
||||||
return {
|
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
|
||||||
id: repository.id,
|
|
||||||
error: { message: releasesResponse.statusText },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||||
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return {
|
return {
|
||||||
id: repository.id,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
|
code: "unexpected",
|
||||||
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message,
|
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
const formattedReleases = data.map((tag) => ({
|
|
||||||
latestRelease: tag.tag_name,
|
|
||||||
latestReleaseAt: tag.published_at,
|
|
||||||
releaseUrl: tag.url,
|
|
||||||
releaseDescription: tag.body,
|
|
||||||
isPreRelease: tag.prerelease,
|
|
||||||
}));
|
|
||||||
return getLatestRelease(formattedReleases, repository, details);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formattedReleases = data.map((tag) => ({
|
||||||
|
latestRelease: tag.tag_name,
|
||||||
|
latestReleaseAt: tag.published_at,
|
||||||
|
releaseUrl: tag.url,
|
||||||
|
releaseDescription: tag.body,
|
||||||
|
isPreRelease: tag.prerelease,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const latestRelease = getLatestRelease(formattedReleases, versionRegex);
|
||||||
|
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
|
|
||||||
|
const details = await this.getDetailsAsync(owner, name);
|
||||||
|
|
||||||
|
return { success: true, data: { ...details, ...latestRelease } };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getDetailsAsync(owner: string, name: string): Promise<DetailsProviderResponse | undefined> {
|
protected async getDetailsAsync(owner: string, name: string): Promise<DetailsProviderResponse | undefined> {
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import type { ReleasesProviderIntegration } from "../interfaces/releases-provide
|
|||||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||||
import type {
|
import type {
|
||||||
DetailsProviderResponse,
|
DetailsProviderResponse,
|
||||||
ReleasesRepository,
|
ReleaseResponse,
|
||||||
ReleasesResponse,
|
|
||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
import { accessTokenResponseSchema, detailsResponseSchema, releasesResponseSchema } from "./docker-hub-schemas";
|
import { accessTokenResponseSchema, detailsResponseSchema, releasesResponseSchema } from "./docker-hub-schemas";
|
||||||
|
|
||||||
@@ -73,49 +72,61 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
private parseIdentifier(identifier: string) {
|
||||||
const relativeUrl = this.getRelativeUrl(repository.identifier);
|
if (!identifier.includes("/")) return { owner: "", name: identifier };
|
||||||
if (relativeUrl === "/") {
|
const [owner, name] = identifier.split("/");
|
||||||
localLogger.warn(
|
if (!owner || !name) {
|
||||||
`Invalid identifier format. Expected 'owner/name' or 'name', for ${repository.identifier} on DockerHub`,
|
localLogger.warn(`Invalid identifier format. Expected 'owner/name' or 'name', for ${identifier} on DockerHub`, {
|
||||||
{
|
identifier,
|
||||||
identifier: repository.identifier,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
id: repository.id,
|
|
||||||
error: { code: "invalidIdentifier" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const details = await this.getDetailsAsync(relativeUrl);
|
|
||||||
|
|
||||||
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
|
||||||
return await fetchWithTrustedCertificatesAsync(this.url(`${relativeUrl}/tags?page_size=100`), {
|
|
||||||
headers,
|
|
||||||
});
|
});
|
||||||
});
|
return null;
|
||||||
|
}
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
|
|
||||||
if (!releasesResponse.ok) {
|
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
|
||||||
return {
|
const parsedIdentifier = this.parseIdentifier(identifier);
|
||||||
id: repository.id,
|
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
|
||||||
error: { message: releasesResponse.statusText },
|
|
||||||
};
|
const { owner, name } = parsedIdentifier;
|
||||||
|
|
||||||
|
const relativeUrl: `/${string}` = owner
|
||||||
|
? `/v2/namespaces/${encodeURIComponent(owner)}/repositories/${encodeURIComponent(name)}`
|
||||||
|
: `/v2/repositories/library/${encodeURIComponent(name)}`;
|
||||||
|
|
||||||
|
for (let page = 0; page <= 5; page++) {
|
||||||
|
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
||||||
|
return await fetchWithTrustedCertificatesAsync(this.url(`${relativeUrl}/tags?page_size=100&page=${page}`), {
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!releasesResponse.ok) {
|
||||||
|
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||||
|
const releasesResult = releasesResponseSchema.safeParse(releasesResponseJson);
|
||||||
|
if (!releasesResult.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: "unexpected",
|
||||||
|
message: releasesResponseJson
|
||||||
|
? JSON.stringify(releasesResponseJson, null, 2)
|
||||||
|
: releasesResult.error.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestRelease = getLatestRelease(releasesResult.data.results, versionRegex);
|
||||||
|
if (!latestRelease) continue;
|
||||||
|
|
||||||
|
const details = await this.getDetailsAsync(relativeUrl);
|
||||||
|
|
||||||
|
return { success: true, data: { ...details, ...latestRelease } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
const releasesResult = releasesResponseSchema.safeParse(releasesResponseJson);
|
|
||||||
|
|
||||||
if (!releasesResult.success) {
|
|
||||||
return {
|
|
||||||
id: repository.id,
|
|
||||||
error: {
|
|
||||||
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : releasesResult.error.message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return getLatestRelease(releasesResult.data.results, repository, details);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDetailsAsync(relativeUrl: `/${string}`): Promise<DetailsProviderResponse | undefined> {
|
private async getDetailsAsync(relativeUrl: `/${string}`): Promise<DetailsProviderResponse | undefined> {
|
||||||
@@ -154,18 +165,6 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRelativeUrl(identifier: string): `/${string}` {
|
|
||||||
if (identifier.indexOf("/") > 0) {
|
|
||||||
const [owner, name] = identifier.split("/");
|
|
||||||
if (!owner || !name) {
|
|
||||||
return "/";
|
|
||||||
}
|
|
||||||
return `/v2/namespaces/${encodeURIComponent(owner)}/repositories/${encodeURIComponent(name)}`;
|
|
||||||
} else {
|
|
||||||
return `/v2/repositories/library/${encodeURIComponent(identifier)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getSessionAsync(fetchAsync: typeof fetch = fetchWithTrustedCertificatesAsync): Promise<string> {
|
private async getSessionAsync(fetchAsync: typeof fetch = fetchWithTrustedCertificatesAsync): Promise<string> {
|
||||||
const response = await fetchAsync(this.url("/v2/auth/token"), {
|
const response = await fetchAsync(this.url("/v2/auth/token"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import { getLatestRelease } from "../interfaces/releases-providers/releases-prov
|
|||||||
import type {
|
import type {
|
||||||
DetailsProviderResponse,
|
DetailsProviderResponse,
|
||||||
ReleaseProviderResponse,
|
ReleaseProviderResponse,
|
||||||
ReleasesRepository,
|
ReleaseResponse,
|
||||||
ReleasesResponse,
|
|
||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" });
|
const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" });
|
||||||
@@ -43,23 +42,24 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
private parseIdentifier(identifier: string) {
|
||||||
const [owner, name] = repository.identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(
|
localLogger.warn(
|
||||||
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with GitHub Container Registry integration`,
|
`Invalid identifier format. Expected 'owner/name', for ${identifier} with GitHub Container Registry integration`,
|
||||||
{
|
{ identifier },
|
||||||
identifier: repository.identifier,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return {
|
return null;
|
||||||
id: repository.id,
|
|
||||||
error: { code: "invalidIdentifier" },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
|
||||||
|
const parsedIdentifier = this.parseIdentifier(identifier);
|
||||||
|
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
|
||||||
|
|
||||||
|
const { owner, name } = parsedIdentifier;
|
||||||
const api = this.getApi();
|
const api = this.getApi();
|
||||||
const details = await this.getDetailsAsync(api, owner, name);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const releasesResponse = await api.rest.packages.getAllPackageVersionsForPackageOwnedByUser({
|
const releasesResponse = await api.rest.packages.getAllPackageVersionsForPackageOwnedByUser({
|
||||||
@@ -83,20 +83,20 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return getLatestRelease(releasesProviderResponse, repository, details);
|
const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex);
|
||||||
|
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
|
|
||||||
|
const details = await this.getDetailsAsync(api, owner, name);
|
||||||
|
|
||||||
|
return { success: true, data: { ...details, ...latestRelease } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof RequestError ? error.message : String(error);
|
const errorMessage = error instanceof RequestError ? error.message : String(error);
|
||||||
|
|
||||||
localLogger.warn(`Failed to get releases for ${owner}\\${name} with GitHub Container Registry integration`, {
|
localLogger.warn(`Failed to get releases for ${owner}\\${name} with GitHub Container Registry integration`, {
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
});
|
});
|
||||||
|
return { success: false, error: { code: "unexpected", message: errorMessage } };
|
||||||
return {
|
|
||||||
id: repository.id,
|
|
||||||
error: { message: errorMessage },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import { getLatestRelease } from "../interfaces/releases-providers/releases-prov
|
|||||||
import type {
|
import type {
|
||||||
DetailsProviderResponse,
|
DetailsProviderResponse,
|
||||||
ReleaseProviderResponse,
|
ReleaseProviderResponse,
|
||||||
ReleasesRepository,
|
ReleaseResponse,
|
||||||
ReleasesResponse,
|
|
||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "GithubIntegration" });
|
const localLogger = logger.child({ module: "GithubIntegration" });
|
||||||
@@ -43,38 +42,32 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
private parseIdentifier(identifier: string) {
|
||||||
const [owner, name] = repository.identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(
|
localLogger.warn(`Invalid identifier format. Expected 'owner/name', for ${identifier} with Github integration`, {
|
||||||
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with Github integration`,
|
identifier,
|
||||||
{
|
});
|
||||||
identifier: repository.identifier,
|
return null;
|
||||||
},
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
id: repository.id,
|
|
||||||
error: { code: "invalidIdentifier" },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
|
||||||
|
const parsedIdentifier = this.parseIdentifier(identifier);
|
||||||
|
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
|
||||||
|
|
||||||
|
const { owner, name } = parsedIdentifier;
|
||||||
const api = this.getApi();
|
const api = this.getApi();
|
||||||
const details = await this.getDetailsAsync(api, owner, name);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const releasesResponse = await api.rest.repos.listReleases({
|
const releasesResponse = await api.rest.repos.listReleases({ owner, repo: name });
|
||||||
owner,
|
|
||||||
repo: name,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (releasesResponse.data.length === 0) {
|
if (releasesResponse.data.length === 0) {
|
||||||
localLogger.warn(`No releases found, for ${repository.identifier} with Github integration`, {
|
localLogger.warn(`No releases found, for ${owner}/${name} with Github integration`, {
|
||||||
identifier: repository.identifier,
|
identifier: `${owner}/${name}`,
|
||||||
});
|
});
|
||||||
return {
|
return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
id: repository.id,
|
|
||||||
error: { code: "noReleasesFound" },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const releasesProviderResponse = releasesResponse.data.reduce<ReleaseProviderResponse[]>((acc, release) => {
|
const releasesProviderResponse = releasesResponse.data.reduce<ReleaseProviderResponse[]>((acc, release) => {
|
||||||
@@ -90,20 +83,20 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return getLatestRelease(releasesProviderResponse, repository, details);
|
const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex);
|
||||||
|
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
|
|
||||||
|
const details = await this.getDetailsAsync(api, owner, name);
|
||||||
|
|
||||||
|
return { success: true, data: { ...details, ...latestRelease } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof OctokitRequestError ? error.message : String(error);
|
const errorMessage = error instanceof OctokitRequestError ? error.message : String(error);
|
||||||
|
|
||||||
localLogger.warn(`Failed to get releases for ${owner}\\${name} with Github integration`, {
|
localLogger.warn(`Failed to get releases for ${owner}\\${name} with Github integration`, {
|
||||||
owner,
|
owner,
|
||||||
name,
|
name,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
});
|
});
|
||||||
|
return { success: false, error: { code: "unexpected", message: errorMessage } };
|
||||||
return {
|
|
||||||
id: repository.id,
|
|
||||||
error: { message: errorMessage },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import { getLatestRelease } from "../interfaces/releases-providers/releases-prov
|
|||||||
import type {
|
import type {
|
||||||
DetailsProviderResponse,
|
DetailsProviderResponse,
|
||||||
ReleaseProviderResponse,
|
ReleaseProviderResponse,
|
||||||
ReleasesRepository,
|
ReleaseResponse,
|
||||||
ReleasesResponse,
|
|
||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "GitlabIntegration" });
|
const localLogger = logger.child({ module: "GitlabIntegration" });
|
||||||
@@ -40,25 +39,20 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
|
||||||
const api = this.getApi();
|
const api = this.getApi();
|
||||||
|
|
||||||
const details = await this.getDetailsAsync(api, repository.identifier);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const releasesResponse = await api.ProjectReleases.all(repository.identifier, {
|
const releasesResponse = await api.ProjectReleases.all(identifier, {
|
||||||
perPage: 100,
|
perPage: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (releasesResponse instanceof Error) {
|
if (releasesResponse instanceof Error) {
|
||||||
localLogger.warn(`Failed to get releases for ${repository.identifier} with Gitlab integration`, {
|
localLogger.warn(`Failed to get releases for ${identifier} with Gitlab integration`, {
|
||||||
identifier: repository.identifier,
|
identifier,
|
||||||
error: releasesResponse.message,
|
error: releasesResponse.message,
|
||||||
});
|
});
|
||||||
return {
|
return { success: false, error: { code: "noReleasesFound" } };
|
||||||
id: repository.id,
|
|
||||||
error: { code: "noReleasesFound" },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const releasesProviderResponse = releasesResponse.reduce<ReleaseProviderResponse[]>((acc, release) => {
|
const releasesProviderResponse = releasesResponse.reduce<ReleaseProviderResponse[]>((acc, release) => {
|
||||||
@@ -76,17 +70,19 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return getLatestRelease(releasesProviderResponse, repository, details);
|
const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex);
|
||||||
} catch (error) {
|
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
localLogger.warn(`Failed to get releases for ${repository.identifier} with Gitlab integration`, {
|
|
||||||
identifier: repository.identifier,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
const details = await this.getDetailsAsync(api, identifier);
|
||||||
id: repository.id,
|
|
||||||
error: { code: "noReleasesFound" },
|
return { success: true, data: { ...details, ...latestRelease } };
|
||||||
};
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
localLogger.warn(`Failed to get releases for ${identifier} with Gitlab integration`, {
|
||||||
|
identifier,
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
return { success: false, error: { code: "unexpected", message: errorMessage } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export type {
|
|||||||
TdarrStatistics,
|
TdarrStatistics,
|
||||||
TdarrWorker,
|
TdarrWorker,
|
||||||
} from "./interfaces/media-transcoding/media-transcoding-types";
|
} from "./interfaces/media-transcoding/media-transcoding-types";
|
||||||
export type { ReleasesResponse } from "./interfaces/releases-providers/releases-providers-types";
|
export type { ReleasesRepository, ReleaseResponse } from "./interfaces/releases-providers/releases-providers-types";
|
||||||
export type { Notification } from "./interfaces/notifications/notification-types";
|
export type { Notification } from "./interfaces/notifications/notification-types";
|
||||||
|
|
||||||
// Schemas
|
// Schemas
|
||||||
|
|||||||
@@ -1,47 +1,21 @@
|
|||||||
import type {
|
import type { ReleaseProviderResponse, ReleaseResponse } from "./releases-providers-types";
|
||||||
DetailsProviderResponse,
|
|
||||||
ReleaseProviderResponse,
|
|
||||||
ReleasesRepository,
|
|
||||||
ReleasesResponse,
|
|
||||||
} from "./releases-providers-types";
|
|
||||||
|
|
||||||
export const getLatestRelease = (
|
export const getLatestRelease = (
|
||||||
releases: ReleaseProviderResponse[],
|
releases: ReleaseProviderResponse[],
|
||||||
repository: ReleasesRepository,
|
versionRegex?: string,
|
||||||
details?: DetailsProviderResponse,
|
): ReleaseProviderResponse | null => {
|
||||||
): ReleasesResponse => {
|
|
||||||
const validReleases = releases.filter((result) => {
|
const validReleases = releases.filter((result) => {
|
||||||
if (result.latestRelease) {
|
if (result.latestRelease) {
|
||||||
return repository.versionRegex ? new RegExp(repository.versionRegex).test(result.latestRelease) : true;
|
return versionRegex ? new RegExp(versionRegex).test(result.latestRelease) : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const latest =
|
return validReleases.length === 0
|
||||||
validReleases.length === 0
|
? null
|
||||||
? ({
|
: validReleases.reduce((latest, current) => (current.latestReleaseAt > latest.latestReleaseAt ? current : latest));
|
||||||
id: repository.id,
|
|
||||||
error: { code: "noMatchingVersion" },
|
|
||||||
} as ReleasesResponse)
|
|
||||||
: validReleases.reduce(
|
|
||||||
(latest, result) => {
|
|
||||||
return {
|
|
||||||
...details,
|
|
||||||
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
|
|
||||||
id: repository.id,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
latestRelease: "",
|
|
||||||
latestReleaseAt: new Date(0),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return latest;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ReleasesProviderIntegration {
|
export interface ReleasesProviderIntegration {
|
||||||
getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse>;
|
getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { TranslationObject } from "@homarr/translation";
|
import type { TranslationObject } from "@homarr/translation";
|
||||||
|
|
||||||
|
export interface ReleasesRepository extends Record<string, unknown> {
|
||||||
|
id: string;
|
||||||
|
identifier: string;
|
||||||
|
versionRegex?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DetailsProviderResponse {
|
export interface DetailsProviderResponse {
|
||||||
projectUrl?: string;
|
projectUrl?: string;
|
||||||
projectDescription?: string;
|
projectDescription?: string;
|
||||||
@@ -19,35 +25,10 @@ export interface ReleaseProviderResponse {
|
|||||||
isPreRelease?: boolean;
|
isPreRelease?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReleasesRepository {
|
|
||||||
id: string;
|
|
||||||
identifier: string;
|
|
||||||
versionRegex?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReleasesErrorKeys = keyof TranslationObject["widget"]["releases"]["error"]["messages"];
|
type ReleasesErrorKeys = keyof TranslationObject["widget"]["releases"]["error"]["messages"];
|
||||||
|
|
||||||
export interface ReleasesResponse {
|
export type ReleaseData = DetailsProviderResponse & ReleaseProviderResponse;
|
||||||
id: string;
|
|
||||||
latestRelease?: string;
|
|
||||||
latestReleaseAt?: Date;
|
|
||||||
releaseUrl?: string;
|
|
||||||
releaseDescription?: string;
|
|
||||||
isPreRelease?: boolean;
|
|
||||||
projectUrl?: string;
|
|
||||||
projectDescription?: string;
|
|
||||||
isFork?: boolean;
|
|
||||||
isArchived?: boolean;
|
|
||||||
createdAt?: Date;
|
|
||||||
starsCount?: number;
|
|
||||||
openIssues?: number;
|
|
||||||
forksCount?: number;
|
|
||||||
|
|
||||||
error?:
|
export type ReleaseError = { code: ReleasesErrorKeys } | { code: "unexpected"; message: string };
|
||||||
| {
|
|
||||||
code: ReleasesErrorKeys;
|
export type ReleaseResponse = { success: true; data: ReleaseData } | { success: false; error: ReleaseError };
|
||||||
}
|
|
||||||
| {
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Integration } from "../base/integration";
|
|||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||||
import type { ReleasesRepository, ReleasesResponse } from "../interfaces/releases-providers/releases-providers-types";
|
import type { ReleaseResponse } from "../interfaces/releases-providers/releases-providers-types";
|
||||||
import { releasesResponseSchema } from "./linuxserverio-schemas";
|
import { releasesResponseSchema } from "./linuxserverio-schemas";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "LinuxServerIOsIntegration" });
|
const localLogger = logger.child({ module: "LinuxServerIOsIntegration" });
|
||||||
@@ -24,56 +24,44 @@ export class LinuxServerIOIntegration extends Integration implements ReleasesPro
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
private parseIdentifier(identifier: string) {
|
||||||
const [owner, name] = repository.identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(
|
localLogger.warn(
|
||||||
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with LinuxServerIO integration`,
|
`Invalid identifier format. Expected 'owner/name', for ${identifier} with LinuxServerIO integration`,
|
||||||
{
|
{ identifier },
|
||||||
identifier: repository.identifier,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return {
|
return null;
|
||||||
id: repository.id,
|
|
||||||
error: { code: "invalidIdentifier" },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLatestMatchingReleaseAsync(identifier: string): Promise<ReleaseResponse> {
|
||||||
|
const { name } = this.parseIdentifier(identifier) ?? {};
|
||||||
|
if (!name) return { success: false, error: { code: "invalidIdentifier" } };
|
||||||
|
|
||||||
const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/images"));
|
const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/images"));
|
||||||
|
|
||||||
if (!releasesResponse.ok) {
|
if (!releasesResponse.ok) {
|
||||||
return {
|
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
|
||||||
id: repository.id,
|
|
||||||
error: { message: releasesResponse.statusText },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||||
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return {
|
return { success: false, error: { code: "unexpected", message: error.message } };
|
||||||
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 {
|
const release = data.data.repositories.linuxserver.find((repo) => repo.name === name);
|
||||||
id: repository.id,
|
if (!release) {
|
||||||
error: { code: "noReleasesFound" },
|
localLogger.warn(`Repository ${name} not found on provider, with LinuxServerIO integration`, {
|
||||||
};
|
name,
|
||||||
}
|
});
|
||||||
|
return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: repository.id,
|
success: true,
|
||||||
|
data: {
|
||||||
latestRelease: release.version,
|
latestRelease: release.version,
|
||||||
latestReleaseAt: release.version_timestamp,
|
latestReleaseAt: release.version_timestamp,
|
||||||
releaseDescription: release.changelog?.shift()?.desc,
|
releaseDescription: release.changelog?.shift()?.desc,
|
||||||
@@ -82,7 +70,7 @@ export class LinuxServerIOIntegration extends Integration implements ReleasesPro
|
|||||||
isArchived: release.deprecated,
|
isArchived: release.deprecated,
|
||||||
createdAt: release.initial_date ? new Date(release.initial_date) : undefined,
|
createdAt: release.initial_date ? new Date(release.initial_date) : undefined,
|
||||||
starsCount: release.stars,
|
starsCount: release.stars,
|
||||||
};
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { TestConnectionError } from "../base/test-connection/test-connection-err
|
|||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||||
import type { ReleasesRepository, ReleasesResponse } from "../interfaces/releases-providers/releases-providers-types";
|
import type { ReleaseResponse } from "../interfaces/releases-providers/releases-providers-types";
|
||||||
import { releasesResponseSchema } from "./npm-schemas";
|
import { releasesResponseSchema } from "./npm-schemas";
|
||||||
|
|
||||||
export class NPMIntegration extends Integration implements ReleasesProviderIntegration {
|
export class NPMIntegration extends Integration implements ReleasesProviderIntegration {
|
||||||
@@ -22,35 +22,35 @@ export class NPMIntegration extends Integration implements ReleasesProviderInteg
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
|
||||||
const releasesResponse = await fetchWithTrustedCertificatesAsync(
|
if (!identifier) return { success: false, error: { code: "invalidIdentifier" } };
|
||||||
this.url(`/${encodeURIComponent(repository.identifier)}`),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url(`/${encodeURIComponent(identifier)}`));
|
||||||
if (!releasesResponse.ok) {
|
if (!releasesResponse.ok) {
|
||||||
return {
|
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
|
||||||
id: repository.id,
|
|
||||||
error: { message: releasesResponse.statusText },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||||
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return {
|
return {
|
||||||
id: repository.id,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
|
code: "unexpected",
|
||||||
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message,
|
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
const formattedReleases = data.time.map((tag) => ({
|
|
||||||
...tag,
|
|
||||||
releaseUrl: `https://www.npmjs.com/package/${encodeURIComponent(data.name)}/v/${encodeURIComponent(tag.latestRelease)}`,
|
|
||||||
releaseDescription: data.versions[tag.latestRelease]?.description ?? "",
|
|
||||||
}));
|
|
||||||
return getLatestRelease(formattedReleases, repository);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formattedReleases = data.time.map((tag) => ({
|
||||||
|
...tag,
|
||||||
|
releaseUrl: `https://www.npmjs.com/package/${encodeURIComponent(data.name)}/v/${encodeURIComponent(tag.latestRelease)}`,
|
||||||
|
releaseDescription: data.versions[tag.latestRelease]?.description ?? "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const latestRelease = getLatestRelease(formattedReleases, versionRegex);
|
||||||
|
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
|
|
||||||
|
return { success: true, data: latestRelease };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import type { ReleasesProviderIntegration } from "../interfaces/releases-provide
|
|||||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||||
import type {
|
import type {
|
||||||
ReleaseProviderResponse,
|
ReleaseProviderResponse,
|
||||||
ReleasesRepository,
|
ReleaseResponse,
|
||||||
ReleasesResponse,
|
|
||||||
} from "../interfaces/releases-providers/releases-providers-types";
|
} from "../interfaces/releases-providers/releases-providers-types";
|
||||||
import { releasesResponseSchema } from "./quay-schemas";
|
import { releasesResponseSchema } from "./quay-schemas";
|
||||||
|
|
||||||
@@ -43,20 +42,22 @@ export class QuayIntegration extends Integration implements ReleasesProviderInte
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
private parseIdentifier(identifier: string) {
|
||||||
const [owner, name] = repository.identifier.split("/");
|
const [owner, name] = identifier.split("/");
|
||||||
if (!owner || !name) {
|
if (!owner || !name) {
|
||||||
localLogger.warn(
|
localLogger.warn(`Invalid identifier format. Expected 'owner/name', for ${identifier} with Quay integration`, {
|
||||||
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with LinuxServerIO integration`,
|
identifier,
|
||||||
{
|
});
|
||||||
identifier: repository.identifier,
|
return null;
|
||||||
},
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
id: repository.id,
|
|
||||||
error: { code: "invalidIdentifier" },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return { owner, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
|
||||||
|
const parsedIdentifier = this.parseIdentifier(identifier);
|
||||||
|
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
|
||||||
|
|
||||||
|
const { owner, name } = parsedIdentifier;
|
||||||
|
|
||||||
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
||||||
return await fetchWithTrustedCertificatesAsync(
|
return await fetchWithTrustedCertificatesAsync(
|
||||||
@@ -68,42 +69,29 @@ export class QuayIntegration extends Integration implements ReleasesProviderInte
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!releasesResponse.ok) {
|
if (!releasesResponse.ok) {
|
||||||
return {
|
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
|
||||||
id: repository.id,
|
|
||||||
error: { message: releasesResponse.statusText },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||||
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return {
|
return { success: false, error: { code: "unexpected", message: error.message } };
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex);
|
||||||
|
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
|
||||||
|
|
||||||
|
return { success: true, data: { projectDescription: data.description, ...latestRelease } };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,19 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { getIconUrl } from "@homarr/definitions";
|
import type { ReleaseResponse, ReleasesRepository } from "@homarr/integrations";
|
||||||
import type { ReleasesResponse } from "@homarr/integrations";
|
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
|
|
||||||
export const releasesRequestHandler = createCachedIntegrationRequestHandler<
|
export const releasesRequestHandler = createCachedIntegrationRequestHandler<
|
||||||
ReleasesResponse,
|
ReleaseResponse,
|
||||||
IntegrationKindByCategory<"releasesProvider">,
|
IntegrationKindByCategory<"releasesProvider">,
|
||||||
{
|
ReleasesRepository
|
||||||
id: string;
|
|
||||||
identifier: string;
|
|
||||||
versionRegex?: string;
|
|
||||||
}
|
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, input) {
|
requestAsync: async (integration, input) => {
|
||||||
const integrationInstance = await createIntegrationAsync(integration);
|
const instance = await createIntegrationAsync(integration);
|
||||||
const response = await integrationInstance.getLatestMatchingReleaseAsync({
|
return instance.getLatestMatchingReleaseAsync(input.identifier, input.versionRegex);
|
||||||
id: input.id,
|
|
||||||
identifier: input.identifier,
|
|
||||||
versionRegex: input.versionRegex,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...response,
|
|
||||||
integration: {
|
|
||||||
name: integration.name,
|
|
||||||
iconUrl: getIconUrl(integration.kind),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(5, "minutes"),
|
cacheDuration: dayjs.duration(5, "minutes"),
|
||||||
queryKey: "repositoriesReleases",
|
queryKey: "repositoriesReleases",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import ReactMarkdown from "react-markdown";
|
|||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useRequiredBoard } from "@homarr/boards/context";
|
import { useRequiredBoard } from "@homarr/boards/context";
|
||||||
import { isDateWithin, isNullOrWhitespace, splitToChunksWithNItems } from "@homarr/common";
|
import { isDateWithin, isNullOrWhitespace, splitToChunksWithNItems } from "@homarr/common";
|
||||||
|
import { getIconUrl } from "@homarr/definitions";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { MaskedOrNormalImage } from "@homarr/ui";
|
import { MaskedOrNormalImage } from "@homarr/ui";
|
||||||
|
|
||||||
@@ -96,55 +97,33 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
|||||||
const repositories = useMemo(() => {
|
const repositories = useMemo(() => {
|
||||||
const formattedResults = options.repositories
|
const formattedResults = options.repositories
|
||||||
.map((repository) => {
|
.map((repository) => {
|
||||||
if (repository.providerIntegrationId === undefined) {
|
if (!repository.providerIntegrationId) return { ...repository, error: { code: "noProviderSeleceted" } };
|
||||||
return {
|
|
||||||
...repository,
|
|
||||||
isNewRelease: false,
|
|
||||||
isStaleRelease: false,
|
|
||||||
latestReleaseAt: undefined,
|
|
||||||
error: {
|
|
||||||
code: "noProviderSeleceted",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = results.flat().find(({ data }) => data.id === repository.id)?.data;
|
const repositoryResult = results.flat().find(({ id }) => id === repository.id);
|
||||||
|
if (!repositoryResult) return { ...repository, error: { code: "noProviderResponse" } };
|
||||||
|
if (!repositoryResult.success) return { ...repository, error: repositoryResult.error };
|
||||||
|
|
||||||
if (response === undefined)
|
const { data: release, integration } = repositoryResult;
|
||||||
return {
|
|
||||||
...repository,
|
const isReleaseWithin = (relativeDate: string) =>
|
||||||
isNewRelease: false,
|
Boolean(relativeDate) && isDateWithin(release.latestReleaseAt, relativeDate);
|
||||||
isStaleRelease: false,
|
|
||||||
latestReleaseAt: undefined,
|
|
||||||
error: {
|
|
||||||
code: "noProviderResponse",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...repository,
|
...repository,
|
||||||
...response,
|
...release,
|
||||||
isNewRelease:
|
integration: { name: integration.name, iconUrl: getIconUrl(integration.kind) },
|
||||||
relativeDateOptions.newReleaseWithin !== "" && response.latestReleaseAt
|
isNewRelease: isReleaseWithin(relativeDateOptions.newReleaseWithin),
|
||||||
? isDateWithin(response.latestReleaseAt, relativeDateOptions.newReleaseWithin)
|
isStaleRelease: !isReleaseWithin(relativeDateOptions.staleReleaseWithin),
|
||||||
: false,
|
viewed: releasesViewedList[repository.id] === release.latestRelease,
|
||||||
isStaleRelease:
|
|
||||||
relativeDateOptions.staleReleaseWithin !== "" && response.latestReleaseAt
|
|
||||||
? !isDateWithin(response.latestReleaseAt, relativeDateOptions.staleReleaseWithin)
|
|
||||||
: false,
|
|
||||||
viewed: releasesViewedList[repository.id] === response.latestRelease,
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(
|
.filter(
|
||||||
(repository) =>
|
(repository) =>
|
||||||
repository.error !== undefined ||
|
"error" in repository || !options.showOnlyHighlighted || repository.isNewRelease || repository.isStaleRelease,
|
||||||
!options.showOnlyHighlighted ||
|
|
||||||
repository.isNewRelease ||
|
|
||||||
repository.isStaleRelease,
|
|
||||||
)
|
)
|
||||||
.sort((repoA, repoB) => {
|
.sort((repoA, repoB) => {
|
||||||
if (repoA.latestReleaseAt === undefined) return -1;
|
if ("error" in repoA) return -1;
|
||||||
if (repoB.latestReleaseAt === undefined) return 1;
|
if ("error" in repoB) return 1;
|
||||||
return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1;
|
return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1;
|
||||||
}) as ReleasesRepositoryResponse[];
|
}) as ReleasesRepositoryResponse[];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user