From c48242a1415c2cb9388e73a180068fdf34d57864 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Wed, 27 Aug 2025 17:13:29 -0700 Subject: [PATCH] feat: set up github endpoint --- src/interfaces/github.ts | 678 ++++++++++++++++++++++++++++++ src/modules/processGitHubEvent.ts | 43 ++ src/server/serve.ts | 8 + 3 files changed, 729 insertions(+) create mode 100644 src/interfaces/github.ts create mode 100644 src/modules/processGitHubEvent.ts diff --git a/src/interfaces/github.ts b/src/interfaces/github.ts new file mode 100644 index 0000000..6506c60 --- /dev/null +++ b/src/interfaces/github.ts @@ -0,0 +1,678 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +/* eslint-disable @typescript-eslint/naming-convention, max-lines -- These are API interfaces. */ + +interface Issue { + id: number; + node_id: string; + url: string; + repository_url: string; + labels_url: string; + comments_url: string; + events_url: string; + html_url: string; + number: number; + state: string; + title: string; + body: string; + user: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + labels: Array<{ + id: number; + node_id: string; + url: string; + name: string; + description: string; + color: string; + default: boolean; + }>; + assignee: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + assignees: Array<{ + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }>; + milestone: { + url: string; + html_url: string; + labels_url: string; + id: number; + node_id: string; + number: number; + state: string; + title: string; + description: string; + creator: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + open_issues: number; + closed_issues: number; + created_at: string; + updated_at: string; + closed_at: string; + due_on: string; + }; + locked: boolean; + active_lock_reason: string; + comments: number; + pull_request: { + url: string; + html_url: string; + diff_url: string; + patch_url: string; + }; + closed_at: unknown; + created_at: string; + updated_at: string; + closed_by: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + author_association: string; + state_reason: string; +} + +interface IssueCreated { + action: "opened"; + issue: Issue; +} + +interface PullRequest { + url: string; + id: number; + node_id: string; + html_url: string; + diff_url: string; + patch_url: string; + issue_url: string; + commits_url: string; + review_comments_url: string; + review_comment_url: string; + comments_url: string; + statuses_url: string; + number: number; + state: string; + locked: boolean; + title: string; + user: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + body: string; + labels: Array<{ + id: number; + node_id: string; + url: string; + name: string; + description: string; + color: string; + default: boolean; + }>; + milestone: { + url: string; + html_url: string; + labels_url: string; + id: number; + node_id: string; + number: number; + state: string; + title: string; + description: string; + creator: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + open_issues: number; + closed_issues: number; + created_at: string; + updated_at: string; + closed_at: string; + due_on: string; + }; + active_lock_reason: string; + created_at: string; + updated_at: string; + closed_at: string; + merged_at: string; + merge_commit_sha: string; + assignee: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + assignees: Array<{ + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }>; + requested_reviewers: Array<{ + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }>; + requested_teams: Array<{ + id: number; + node_id: string; + url: string; + html_url: string; + name: string; + slug: string; + description: string; + privacy: string; + notification_setting: string; + permission: string; + members_url: string; + repositories_url: string; + }>; + head: { + label: string; + ref: string; + sha: string; + user: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + repo: { + id: number; + node_id: string; + name: string; + full_name: string; + owner: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + private: boolean; + html_url: string; + description: string; + fork: boolean; + url: string; + archive_url: string; + assignees_url: string; + blobs_url: string; + branches_url: string; + collaborators_url: string; + comments_url: string; + commits_url: string; + compare_url: string; + contents_url: string; + contributors_url: string; + deployments_url: string; + downloads_url: string; + events_url: string; + forks_url: string; + git_commits_url: string; + git_refs_url: string; + git_tags_url: string; + git_url: string; + issue_comment_url: string; + issue_events_url: string; + issues_url: string; + keys_url: string; + labels_url: string; + languages_url: string; + merges_url: string; + milestones_url: string; + notifications_url: string; + pulls_url: string; + releases_url: string; + ssh_url: string; + stargazers_url: string; + statuses_url: string; + subscribers_url: string; + subscription_url: string; + tags_url: string; + teams_url: string; + trees_url: string; + clone_url: string; + mirror_url: string; + hooks_url: string; + svn_url: string; + homepage: string; + language: unknown; + forks_count: number; + stargazers_count: number; + watchers_count: number; + size: number; + default_branch: string; + open_issues_count: number; + topics: Array; + has_issues: boolean; + has_projects: boolean; + has_wiki: boolean; + has_pages: boolean; + has_downloads: boolean; + has_discussions: boolean; + archived: boolean; + disabled: boolean; + pushed_at: string; + created_at: string; + updated_at: string; + permissions: { + admin: boolean; + push: boolean; + pull: boolean; + }; + allow_rebase_merge: boolean; + temp_clone_token: string; + allow_squash_merge: boolean; + allow_merge_commit: boolean; + allow_forking: boolean; + forks: number; + open_issues: number; + license: { + key: string; + name: string; + url: string; + spdx_id: string; + node_id: string; + }; + watchers: number; + }; + }; + base: { + label: string; + ref: string; + sha: string; + user: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + repo: { + id: number; + node_id: string; + name: string; + full_name: string; + owner: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + private: boolean; + html_url: string; + description: string; + fork: boolean; + url: string; + archive_url: string; + assignees_url: string; + blobs_url: string; + branches_url: string; + collaborators_url: string; + comments_url: string; + commits_url: string; + compare_url: string; + contents_url: string; + contributors_url: string; + deployments_url: string; + downloads_url: string; + events_url: string; + forks_url: string; + git_commits_url: string; + git_refs_url: string; + git_tags_url: string; + git_url: string; + issue_comment_url: string; + issue_events_url: string; + issues_url: string; + keys_url: string; + labels_url: string; + languages_url: string; + merges_url: string; + milestones_url: string; + notifications_url: string; + pulls_url: string; + releases_url: string; + ssh_url: string; + stargazers_url: string; + statuses_url: string; + subscribers_url: string; + subscription_url: string; + tags_url: string; + teams_url: string; + trees_url: string; + clone_url: string; + mirror_url: string; + hooks_url: string; + svn_url: string; + homepage: string; + language: unknown; + forks_count: number; + stargazers_count: number; + watchers_count: number; + size: number; + default_branch: string; + open_issues_count: number; + topics: Array; + has_issues: boolean; + has_projects: boolean; + has_wiki: boolean; + has_pages: boolean; + has_downloads: boolean; + has_discussions: boolean; + archived: boolean; + disabled: boolean; + pushed_at: string; + created_at: string; + updated_at: string; + permissions: { + admin: boolean; + push: boolean; + pull: boolean; + }; + allow_rebase_merge: boolean; + temp_clone_token: string; + allow_squash_merge: boolean; + allow_merge_commit: boolean; + forks: number; + open_issues: number; + license: { + key: string; + name: string; + url: string; + spdx_id: string; + node_id: string; + }; + watchers: number; + }; + }; + _links: { + self: { + href: string; + }; + html: { + href: string; + }; + issue: { + href: string; + }; + comments: { + href: string; + }; + review_comments: { + href: string; + }; + review_comment: { + href: string; + }; + commits: { + href: string; + }; + statuses: { + href: string; + }; + }; + author_association: string; + auto_merge: unknown; + draft: boolean; + merged: boolean; + mergeable: boolean; + rebaseable: boolean; + mergeable_state: string; + merged_by: { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; + }; + comments: number; + review_comments: number; + maintainer_can_modify: boolean; + commits: number; + additions: number; + deletions: number; + changed_files: number; +} + +interface PullRequestCreated { + action: "opened"; + number: number; + pull_request: PullRequest; +} + +type Ping = Record; + +type GithubPayload = IssueCreated | PullRequestCreated | Ping; + +export type { IssueCreated, PullRequestCreated, Ping, GithubPayload }; diff --git a/src/modules/processGitHubEvent.ts b/src/modules/processGitHubEvent.ts new file mode 100644 index 0000000..d9d666f --- /dev/null +++ b/src/modules/processGitHubEvent.ts @@ -0,0 +1,43 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import type { IssueCreated, + PullRequestCreated, + GithubPayload } from "../interfaces/github.js"; +import type { FastifyRequest, FastifyReply } from "fastify"; + +const isIssue = (body: GithubPayload): body is IssueCreated => { + return "issue" in body; +}; + +const isPull = (body: GithubPayload): body is PullRequestCreated => { + return "pull_request" in body; +}; + +/** + * Handles a payload from a GitHub webhook. + * @param request - The Fastify request payload. + * @param response - The Fastify reply class. + */ +export const processGithubEvent = async( + // eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify standard. + request: FastifyRequest<{ Body: GithubPayload }>, + response: FastifyReply, +): Promise => { + const event = request.headers["x-github-event"]; + if (typeof event !== "string") { + await response.status(400). + send({ message: "Invalid GitHub event header." }); + return; + } + if (event === "ping") { + await response.status(200).send({ message: "Pong!" }); + return; + } + const { action: _action } = request.body; + isIssue(request.body); + isPull(request.body); +}; diff --git a/src/server/serve.ts b/src/server/serve.ts index 826d7e0..36ea927 100644 --- a/src/server/serve.ts +++ b/src/server/serve.ts @@ -5,7 +5,9 @@ */ import fastify from "fastify"; +import { processGithubEvent } from "../modules/processGitHubEvent.js"; import { logger } from "../utils/logger.js"; +import type { GithubPayload } from "../interfaces/github.js"; const html = ` @@ -62,6 +64,12 @@ export const instantiateServer = (): void => { response.send(html); }); + server. + // eslint-disable-next-line @typescript-eslint/naming-convention -- Fastify standard. + post<{ Body: GithubPayload }>("/github", async(request, response) => { + await processGithubEvent(request, response); + }); + server.listen({ port: 7044 }, (error) => { if (error) { void logger.error("instantiate server", error);