Canvas: Auto register github webhook upon deploy
Change-Id: I0321a032014d58016926189869b0fc24ad7ee2b1
diff --git a/apps/canvas/back/src/github.ts b/apps/canvas/back/src/github.ts
index 60d2191..b758aca 100644
--- a/apps/canvas/back/src/github.ts
+++ b/apps/canvas/back/src/github.ts
@@ -16,6 +16,18 @@
}),
);
+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 {
@@ -29,6 +41,7 @@
return {
Authorization: `Bearer ${this.token}`,
Accept: "application/vnd.github.v3+json",
+ "X-GitHub-Api-Version": "2022-11-28",
};
}
@@ -74,4 +87,55 @@
),
);
}
+
+ 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
+ }
+ }
}