2025-07-11 19:54:17 +01:00
|
|
|
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 {
|
|
|
|
|
DetailsProviderResponse,
|
2025-10-29 14:33:50 -04:00
|
|
|
ReleaseResponse,
|
2025-07-11 19:54:17 +01:00
|
|
|
} from "../interfaces/releases-providers/releases-providers-types";
|
|
|
|
|
import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas";
|
|
|
|
|
|
|
|
|
|
const localLogger = logger.child({ module: "CodebergIntegration" });
|
|
|
|
|
|
|
|
|
|
export class CodebergIntegration extends Integration implements ReleasesProviderIntegration {
|
|
|
|
|
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
2025-08-01 10:13:20 +01:00
|
|
|
if (!this.hasSecretValue("personalAccessToken")) return await callback(undefined);
|
2025-07-11 19:54:17 +01:00
|
|
|
|
|
|
|
|
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("/version"), {
|
|
|
|
|
headers,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
return TestConnectionError.StatusResult(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-29 14:33:50 -04:00
|
|
|
private parseIdentifier(identifier: string) {
|
|
|
|
|
const [owner, name] = identifier.split("/");
|
2025-07-11 19:54:17 +01:00
|
|
|
if (!owner || !name) {
|
|
|
|
|
localLogger.warn(
|
2025-10-29 14:33:50 -04:00
|
|
|
`Invalid identifier format. Expected 'owner/name', for ${identifier} with Codeberg integration`,
|
|
|
|
|
{ identifier },
|
2025-07-11 19:54:17 +01:00
|
|
|
);
|
2025-10-29 14:33:50 -04:00
|
|
|
return null;
|
2025-07-11 19:54:17 +01:00
|
|
|
}
|
2025-10-29 14:33:50 -04:00
|
|
|
return { owner, name };
|
|
|
|
|
}
|
2025-07-11 19:54:17 +01:00
|
|
|
|
2025-10-29 14:33:50 -04:00
|
|
|
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;
|
2025-07-11 19:54:17 +01:00
|
|
|
|
|
|
|
|
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
2025-08-01 10:13:20 +01:00
|
|
|
return await fetchWithTrustedCertificatesAsync(
|
2025-07-11 19:54:17 +01:00
|
|
|
this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`),
|
|
|
|
|
{ headers },
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
if (!releasesResponse.ok) {
|
2025-10-29 14:33:50 -04:00
|
|
|
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
|
2025-07-11 19:54:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const releasesResponseJson: unknown = await releasesResponse.json();
|
|
|
|
|
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
|
|
|
|
if (!success) {
|
|
|
|
|
return {
|
2025-10-29 14:33:50 -04:00
|
|
|
success: false,
|
2025-07-11 19:54:17 +01:00
|
|
|
error: {
|
2025-10-29 14:33:50 -04:00
|
|
|
code: "unexpected",
|
2025-07-11 19:54:17 +01:00
|
|
|
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-10-29 14:33:50 -04:00
|
|
|
|
|
|
|
|
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 } };
|
2025-07-11 19:54:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected async getDetailsAsync(owner: string, name: string): Promise<DetailsProviderResponse | undefined> {
|
|
|
|
|
const response = await this.withHeadersAsync(async (headers) => {
|
|
|
|
|
return await fetchWithTrustedCertificatesAsync(
|
|
|
|
|
this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`),
|
|
|
|
|
{
|
|
|
|
|
headers,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
localLogger.warn(`Failed to get details response for ${owner}/${name} with Codeberg integration`, {
|
|
|
|
|
owner,
|
|
|
|
|
name,
|
|
|
|
|
error: response.statusText,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const responseJson = await response.json();
|
|
|
|
|
const { data, success, error } = detailsResponseSchema.safeParse(responseJson);
|
|
|
|
|
|
|
|
|
|
if (!success) {
|
|
|
|
|
localLogger.warn(`Failed to parse details response for ${owner}/${name} with Codeberg integration`, {
|
|
|
|
|
owner,
|
|
|
|
|
name,
|
|
|
|
|
error,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
projectUrl: data.html_url,
|
|
|
|
|
projectDescription: data.description,
|
|
|
|
|
isFork: data.fork,
|
|
|
|
|
isArchived: data.archived,
|
|
|
|
|
createdAt: data.created_at,
|
|
|
|
|
starsCount: data.stars_count,
|
|
|
|
|
openIssues: data.open_issues_count,
|
|
|
|
|
forksCount: data.forks_count,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|