blob: df8eacf7dfacd7122680c511c73355e5a07adecc [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001import axios from "axios";
gio9b7421a2025-06-18 12:31:13 +00002import {
3 GithubRepository,
4 GithubRepositoriesSchema,
5 DeployKeysSchema,
6 ListWebhooksResponseSchema,
7 DeployKeys,
8} from "config";
giod0026612025-05-08 13:00:36 +00009
10export class GithubClient {
11 private token: string;
12
13 constructor(token: string) {
14 this.token = token;
15 }
16
17 private getHeaders() {
18 return {
19 Authorization: `Bearer ${this.token}`,
20 Accept: "application/vnd.github.v3+json",
gio76d8ae62025-05-19 15:21:54 +000021 "X-GitHub-Api-Version": "2022-11-28",
giod0026612025-05-08 13:00:36 +000022 };
23 }
24
25 async getRepositories(): Promise<GithubRepository[]> {
26 const response = await axios.get("https://api.github.com/user/repos", {
27 headers: this.getHeaders(),
28 });
gio9b7421a2025-06-18 12:31:13 +000029 return GithubRepositoriesSchema.parse(response.data);
giod0026612025-05-08 13:00:36 +000030 }
31
32 async addDeployKey(repoPath: string, key: string) {
33 const sshUrl = repoPath;
34 const repoOwnerAndName = sshUrl.replace("git@github.com:", "").replace(".git", "");
gio9b7421a2025-06-18 12:31:13 +000035 let existingKeys: DeployKeys = [];
gioa71316d2025-05-24 09:41:36 +040036 const response = await axios.get(`https://api.github.com/repos/${repoOwnerAndName}/keys`, {
37 headers: this.getHeaders(),
38 });
39 const parsedResult = DeployKeysSchema.safeParse(response.data);
40 if (parsedResult.success) {
41 existingKeys = parsedResult.data;
42 } else {
43 console.error("Failed to parse existing deploy keys:", parsedResult.error);
44 }
45 const keyToAddParts = key.trim().split(" ");
46 const mainKeyPartToAdd = keyToAddParts.length > 1 ? keyToAddParts.slice(0, 2).join(" ") : key.trim();
47 const keyAlreadyExists = existingKeys.some((existingKeyObj) => {
48 const existingKeyParts = existingKeyObj.key.trim().split(" ");
49 const mainExistingKeyPart =
50 existingKeyParts.length > 1 ? existingKeyParts.slice(0, 2).join(" ") : existingKeyObj.key.trim();
51 return mainExistingKeyPart === mainKeyPartToAdd;
52 });
53 if (keyAlreadyExists) {
54 return;
55 }
giod0026612025-05-08 13:00:36 +000056 await axios.post(
57 `https://api.github.com/repos/${repoOwnerAndName}/keys`,
58 {
59 title: "dodo",
60 key: key,
61 read_only: true,
62 },
63 {
64 headers: this.getHeaders(),
65 },
66 );
67 }
gio3ed59592025-05-14 16:51:09 +000068
69 async removeDeployKey(repoPath: string, key: string) {
70 const sshUrl = repoPath;
71 const repoOwnerAndName = sshUrl.replace("git@github.com:", "").replace(".git", "");
72 const response = await axios.get(`https://api.github.com/repos/${repoOwnerAndName}/keys`, {
73 headers: this.getHeaders(),
74 });
75 const result = DeployKeysSchema.safeParse(response.data);
76 if (!result.success) {
gioa71316d2025-05-24 09:41:36 +040077 console.error("Failed to parse deploy keys response for removal:", result.error);
78 // Depending on desired robustness, you might throw an error or return early.
79 // For now, if parsing fails, we can't identify keys to remove.
80 throw new Error("Failed to parse deploy keys response during removal process");
gio3ed59592025-05-14 16:51:09 +000081 }
gioa71316d2025-05-24 09:41:36 +040082
83 // Extract the main part of the key we intend to remove
84 const keyToRemoveParts = key.trim().split(" ");
85 const mainKeyPartToRemove = keyToRemoveParts.length > 1 ? keyToRemoveParts.slice(0, 2).join(" ") : key.trim();
86
87 const deployKeysToDelete = result.data.filter((existingKeyObj) => {
88 const existingKeyParts = existingKeyObj.key.trim().split(" ");
89 const mainExistingKeyPart =
90 existingKeyParts.length > 1 ? existingKeyParts.slice(0, 2).join(" ") : existingKeyObj.key.trim();
91 return mainExistingKeyPart === mainKeyPartToRemove;
92 });
93
94 if (deployKeysToDelete.length === 0) {
95 console.log(
96 `No deploy key matching '${mainKeyPartToRemove.substring(0, 50)}...' found in repo ${repoOwnerAndName} for removal.`,
97 );
98 return;
99 }
100
gio3ed59592025-05-14 16:51:09 +0000101 await Promise.all(
gioa71316d2025-05-24 09:41:36 +0400102 deployKeysToDelete.map((deployKey) => {
103 console.log(
104 `Removing deploy key ID ${deployKey.id} ('${deployKey.key.substring(0, 50)}...') from repo ${repoOwnerAndName}`,
105 );
106 return axios.delete(`https://api.github.com/repos/${repoOwnerAndName}/keys/${deployKey.id}`, {
gio3ed59592025-05-14 16:51:09 +0000107 headers: this.getHeaders(),
gioa71316d2025-05-24 09:41:36 +0400108 });
109 }),
110 );
111 console.log(
112 `Successfully initiated removal of ${deployKeysToDelete.length} matching deploy key(s) from ${repoOwnerAndName}.`,
gio3ed59592025-05-14 16:51:09 +0000113 );
114 }
gio76d8ae62025-05-19 15:21:54 +0000115
116 async addPushWebhook(repoPath: string, callbackAddress: string): Promise<boolean> {
117 const repoOwnerAndName = repoPath.replace("git@github.com:", "").replace(".git", "");
118 const resp = await axios.post(
119 `https://api.github.com/repos/${repoOwnerAndName}/hooks`,
120 {
121 name: "web",
122 active: true,
123 events: ["push"],
124 config: {
125 url: callbackAddress,
126 content_type: "json",
127 },
128 },
129 {
130 headers: this.getHeaders(),
131 },
132 );
133 return resp.status === 201;
134 }
135
136 async removePushWebhook(repoPath: string, callbackAddress: string): Promise<boolean> {
137 const repoOwnerAndName = repoPath.replace("git@github.com:", "").replace(".git", "");
138 const listHooksUrl = `https://api.github.com/repos/${repoOwnerAndName}/hooks`;
139 try {
140 const response = await axios.get(listHooksUrl, {
141 headers: this.getHeaders(),
142 });
143 const parsedHooks = ListWebhooksResponseSchema.safeParse(response.data);
144 if (!parsedHooks.success) {
145 throw new Error(`Failed to parse webhooks list for ${repoOwnerAndName}`);
146 }
147 const results = await Promise.all(
148 parsedHooks.data
149 .filter((hook) => hook.config.url === callbackAddress && hook.events.includes("push"))
150 .map(async (hook) => {
151 const deleteHookUrl = `https://api.github.com/repos/${repoOwnerAndName}/hooks/${hook.id}`;
152 const resp = await axios.delete(deleteHookUrl, { headers: this.getHeaders() });
153 return resp.status === 204;
154 }),
155 );
156 return results.reduce((acc, curr) => acc && curr, true);
157 // eslint-disable-next-line @typescript-eslint/no-explicit-any
158 } catch (error: any) {
159 console.error(
160 `Failed to list or remove webhooks for ${repoOwnerAndName}:`,
161 error.response?.data || error.message,
162 );
163 throw error; // Re-throw to let the caller handle it
164 }
165 }
giod0026612025-05-08 13:00:36 +0000166}