Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: help command manifest #71

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
82e2c65
chore: help for plugins (WIP)
gentlementlegen Jul 9, 2024
f7f6421
chore: display worker manifest
gentlementlegen Jul 10, 2024
a259fd5
chore: display worker manifest
gentlementlegen Jul 10, 2024
6fe69c2
chore: display worker manifest
gentlementlegen Jul 10, 2024
687b190
chore: display worker manifest
gentlementlegen Jul 10, 2024
c0a6cff
chore: simplified logic for help command
gentlementlegen Jul 10, 2024
798f58b
chore: sorting commands
gentlementlegen Jul 10, 2024
83e6e14
chore: sorting commands
gentlementlegen Jul 10, 2024
4db9e0a
chore: split help command code
gentlementlegen Jul 10, 2024
bfa8fe8
fix: added tests related to help command
gentlementlegen Jul 10, 2024
7154e4a
chore: split help command code
gentlementlegen Jul 10, 2024
c21454a
Merge branch 'refs/heads/meniole-main' into feat/help-command-manifest
gentlementlegen Jul 10, 2024
c53415c
chore: upgraded bun
gentlementlegen Jul 11, 2024
b9e78a4
chore: split logic for help
gentlementlegen Jul 11, 2024
10a929d
chore: manifest
gentlementlegen Jul 11, 2024
2b810fe
chore: manifest
gentlementlegen Jul 11, 2024
812f865
chore: removed logs
gentlementlegen Jul 11, 2024
b746886
chore: renamed example key
gentlementlegen Jul 11, 2024
e90c1f6
chore: changed tests to jest
gentlementlegen Jul 15, 2024
eecfc5b
chore: changed tests to jest
gentlementlegen Jul 15, 2024
003c712
chore: changed tests to jest
gentlementlegen Jul 15, 2024
37cec1c
chore: changed tests to jest
gentlementlegen Jul 15, 2024
a6c87c0
chore: updates packages
gentlementlegen Jul 15, 2024
8b27baa
fix: enabled nodejs compat
gentlementlegen Jul 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
rndquu marked this conversation as resolved.
Show resolved Hide resolved
}
}
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 }),
gentlementlegen marked this conversation as resolved.
Show resolved Hide resolved
});

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
Loading