diff --git a/src/github/handlers/index.ts b/src/github/handlers/index.ts index ac56ab7..522d6ae 100644 --- a/src/github/handlers/index.ts +++ b/src/github/handlers/index.ts @@ -2,6 +2,7 @@ import { EmitterWebhookEvent } from "@octokit/webhooks"; import { GitHubContext } from "../github-context"; import { GitHubEventHandler } from "../github-event-handler"; import { getConfig } from "../utils/config"; +import issueCommentCreated from "./issue-comment-created"; import { repositoryDispatch } from "./repository-dispatch"; import { dispatchWorker, dispatchWorkflow, getDefaultBranch } from "../utils/workflow-dispatch"; import { PluginInput } from "../types/plugin"; @@ -19,6 +20,7 @@ function tryCatchWrapper(fn: (event: EmitterWebhookEvent) => unknown) { export function bindHandlers(eventHandler: GitHubEventHandler) { eventHandler.on("repository_dispatch", repositoryDispatch); + eventHandler.on("issue_comment.created", issueCommentCreated); eventHandler.onAny(tryCatchWrapper((event) => handleEvent(event, eventHandler))); // onAny should also receive GithubContext but the types in octokit/webhooks are weird } diff --git a/src/github/handlers/issue-comment-created.ts b/src/github/handlers/issue-comment-created.ts new file mode 100644 index 0000000..45d4a52 --- /dev/null +++ b/src/github/handlers/issue-comment-created.ts @@ -0,0 +1,36 @@ +import { GitHubContext } from "../github-context"; +import { getConfig } from "../utils/config"; + +export default async function issueCommentCreated(context: GitHubContext<"issue_comment.created">) { + const body = context.payload.comment.body.trim(); + if (/^\/help$/.test(body)) { + const comments = [ + "### Available Commands\n\n", + "| Command | Description | Example |", + "|---|---|---|", + "| `/help` | List all available commands. | `/help` |", + ]; + const configuration = await getConfig(context); + for (const pluginArray of Object.values(configuration.plugins)) { + for (const plugin of pluginArray) { + // Only show plugins that have commands available for the user + if (plugin.command) { + comments.push(`| \`${getContent(plugin.command)}\` | ${getContent(plugin.description)} | \`${getContent(plugin.example)}\` |`); + } + } + } + await context.octokit.issues.createComment({ + body: comments.join("\n"), + issue_number: context.payload.issue.number, + owner: context.payload.repository.owner.login, + repo: context.payload.repository.name, + }); + } +} + +/** + * Ensures that passed content does not break MD display within the table. + */ +function getContent(content: string | undefined) { + return content ? content.replace("|", "\\|") : "-"; +} diff --git a/tests/events.test.ts b/tests/events.test.ts new file mode 100644 index 0000000..5180559 --- /dev/null +++ b/tests/events.test.ts @@ -0,0 +1,85 @@ +import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods"; +import { afterAll, afterEach, beforeAll, describe, expect, it, mock, spyOn } from "bun:test"; +import { config } from "dotenv"; +import { GitHubContext } from "../src/github/github-context"; +import { GitHubEventHandler } from "../src/github/github-event-handler"; +import issueCommentCreated from "../src/github/handlers/issue-comment-created"; +import { server } from "./__mocks__/node"; +import { WebhooksMocked } from "./__mocks__/webhooks"; + +void mock.module("@octokit/webhooks", () => ({ + Webhooks: WebhooksMocked, +})); + +config({ path: ".dev.vars" }); + +beforeAll(() => { + server.listen(); +}); +afterEach(() => { + server.resetHandlers(); +}); +afterAll(() => { + server.close(); +}); + +describe("Event related tests", () => { + it("Should post the help menu when /help command is invoked", async () => { + const issues = { + createComment(params?: RestEndpointMethodTypes["issues"]["createComment"]["parameters"]) { + return params; + }, + }; + const spy = spyOn(issues, "createComment"); + await issueCommentCreated({ + id: "", + key: "issue_comment.created", + octokit: { + issues, + rest: { + repos: { + getContent() { + return { + data: ` + plugins: + issue_comment.created: + - name: "Run on comment created" + description: "Plugin A" + example: /command [foo | bar] + command: /command + uses: + - id: plugin-A + plugin: https://plugin-a.internal + `, + }; + }, + }, + }, + }, + eventHandler: {} as GitHubEventHandler, + payload: { + repository: { + owner: { login: "ubiquity" }, + name: "ubiquibot-kernel", + }, + issue: { number: 1 }, + comment: { + body: "/help", + }, + } as unknown as GitHubContext<"issue_comment.created">["payload"], + } as unknown as GitHubContext); + expect(spy).toBeCalledTimes(1); + expect(spy.mock.calls).toEqual([ + [ + { + body: + "### Available Commands\n\n\n| Command | Description | Example |\n|---|---|---|\n| `/help` | List" + + " all available commands. | `/help` |\n| `/command` | Plugin A | `/command [foo \\| bar]` |", + issue_number: 1, + owner: "ubiquity", + repo: "ubiquibot-kernel", + }, + ], + ]); + }); +});