blob: e201c498ba80bd1e8b89725deda204bbf517fa36 [file] [log] [blame]
import axios from "axios";
import { GithubRepository, GithubRepositoriesSchema, DeployKeysSchema, ListWebhooksResponseSchema } 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 allRepos: GithubRepository[] = [];
let url: string | null = "https://api.github.com/user/repos?per_page=100";
while (url) {
const response = await axios.get(url, {
headers: this.getHeaders(),
});
const reposOnPage = GithubRepositoriesSchema.parse(response.data);
allRepos.push(...reposOnPage);
const linkHeader = response.headers.link as string | undefined;
if (linkHeader) {
const nextLink = linkHeader.split(",").find((s) => s.includes('rel="next"'));
if (nextLink) {
const match = nextLink.match(/<(.*)>/);
url = match ? match[1] : null;
} else {
url = null;
}
} else {
url = null;
}
}
return allRepos;
}
// TODO(giolekva): generate unique deploy key per repo and add it to the repo
async addDeployKey(repoPath: string, key: string) {
await this.addSSHKey(key);
// 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) {
this.removeSSHKey(key);
// 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
}
}
async addSSHKey(key: string) {
let existingKeys: { key: string }[] = [];
const response = await axios.get(`https://api.github.com/user/keys`, {
headers: this.getHeaders(),
});
if (Array.isArray(response.data)) {
existingKeys = response.data;
}
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/user/keys`,
{
title: "dodo",
key: key,
},
{
headers: this.getHeaders(),
},
);
}
async removeSSHKey(key: string) {
const response = await axios.get(`https://api.github.com/user/keys`, {
headers: this.getHeaders(),
});
const result = DeployKeysSchema.safeParse(response.data);
if (!result.success) {
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) {
return;
}
await Promise.all(
deployKeysToDelete.map((deployKey) => {
return axios.delete(`https://api.github.com/user/keys/${deployKey.id}`, {
headers: this.getHeaders(),
});
}),
);
}
}