blob: b758aca72887ba66a64ddb4919f5a4521393f244 [file] [log] [blame]
import axios from "axios";
import { z } from "zod";
export const GithubRepositorySchema = z.object({
id: z.number(),
name: z.string(),
full_name: z.string(),
html_url: z.string(),
ssh_url: z.string(),
});
const DeployKeysSchema = z.array(
z.object({
id: z.number(),
key: z.string(),
}),
);
const WebhookSchema = z.object({
id: z.number(),
config: z.object({
url: z.string().optional(), // url might not always be present
content_type: z.string().optional(),
}),
events: z.array(z.string()),
active: z.boolean(),
});
const ListWebhooksResponseSchema = z.array(WebhookSchema);
export type GithubRepository = z.infer<typeof GithubRepositorySchema>;
export class GithubClient {
private token: string;
constructor(token: string) {
this.token = token;
}
private getHeaders() {
return {
Authorization: `Bearer ${this.token}`,
Accept: "application/vnd.github.v3+json",
"X-GitHub-Api-Version": "2022-11-28",
};
}
async getRepositories(): Promise<GithubRepository[]> {
const response = await axios.get("https://api.github.com/user/repos", {
headers: this.getHeaders(),
});
return z.array(GithubRepositorySchema).parse(response.data);
}
async addDeployKey(repoPath: string, key: string) {
const sshUrl = repoPath;
const repoOwnerAndName = sshUrl.replace("git@github.com:", "").replace(".git", "");
await axios.post(
`https://api.github.com/repos/${repoOwnerAndName}/keys`,
{
title: "dodo",
key: key,
read_only: true,
},
{
headers: this.getHeaders(),
},
);
}
async removeDeployKey(repoPath: string, key: string) {
const sshUrl = repoPath;
const repoOwnerAndName = sshUrl.replace("git@github.com:", "").replace(".git", "");
const response = await axios.get(`https://api.github.com/repos/${repoOwnerAndName}/keys`, {
headers: this.getHeaders(),
});
const result = DeployKeysSchema.safeParse(response.data);
if (!result.success) {
throw new Error("Failed to parse deploy keys response");
}
const deployKeys = result.data.filter((k) => k.key === key);
await Promise.all(
deployKeys.map((deployKey) =>
axios.delete(`https://api.github.com/repos/${repoOwnerAndName}/keys/${deployKey.id}`, {
headers: this.getHeaders(),
}),
),
);
}
async addPushWebhook(repoPath: string, callbackAddress: string): Promise<boolean> {
const repoOwnerAndName = repoPath.replace("git@github.com:", "").replace(".git", "");
const resp = await axios.post(
`https://api.github.com/repos/${repoOwnerAndName}/hooks`,
{
name: "web",
active: true,
events: ["push"],
config: {
url: callbackAddress,
content_type: "json",
},
},
{
headers: this.getHeaders(),
},
);
return resp.status === 201;
}
async removePushWebhook(repoPath: string, callbackAddress: string): Promise<boolean> {
const repoOwnerAndName = repoPath.replace("git@github.com:", "").replace(".git", "");
const listHooksUrl = `https://api.github.com/repos/${repoOwnerAndName}/hooks`;
try {
const response = await axios.get(listHooksUrl, {
headers: this.getHeaders(),
});
const parsedHooks = ListWebhooksResponseSchema.safeParse(response.data);
if (!parsedHooks.success) {
throw new Error(`Failed to parse webhooks list for ${repoOwnerAndName}`);
}
const results = await Promise.all(
parsedHooks.data
.filter((hook) => hook.config.url === callbackAddress && hook.events.includes("push"))
.map(async (hook) => {
const deleteHookUrl = `https://api.github.com/repos/${repoOwnerAndName}/hooks/${hook.id}`;
const resp = await axios.delete(deleteHookUrl, { headers: this.getHeaders() });
return resp.status === 204;
}),
);
return results.reduce((acc, curr) => acc && curr, true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error(
`Failed to list or remove webhooks for ${repoOwnerAndName}:`,
error.response?.data || error.message,
);
throw error; // Re-throw to let the caller handle it
}
}
}