blob: df8eacf7dfacd7122680c511c73355e5a07adecc [file] [log] [blame]
import axios from "axios";
import {
GithubRepository,
GithubRepositoriesSchema,
DeployKeysSchema,
ListWebhooksResponseSchema,
DeployKeys,
} from "config";
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 GithubRepositoriesSchema.parse(response.data);
}
async addDeployKey(repoPath: string, key: string) {
const sshUrl = repoPath;
const repoOwnerAndName = sshUrl.replace("git@github.com:", "").replace(".git", "");
let existingKeys: DeployKeys = [];
const response = await axios.get(`https://api.github.com/repos/${repoOwnerAndName}/keys`, {
headers: this.getHeaders(),
});
const parsedResult = DeployKeysSchema.safeParse(response.data);
if (parsedResult.success) {
existingKeys = parsedResult.data;
} else {
console.error("Failed to parse existing deploy keys:", parsedResult.error);
}
const keyToAddParts = key.trim().split(" ");
const mainKeyPartToAdd = keyToAddParts.length > 1 ? keyToAddParts.slice(0, 2).join(" ") : key.trim();
const keyAlreadyExists = existingKeys.some((existingKeyObj) => {
const existingKeyParts = existingKeyObj.key.trim().split(" ");
const mainExistingKeyPart =
existingKeyParts.length > 1 ? existingKeyParts.slice(0, 2).join(" ") : existingKeyObj.key.trim();
return mainExistingKeyPart === mainKeyPartToAdd;
});
if (keyAlreadyExists) {
return;
}
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) {
console.error("Failed to parse deploy keys response for removal:", result.error);
// Depending on desired robustness, you might throw an error or return early.
// For now, if parsing fails, we can't identify keys to remove.
throw new Error("Failed to parse deploy keys response during removal process");
}
// Extract the main part of the key we intend to remove
const keyToRemoveParts = key.trim().split(" ");
const mainKeyPartToRemove = keyToRemoveParts.length > 1 ? keyToRemoveParts.slice(0, 2).join(" ") : key.trim();
const deployKeysToDelete = result.data.filter((existingKeyObj) => {
const existingKeyParts = existingKeyObj.key.trim().split(" ");
const mainExistingKeyPart =
existingKeyParts.length > 1 ? existingKeyParts.slice(0, 2).join(" ") : existingKeyObj.key.trim();
return mainExistingKeyPart === mainKeyPartToRemove;
});
if (deployKeysToDelete.length === 0) {
console.log(
`No deploy key matching '${mainKeyPartToRemove.substring(0, 50)}...' found in repo ${repoOwnerAndName} for removal.`,
);
return;
}
await Promise.all(
deployKeysToDelete.map((deployKey) => {
console.log(
`Removing deploy key ID ${deployKey.id} ('${deployKey.key.substring(0, 50)}...') from repo ${repoOwnerAndName}`,
);
return axios.delete(`https://api.github.com/repos/${repoOwnerAndName}/keys/${deployKey.id}`, {
headers: this.getHeaders(),
});
}),
);
console.log(
`Successfully initiated removal of ${deployKeysToDelete.length} matching deploy key(s) from ${repoOwnerAndName}.`,
);
}
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
}
}
}