Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/development' into sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
whilefoo committed Jun 20, 2024
2 parents b42f9b6 + 3249656 commit 7e51e56
Show file tree
Hide file tree
Showing 21 changed files with 607 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log"],
"useGitignore": true,
"language": "en",
"words": ["dataurl", "devpool", "fkey", "outdir", "servedir", "supabase", "typebox", "ubiquibot", "smee"],
"words": ["dataurl", "devpool", "fkey", "mswjs", "outdir", "servedir", "supabase", "typebox", "ubiquibot", "smee", "tomlify"],
"dictionaries": ["typescript", "node", "software-terms"],
"import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"],
"ignoreRegExpList": ["[0-9a-fA-F]{6}"]
Expand Down
2 changes: 1 addition & 1 deletion .github/knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const config: KnipConfig = {
project: ["src/**/*.ts"],
ignoreBinaries: ["build", "i", "format:cspell", "awk", "lsof"],
ignoreExportsUsedInFile: true,
ignoreDependencies: ["@mswjs/data", "esbuild", "eslint-config-prettier", "eslint-plugin-prettier"],
ignoreDependencies: ["@mswjs/data", "esbuild", "eslint-config-prettier", "eslint-plugin-prettier", "msw"],
};

export default config;
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ jobs:

- uses: oven-sh/setup-bun@v1

- name: Run setup script
run: |
bun install
bun setup-kv
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

- uses: cloudflare/wrangler-action@v3
with:
wranglerVersion: '3.57.0'
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install bun
run: npm i -g bun
- uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Run typecheck
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,40 @@ const output: PluginOutput = {
};
```
## Plugin Quick Start
The kernel supports 2 types of plugins:
1. Github actions ([wiki](https://github.com/ubiquity/ubiquibot-kernel/wiki/How-it-works))
2. Cloudflare Workers (which are simple backend servers with a single API route)
How to run a "hello-world" plugin the Cloudflare way:
1. Run `bun dev` to spin up the kernel
2. Run `bun plugin:hello-world` to spin up a local server for the "hello-world" plugin
3. Update the bot's config file in the repository where you use the bot (`OWNER/REPOSITORY/.github/.ubiquibot-config.yml`):
```
plugins:
'issue_comment.created':
- name: "hello-world-plugin name"
description: "hello-world-plugin description"
command: "/hello"
example: "/hello example"
skipBotEvents: true
uses:
# hello-world-plugin
- plugin: http://127.0.0.1:9090
type: github
with:
response: world
```
4. Post a `/hello` comment in any issue
5. The bot should respond with the `world` message ([example](https://github.com/rndquu-org/test-repo/issues/54#issuecomment-2149313139))
How it works:
1. When you post the `/hello` command the kernel receives the `issue_comment.created` event
2. The kernel matches the `/hello` command to the plugin that should be executed (i.e. the API method that should be called)
3. The kernel passes github event payload, bot's access token and plugin settings (from `.ubiquibot-config.yml`) to the plugin endpoint
4. The plugin performs all of the required actions and returns the result
## Testing
### Jest
Expand Down
Binary file modified bun.lockb
Binary file not shown.
104 changes: 104 additions & 0 deletions deploy/setup_kv_namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* The purpose of the script is to ensure that the KV for the worker is properly set on deployment.
* There is currently a bug that makes the environment reset on each deploy, because of a problem with Wrangler not
* parsing the TOML configuration properly. See https://github.com/cloudflare/workers-sdk/issues/5634
* It seems to only work when the values are set at the root of the TOML, not withing the environments.
* This scripts takes out the Production values for kv_namespaces and rewrites them at the root of the TOML file.
*/

import { execSync } from "child_process";
import * as fs from "fs";
import * as toml from "toml";
// @ts-expect-error No typings exist for this package
import * as tomlify from "tomlify-j0.4";

const tomlFilePath = "./wrangler.toml";
const wranglerToml: WranglerConfiguration = toml.parse(fs.readFileSync(tomlFilePath, "utf-8"));

const NAMESPACE_TITLE = "kv";
const NAMESPACE_TITLE_WITH_PREFIX = `${wranglerToml.name}-${NAMESPACE_TITLE}`;
const BINDING_NAME = "PLUGIN_CHAIN_STATE";

interface Namespace {
id: string;
title: string;
}

interface WranglerConfiguration {
name: string;
env: {
production: {
kv_namespaces?: {
id: string;
binding: string;
}[];
};
dev: {
kv_namespaces?: {
id: string;
binding: string;
}[];
};
};
kv_namespaces: {
id: string;
binding: string;
}[];
}

function updateWranglerToml(namespaceId: string) {
// Ensure kv_namespaces array exists
if (!wranglerToml.kv_namespaces) {
wranglerToml.kv_namespaces = [];
}
if (wranglerToml.env.production.kv_namespaces) {
wranglerToml.kv_namespaces = wranglerToml.env.production.kv_namespaces;
delete wranglerToml.env.production.kv_namespaces;
}
if (wranglerToml.env.dev.kv_namespaces) {
delete wranglerToml.env.dev.kv_namespaces;
}

const existingNamespace = wranglerToml.kv_namespaces.find((o) => o.binding === BINDING_NAME);
if (existingNamespace) {
existingNamespace.id = namespaceId;
} else {
wranglerToml.kv_namespaces.push({
binding: BINDING_NAME,
id: namespaceId,
});
}

fs.writeFileSync(tomlFilePath, tomlify.toToml(wranglerToml, { space: 1 }));
}

async function main() {
// Check if the namespace exists or create a new one
let namespaceId: string;
try {
const res = execSync(`wrangler kv:namespace create ${NAMESPACE_TITLE}`).toString();
const newId = res.match(/id = \s*"([^"]+)"/)?.[1];
if (!newId) {
throw new Error(`The new ID could not be found.`);
}
namespaceId = newId;
console.log(`Namespace created with ID: ${namespaceId}`);
} catch (error) {
const listOutput = JSON.parse(execSync(`wrangler kv:namespace list`).toString()) as Namespace[];
const existingNamespace = listOutput.find((o) => o.title === NAMESPACE_TITLE_WITH_PREFIX);
if (!existingNamespace) {
throw new Error(`Error creating namespace: ${error}`);
}
namespaceId = existingNamespace.id;
console.log(`Namespace ${NAMESPACE_TITLE_WITH_PREFIX} already exists with ID: ${namespaceId}`);
}

updateWranglerToml(namespaceId);
}

main()
.then(() => console.log("Successfully bound namespace."))
.catch((e) => {
console.error("Error checking or creating namespace:", e);
process.exit(1);
});
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"proxy": "tsx src/proxy.ts",
"knip": "knip --config .github/knip.ts",
"knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts",
"test": "jest test"
"test": "jest test",
"plugin:hello-world": "tsx tests/__mocks__/hello-world-plugin.ts",
"setup-kv": "bun --env-file=.dev.vars deploy/setup_kv_namespace.ts"
},
"keywords": [
"typescript",
Expand All @@ -46,6 +48,7 @@
"dotenv": "^16.4.4",
"hono": "^4.4.7",
"smee-client": "^2.0.0",
"typebox-validators": "0.3.5",
"yaml": "^2.4.1"
},
"devDependencies": {
Expand All @@ -61,6 +64,8 @@
"@cspell/dict-software-terms": "^3.3.18",
"@cspell/dict-typescript": "^3.1.2",
"@mswjs/data": "0.16.1",
"@mswjs/http-middleware": "^0.10.1",
"@types/bun": "1.1.3",
"@types/node": "^20.11.19",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
Expand All @@ -75,9 +80,11 @@
"lint-staged": "^15.2.2",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
"toml": "3.0.0",
"tomlify-j0.4": "3.0.0",
"tsx": "^4.7.1",
"typescript": "^5.3.3",
"wrangler": "^3.58.0"
"wrangler": "3.58.0"
},
"lint-staged": {
"*.ts": [
Expand Down
8 changes: 5 additions & 3 deletions src/github/github-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as Webhook
import { customOctokit } from "./github-client";
import { GitHubEventHandler } from "./github-event-handler";

export class GitHubContext<T extends WebhookEventName = WebhookEventName> {
export class GitHubContext<TSupportedEvents extends WebhookEventName = WebhookEventName> {
public key: WebhookEventName;
public name: WebhookEventName;
public id: string;
public payload: WebhookEvent<T>["payload"];
public payload: {
[K in TSupportedEvents]: K extends WebhookEventName ? WebhookEvent<K> : never;
}[TSupportedEvents]["payload"];
public octokit: InstanceType<typeof customOctokit>;
public eventHandler: InstanceType<typeof GitHubEventHandler>;

constructor(eventHandler: InstanceType<typeof GitHubEventHandler>, event: WebhookEvent<T>, octokit: InstanceType<typeof customOctokit>) {
constructor(eventHandler: InstanceType<typeof GitHubEventHandler>, event: WebhookEvent<TSupportedEvents>, octokit: InstanceType<typeof customOctokit>) {
this.eventHandler = eventHandler;
this.name = event.name;
this.id = event.id;
Expand Down
2 changes: 1 addition & 1 deletion src/github/github-event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class GitHubEventHandler {

return await crypto.subtle.importKey(
"pkcs8",
binaryDer.buffer,
binaryDer.buffer as ArrayBuffer,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
Expand Down
2 changes: 2 additions & 0 deletions src/github/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
}

Expand Down
36 changes: 36 additions & 0 deletions src/github/handlers/issue-comment-created.ts
Original file line number Diff line number Diff line change
@@ -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("|", "\\|") : "-";
}
4 changes: 3 additions & 1 deletion src/github/types/plugin-configuration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Type as T } from "@sinclair/typebox";
import { StaticDecode } from "@sinclair/typebox";
import { StandardValidator } from "typebox-validators";
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-._]+)?))?$");
Expand Down Expand Up @@ -49,7 +50,6 @@ const pluginChainSchema = T.Array(
T.Object({
id: T.Optional(T.String()),
plugin: githubPluginType(),
type: T.Union([T.Literal("github")], { default: "github" }),
with: T.Record(T.String(), T.Unknown(), { default: {} }),
}),
{ minItems: 1, default: [] }
Expand All @@ -73,4 +73,6 @@ export const configSchema = T.Object({
plugins: T.Record(T.Enum(githubWebhookEvents), handlerSchema, { default: {} }),
});

export const configSchemaValidator = new StandardValidator(configSchema);

export type PluginConfiguration = StaticDecode<typeof configSchema>;
Loading

0 comments on commit 7e51e56

Please sign in to comment.