blob: b758aca72887ba66a64ddb4919f5a4521393f244 [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001import axios from "axios";
2import { z } from "zod";
3
4export const GithubRepositorySchema = z.object({
5 id: z.number(),
6 name: z.string(),
7 full_name: z.string(),
8 html_url: z.string(),
9 ssh_url: z.string(),
10});
11
gio3ed59592025-05-14 16:51:09 +000012const DeployKeysSchema = z.array(
13 z.object({
14 id: z.number(),
15 key: z.string(),
16 }),
17);
18
gio76d8ae62025-05-19 15:21:54 +000019const WebhookSchema = z.object({
20 id: z.number(),
21 config: z.object({
22 url: z.string().optional(), // url might not always be present
23 content_type: z.string().optional(),
24 }),
25 events: z.array(z.string()),
26 active: z.boolean(),
27});
28
29const ListWebhooksResponseSchema = z.array(WebhookSchema);
30
giod0026612025-05-08 13:00:36 +000031export type GithubRepository = z.infer<typeof GithubRepositorySchema>;
32
33export class GithubClient {
34 private token: string;
35
36 constructor(token: string) {
37 this.token = token;
38 }
39
40 private getHeaders() {
41 return {
42 Authorization: `Bearer ${this.token}`,
43 Accept: "application/vnd.github.v3+json",
gio76d8ae62025-05-19 15:21:54 +000044 "X-GitHub-Api-Version": "2022-11-28",
giod0026612025-05-08 13:00:36 +000045 };
46 }
47
48 async getRepositories(): Promise<GithubRepository[]> {
49 const response = await axios.get("https://api.github.com/user/repos", {
50 headers: this.getHeaders(),
51 });
52 return z.array(GithubRepositorySchema).parse(response.data);
53 }
54
55 async addDeployKey(repoPath: string, key: string) {
56 const sshUrl = repoPath;
57 const repoOwnerAndName = sshUrl.replace("git@github.com:", "").replace(".git", "");
giod0026612025-05-08 13:00:36 +000058 await axios.post(
59 `https://api.github.com/repos/${repoOwnerAndName}/keys`,
60 {
61 title: "dodo",
62 key: key,
63 read_only: true,
64 },
65 {
66 headers: this.getHeaders(),
67 },
68 );
69 }
gio3ed59592025-05-14 16:51:09 +000070
71 async removeDeployKey(repoPath: string, key: string) {
72 const sshUrl = repoPath;
73 const repoOwnerAndName = sshUrl.replace("git@github.com:", "").replace(".git", "");
74 const response = await axios.get(`https://api.github.com/repos/${repoOwnerAndName}/keys`, {
75 headers: this.getHeaders(),
76 });
77 const result = DeployKeysSchema.safeParse(response.data);
78 if (!result.success) {
79 throw new Error("Failed to parse deploy keys response");
80 }
81 const deployKeys = result.data.filter((k) => k.key === key);
82 await Promise.all(
83 deployKeys.map((deployKey) =>
84 axios.delete(`https://api.github.com/repos/${repoOwnerAndName}/keys/${deployKey.id}`, {
85 headers: this.getHeaders(),
86 }),
87 ),
88 );
89 }
gio76d8ae62025-05-19 15:21:54 +000090
91 async addPushWebhook(repoPath: string, callbackAddress: string): Promise<boolean> {
92 const repoOwnerAndName = repoPath.replace("git@github.com:", "").replace(".git", "");
93 const resp = await axios.post(
94 `https://api.github.com/repos/${repoOwnerAndName}/hooks`,
95 {
96 name: "web",
97 active: true,
98 events: ["push"],
99 config: {
100 url: callbackAddress,
101 content_type: "json",
102 },
103 },
104 {
105 headers: this.getHeaders(),
106 },
107 );
108 return resp.status === 201;
109 }
110
111 async removePushWebhook(repoPath: string, callbackAddress: string): Promise<boolean> {
112 const repoOwnerAndName = repoPath.replace("git@github.com:", "").replace(".git", "");
113 const listHooksUrl = `https://api.github.com/repos/${repoOwnerAndName}/hooks`;
114 try {
115 const response = await axios.get(listHooksUrl, {
116 headers: this.getHeaders(),
117 });
118 const parsedHooks = ListWebhooksResponseSchema.safeParse(response.data);
119 if (!parsedHooks.success) {
120 throw new Error(`Failed to parse webhooks list for ${repoOwnerAndName}`);
121 }
122 const results = await Promise.all(
123 parsedHooks.data
124 .filter((hook) => hook.config.url === callbackAddress && hook.events.includes("push"))
125 .map(async (hook) => {
126 const deleteHookUrl = `https://api.github.com/repos/${repoOwnerAndName}/hooks/${hook.id}`;
127 const resp = await axios.delete(deleteHookUrl, { headers: this.getHeaders() });
128 return resp.status === 204;
129 }),
130 );
131 return results.reduce((acc, curr) => acc && curr, true);
132 // eslint-disable-next-line @typescript-eslint/no-explicit-any
133 } catch (error: any) {
134 console.error(
135 `Failed to list or remove webhooks for ${repoOwnerAndName}:`,
136 error.response?.data || error.message,
137 );
138 throw error; // Re-throw to let the caller handle it
139 }
140 }
giod0026612025-05-08 13:00:36 +0000141}