Skip to content

Commit

Permalink
Merge pull request #71 from gentlementlegen/feat/help-command-manifest
Browse files Browse the repository at this point in the history
feat: help command manifest
  • Loading branch information
gentlementlegen authored Jul 17, 2024
2 parents a790b9b + 8b27baa commit 056989f
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/bun-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ jobs:
- name: Build & Run test suite
run: |
bun i
bun test --coverage
bun jest:test
Binary file modified bun.lockb
100755 → 100644
Binary file not shown.
11 changes: 11 additions & 0 deletions jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"preset": "ts-jest",
"testEnvironment": "node",
"roots": ["./tests"],
"coveragePathIgnorePatterns": ["node_modules", "mocks"],
"collectCoverage": true,
"coverageReporters": ["json", "lcov", "text", "clover", "json-summary"],
"reporters": ["default", "jest-junit", "jest-md-dashboard"],
"coverageDirectory": "coverage",
"setupFiles": ["dotenv/config"]
}
41 changes: 22 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"proxy": "tsx src/proxy.ts",
"knip": "knip --config .github/knip.ts",
"knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts",
"test": "bun test",
"jest:test": "jest --coverage",
"plugin:hello-world": "tsx tests/__mocks__/hello-world-plugin.ts",
"setup-kv": "bun --env-file=.dev.vars deploy/setup-kv-namespace.ts"
},
Expand All @@ -37,15 +37,16 @@
"dependencies": {
"@octokit/auth-app": "7.1.0",
"@octokit/core": "6.1.2",
"@octokit/plugin-paginate-rest": "11.3.0",
"@octokit/plugin-rest-endpoint-methods": "13.2.1",
"@octokit/plugin-paginate-rest": "11.3.3",
"@octokit/plugin-rest-endpoint-methods": "13.2.4",
"@octokit/plugin-retry": "7.1.1",
"@octokit/plugin-throttling": "9.3.0",
"@octokit/plugin-throttling": "9.3.1",
"@octokit/types": "13.5.0",
"@octokit/webhooks": "13.2.7",
"@octokit/webhooks": "13.2.8",
"@octokit/webhooks-types": "7.5.1",
"@sinclair/typebox": "0.32.33",
"@sinclair/typebox": "0.32.34",
"dotenv": "16.4.5",
"jest": "29.7.0",
"smee-client": "2.0.1",
"typebox-validators": "0.3.5",
"yaml": "2.4.5"
Expand All @@ -57,29 +58,32 @@
"@cspell/dict-node": "5.0.1",
"@cspell/dict-software-terms": "3.4.6",
"@cspell/dict-typescript": "3.1.5",
"@eslint/js": "9.5.0",
"@eslint/js": "9.7.0",
"@jest/globals": "29.7.0",
"@mswjs/data": "0.16.1",
"@mswjs/http-middleware": "0.10.1",
"@types/bun": "1.1.5",
"@types/node": "20.14.6",
"@types/node": "20.14.10",
"cspell": "8.9.0",
"esbuild": "0.21.5",
"eslint": "9.5.0",
"esbuild": "0.23.0",
"eslint": "9.7.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-check-file": "2.8.0",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-sonarjs": "1.0.3",
"husky": "9.0.11",
"knip": "5.22.0",
"jest-junit": "16.0.0",
"jest-md-dashboard": "0.8.0",
"knip": "5.26.0",
"lint-staged": "15.2.7",
"npm-run-all": "4.1.5",
"prettier": "3.3.2",
"prettier": "3.3.3",
"toml": "3.0.0",
"tomlify-j0.4": "3.0.0",
"tsx": "4.15.6",
"typescript": "5.4.5",
"typescript-eslint": "7.13.1",
"wrangler": "3.61.0"
"ts-jest": "29.2.2",
"tsx": "4.16.2",
"typescript": "5.5.3",
"typescript-eslint": "7.16.0",
"wrangler": "3.64.0"
},
"lint-staged": {
"*.ts": [
Expand All @@ -94,6 +98,5 @@
"extends": [
"@commitlint/config-conventional"
]
},
"packageManager": "bun@1.1.15"
}
}
86 changes: 86 additions & 0 deletions src/github/handlers/help-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { getConfig } from "../utils/config";
import { GithubPlugin, isGithubPlugin } from "../types/plugin-configuration";
import { GitHubContext } from "../github-context";
import { Manifest, manifestSchema, manifestValidator } from "../../types/manifest";
import { Value } from "@sinclair/typebox/value";
import { Buffer } from "node:buffer";

async function parseCommandsFromManifest(context: GitHubContext<"issue_comment.created">, plugin: string | GithubPlugin) {
const commands: string[] = [];
const manifest = await (isGithubPlugin(plugin) ? fetchActionManifest(context, plugin) : fetchWorkerManifest(plugin));
if (manifest) {
Value.Default(manifestSchema, manifest);
const errors = manifestValidator.testReturningErrors(manifest);
if (errors !== null) {
console.error(`Failed to load the manifest for ${JSON.stringify(plugin)}`);
for (const error of errors) {
console.error(error);
}
} else {
if (manifest?.commands) {
for (const [key, value] of Object.entries(manifest.commands)) {
commands.push(`| \`/${getContent(key)}\` | ${getContent(value.description)} | \`${getContent(value["ubiquity:example"])}\` |`);
}
}
}
}
return commands;
}

export async function postHelpCommand(context: GitHubContext<"issue_comment.created">) {
const comments = [
"### Available Commands\n\n",
"| Command | Description | Example |",
"|---|---|---|",
"| `/help` | List all available commands. | `/help` |",
];
const commands: string[] = [];
const configuration = await getConfig(context);
for (const pluginArray of Object.values(configuration.plugins)) {
for (const pluginElement of pluginArray) {
const { plugin } = pluginElement.uses[0];
commands.push(...(await parseCommandsFromManifest(context, plugin)));
}
}
await context.octokit.issues.createComment({
body: comments.concat(commands.sort()).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("|", "\\|") : "-";
}

async function fetchActionManifest(context: GitHubContext<"issue_comment.created">, { owner, repo }: GithubPlugin): Promise<Manifest | null> {
try {
const { data } = await context.octokit.repos.getContent({
owner,
repo,
path: "manifest.json",
});
if ("content" in data) {
const content = Buffer.from(data.content, "base64").toString();
return JSON.parse(content);
}
} catch (e) {
console.warn(`Could not find a manifest for ${owner}/${repo}: ${e}`);
}
return null;
}

async function fetchWorkerManifest(url: string): Promise<Manifest | null> {
const manifestUrl = `${url}/manifest.json`;
try {
const result = await fetch(manifestUrl);
return (await result.json()) as Manifest;
} catch (e) {
console.warn(`Could not find a manifest for ${manifestUrl}: ${e}`);
}
return null;
}
31 changes: 2 additions & 29 deletions src/github/handlers/issue-comment-created.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,9 @@
import { GitHubContext } from "../github-context";
import { getConfig } from "../utils/config";
import { postHelpCommand } from "./help-command";

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,
});
await postHelpCommand(context);
}
}

/**
* Ensures that passed content does not break MD display within the table.
*/
function getContent(content: string | undefined) {
return content ? content.replace("|", "\\|") : "-";
}
2 changes: 1 addition & 1 deletion src/github/types/plugin-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { githubWebhookEvents } from "./webhook-events";

const pluginNameRegex = new RegExp("^([0-9a-zA-Z-._]+)\\/([0-9a-zA-Z-._]+)(?::([0-9a-zA-Z-._]+))?(?:@([0-9a-zA-Z-._]+(?:\\/[0-9a-zA-Z-._]+)?))?$");

type GithubPlugin = {
export type GithubPlugin = {
owner: string;
repo: string;
workflowId: string;
Expand Down
17 changes: 17 additions & 0 deletions src/types/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type Static, Type as T } from "@sinclair/typebox";
import { StandardValidator } from "typebox-validators";

export const commandSchema = T.Object({
description: T.String({ minLength: 1 }),
"ubiquity:example": T.String({ minLength: 1 }),
});

export const manifestSchema = T.Object({
name: T.String({ minLength: 1 }),
description: T.String({ minLength: 1 }),
commands: T.Record(T.String(), commandSchema),
});

export const manifestValidator = new StandardValidator(manifestSchema);

export type Manifest = Static<typeof manifestSchema>;
5 changes: 3 additions & 2 deletions tests/configuration.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterAll, afterEach, beforeAll, describe, expect, it, mock } from "bun:test";
import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals";
import { config } from "dotenv";
import { server } from "./__mocks__/node";
import { WebhooksMocked } from "./__mocks__/webhooks";
Expand All @@ -8,8 +8,9 @@ import { GitHubEventHandler } from "../src/github/github-event-handler";

config({ path: ".dev.vars" });

void mock.module("@octokit/webhooks", () => ({
jest.mock("@octokit/webhooks", () => ({
Webhooks: WebhooksMocked,
emitterEventNames: [],
}));

const issueOpened = "issues.opened";
Expand Down
55 changes: 48 additions & 7 deletions tests/events.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
import { afterAll, afterEach, beforeAll, describe, expect, it, mock, spyOn } from "bun:test";
import { afterAll, afterEach, beforeAll, describe, expect, it, jest, beforeEach } from "@jest/globals";
import { config } from "dotenv";
import { http, HttpResponse } from "msw";
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", () => ({
jest.mock("@octokit/webhooks", () => ({
Webhooks: WebhooksMocked,
}));

Expand All @@ -24,13 +25,33 @@ afterAll(() => {
});

describe("Event related tests", () => {
beforeEach(() => {
server.use(
http.get("https://plugin-a.internal/manifest.json", () =>
HttpResponse.json({
commands: {
foo: {
command: "/foo",
description: "foo command",
example: "/foo bar",
},
bar: {
command: "/bar",
description: "bar command",
example: "/bar foo",
},
},
})
)
);
});
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");
const spy = jest.spyOn(issues, "createComment");
await issueCommentCreated({
id: "",
key: "issue_comment.created",
Expand All @@ -44,17 +65,37 @@ describe("Event related tests", () => {
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
- name: "Some Action plugin"
uses:
- id: plugin-B
plugin: ubiquibot/plugin-b
`,
};
},
},
},
repos: {
getContent() {
return {
data: {
content: btoa(
JSON.stringify({
commands: [
{
command: "/action",
description: "action",
example: "/action",
},
],
})
),
},
};
},
},
},
eventHandler: {} as GitHubEventHandler,
payload: {
Expand All @@ -74,7 +115,7 @@ describe("Event related tests", () => {
{
body:
"### Available Commands\n\n\n| Command | Description | Example |\n|---|---|---|\n| `/help` | List" +
" all available commands. | `/help` |\n| `/command` | Plugin A | `/command [foo \\| bar]` |",
" all available commands. | `/help` |\n| `/action` | action | `/action` |\n| `/bar` | bar command | `/bar foo` |\n| `/foo` | foo command | `/foo bar` |",
issue_number: 1,
owner: "ubiquity",
repo: "ubiquibot-kernel",
Expand Down
Loading

0 comments on commit 056989f

Please sign in to comment.