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
+		}
+	}
 }