UNPKG

repoweaver

Version:

A GitHub App that skillfully weaves multiple templates together to create and update repositories with intelligent merge strategies

276 lines (235 loc) 8.54 kB
import { Request, Response } from 'express'; import { createHmac } from 'crypto'; import { GitHubClient } from './github-client'; import { GitHubBootstrapper } from './github-bootstrapper'; import { Database } from './database'; export interface WebhookEvent { action: string; installation?: { id: number; account: { login: string; type: string; }; }; repository?: { name: string; full_name: string; owner: { login: string; }; }; repositories?: Array<{ name: string; full_name: string; }>; sender?: { login: string; }; } export class WebhookHandler { private webhookSecret: string; private appId: string; private privateKey: string; private database: Database; constructor( webhookSecret: string, appId: string, privateKey: string, database: Database ) { this.webhookSecret = webhookSecret; this.appId = appId; this.privateKey = privateKey; this.database = database; } async handleWebhook(req: Request, res: Response): Promise<void> { try { // Verify webhook signature if (!this.verifySignature(req)) { res.status(401).json({ error: 'Invalid signature' }); return; } const event = req.headers['x-github-event'] as string; const payload = req.body as WebhookEvent; console.log(`Received webhook: ${event} - ${payload.action}`); switch (event) { case 'installation': await this.handleInstallation(payload); break; case 'installation_repositories': await this.handleInstallationRepositories(payload); break; case 'push': await this.handlePush(payload); break; case 'repository': await this.handleRepository(payload); break; case 'pull_request': await this.handlePullRequest(payload); break; default: console.log(`Unhandled event: ${event}`); } res.status(200).json({ message: 'Webhook processed successfully' }); } catch (error) { console.error('Webhook processing error:', error); res.status(500).json({ error: 'Internal server error' }); } } private verifySignature(req: Request): boolean { const signature = req.headers['x-hub-signature-256'] as string; if (!signature) { return false; } const hmac = createHmac('sha256', this.webhookSecret); hmac.update(JSON.stringify(req.body)); const expectedSignature = `sha256=${hmac.digest('hex')}`; return signature === expectedSignature; } private async handleInstallation(payload: WebhookEvent): Promise<void> { if (!payload.installation) return; const { installation } = payload; switch (payload.action) { case 'created': await this.database.createInstallation({ id: installation.id, account: installation.account.login, accountType: installation.account.type }); console.log(`Installation created for ${installation.account.login}`); break; case 'deleted': await this.database.deleteInstallation(installation.id); console.log(`Installation deleted for ${installation.account.login}`); break; case 'suspend': await this.database.suspendInstallation(installation.id); console.log(`Installation suspended for ${installation.account.login}`); break; case 'unsuspend': await this.database.unsuspendInstallation(installation.id); console.log(`Installation unsuspended for ${installation.account.login}`); break; } } private async handleInstallationRepositories(payload: WebhookEvent): Promise<void> { if (!payload.installation || !payload.repositories) return; const { installation, repositories } = payload; switch (payload.action) { case 'added': for (const repo of repositories) { await this.database.addRepositoryToInstallation( installation.id, repo.name, repo.full_name ); } console.log(`Added ${repositories.length} repositories to installation ${installation.id}`); break; case 'removed': for (const repo of repositories) { await this.database.removeRepositoryFromInstallation( installation.id, repo.name ); } console.log(`Removed ${repositories.length} repositories from installation ${installation.id}`); break; } } private async handlePush(payload: WebhookEvent): Promise<void> { if (!payload.repository || !payload.installation) return; const { repository, installation } = payload; // Check if this is a push to a template repository const templateConfigs = await this.database.getTemplateConfigurations(repository.full_name); if (templateConfigs.length > 0) { console.log(`Template repository ${repository.full_name} was updated`); // Queue updates for all repositories using this template for (const config of templateConfigs) { await this.queueTemplateUpdate( installation.id, config.targetRepository, repository.full_name ); } } } private async handleRepository(payload: WebhookEvent): Promise<void> { if (!payload.repository || !payload.installation) return; const { repository, installation } = payload; switch (payload.action) { case 'created': // Check if this repository should be auto-configured with templates const installationConfig = await this.database.getInstallationConfig(installation.id); if (installationConfig?.autoConfigureTemplates) { await this.autoConfigureRepository(installation.id, repository); } break; case 'deleted': await this.database.deleteRepositoryConfig(repository.full_name); break; } } private async handlePullRequest(payload: WebhookEvent): Promise<void> { if (!payload.repository || !payload.installation) return; // Handle PR events related to template updates // This could include auto-merging approved template updates console.log(`Pull request ${payload.action} in ${payload.repository.full_name}`); } private async queueTemplateUpdate( installationId: number, targetRepository: string, templateRepository: string ): Promise<void> { try { const client = new GitHubClient(this.appId, this.privateKey, installationId); const bootstrapper = new GitHubBootstrapper(client); // Get repository configuration const [owner, repo] = targetRepository.split('/'); const templates = await bootstrapper.getRepositoryTemplates(owner, repo); // Find the template that was updated const templateConfig = templates.find(t => t.includes(templateRepository)); if (!templateConfig) { console.log(`No template configuration found for ${templateRepository}`); return; } // Queue the update job await this.database.queueJob({ type: 'template_update', installationId, targetRepository, templateRepository, status: 'pending', createdAt: new Date() }); console.log(`Queued template update for ${targetRepository} from ${templateRepository}`); } catch (error) { console.error(`Failed to queue template update: ${error}`); } } private async autoConfigureRepository( installationId: number, repository: { name: string; full_name: string; owner: { login: string } } ): Promise<void> { try { const client = new GitHubClient(this.appId, this.privateKey, installationId); const bootstrapper = new GitHubBootstrapper(client); // Get default templates for this installation const installationConfig = await this.database.getInstallationConfig(installationId); const defaultTemplates = installationConfig?.defaultTemplates || []; if (defaultTemplates.length > 0) { // Save default templates to the new repository await bootstrapper.saveRepositoryTemplates( repository.owner.login, repository.name, defaultTemplates ); console.log(`Auto-configured ${repository.full_name} with ${defaultTemplates.length} templates`); } } catch (error) { console.error(`Failed to auto-configure repository: ${error}`); } } }