From c7b8fd91811831d7bbe6f9549e798e819cc2680f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=A2=E3=83=AC=E3=82=AF=E3=82=B5=E3=83=B3=E3=83=80?= =?UTF-8?q?=E3=83=BC=2Eeth?= <4975670+0x4007@users.noreply.github.com> Date: Tue, 22 Oct 2024 18:57:29 +0900 Subject: [PATCH 1/8] Revert "PR #108" --- revert-pr-108.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 revert-pr-108.txt diff --git a/revert-pr-108.txt b/revert-pr-108.txt new file mode 100644 index 0000000..72e6611 --- /dev/null +++ b/revert-pr-108.txt @@ -0,0 +1 @@ +This file was created to revert PR #108 \ No newline at end of file From 057ac26fcc0cbf7386ccfd468984c595f0ef4324 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Thu, 24 Oct 2024 14:09:45 +0900 Subject: [PATCH 2/8] refactor: rename and update CloudflareKv to KvStore Abstracted CloudflareKv to KvStore interface and added EmptyStore class. --- src/github/github-event-handler.ts | 6 ++-- src/github/utils/cloudflare-kv.ts | 15 -------- src/github/utils/kv-store.ts | 55 ++++++++++++++++++++++++++++++ src/worker.ts | 4 +-- tests/dispatch.test.ts | 6 +++- 5 files changed, 65 insertions(+), 21 deletions(-) delete mode 100644 src/github/utils/cloudflare-kv.ts create mode 100644 src/github/utils/kv-store.ts diff --git a/src/github/github-event-handler.ts b/src/github/github-event-handler.ts index 0b00a96..b807b55 100644 --- a/src/github/github-event-handler.ts +++ b/src/github/github-event-handler.ts @@ -2,7 +2,7 @@ import { EmitterWebhookEvent, Webhooks } from "@octokit/webhooks"; import { customOctokit } from "./github-client"; import { GitHubContext, SimplifiedContext } from "./github-context"; import { createAppAuth } from "@octokit/auth-app"; -import { CloudflareKv } from "./utils/cloudflare-kv"; +import { KvStore } from "./utils/kv-store"; import { PluginChainState } from "./types/plugin"; export type Options = { @@ -10,7 +10,7 @@ export type Options = { webhookSecret: string; appId: string | number; privateKey: string; - pluginChainState: CloudflareKv; + pluginChainState: KvStore; }; export class GitHubEventHandler { @@ -18,7 +18,7 @@ export class GitHubEventHandler { public on: Webhooks["on"]; public onAny: Webhooks["onAny"]; public onError: Webhooks["onError"]; - public pluginChainState: CloudflareKv; + public pluginChainState: KvStore; readonly environment: "production" | "development"; private readonly _webhookSecret: string; diff --git a/src/github/utils/cloudflare-kv.ts b/src/github/utils/cloudflare-kv.ts deleted file mode 100644 index f5407a3..0000000 --- a/src/github/utils/cloudflare-kv.ts +++ /dev/null @@ -1,15 +0,0 @@ -export class CloudflareKv { - private _kv: KVNamespace; - - constructor(kv: KVNamespace) { - this._kv = kv; - } - - get(id: string): Promise { - return this._kv.get(id, "json"); - } - - put(id: string, state: T): Promise { - return this._kv.put(id, JSON.stringify(state)); - } -} diff --git a/src/github/utils/kv-store.ts b/src/github/utils/kv-store.ts new file mode 100644 index 0000000..7b0a6cb --- /dev/null +++ b/src/github/utils/kv-store.ts @@ -0,0 +1,55 @@ +/** + * KvStore is an interface representing a simple key-value store. + * + * @template T - The type of the value to be stored and retrieved. + */ +export interface KvStore { + get(id: string): Promise; + put(id: string, state: T): Promise; +} + +/** + * CloudflareKv is a class that provides an interface to interact with + * Cloudflare KV (Key-Value) storage. + * + * It implements the KvStore interface to handle generic types. + * + * @template T - The type of the values being stored. + */ +export class CloudflareKv implements KvStore { + private _kv: KVNamespace; + + constructor(kv: KVNamespace) { + this._kv = kv; + } + + get(id: string): Promise { + return this._kv.get(id, "json"); + } + + put(id: string, state: T): Promise { + return this._kv.put(id, JSON.stringify(state)); + } +} + +/** + * A class that implements the KvStore interface, representing an empty key-value store. + * All get operations return null and put operations do nothing, but log the action. + * + * @template T - The type of values to be stored. + */ +export class EmptyStore implements KvStore { + constructor(kv: KVNamespace) { + console.log(`Creating empty kv`, kv); + } + + get(id: string): Promise { + console.log(`get KV ${id}`); + return Promise.resolve(null); + } + + put(id: string, state: T): Promise { + console.log(`put KV ${id} ${state}`); + return Promise.resolve(); + } +} diff --git a/src/worker.ts b/src/worker.ts index 7314600..f186bcc 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -3,7 +3,7 @@ import { Value } from "@sinclair/typebox/value"; import { GitHubEventHandler } from "./github/github-event-handler"; import { bindHandlers } from "./github/handlers"; import { Env, envSchema } from "./github/types/env"; -import { CloudflareKv } from "./github/utils/cloudflare-kv"; +import { EmptyStore } from "./github/utils/kv-store"; import { WebhookEventName } from "@octokit/webhooks-types"; export default { @@ -18,7 +18,7 @@ export default { webhookSecret: env.APP_WEBHOOK_SECRET, appId: env.APP_ID, privateKey: env.APP_PRIVATE_KEY, - pluginChainState: new CloudflareKv(env.PLUGIN_CHAIN_STATE), + pluginChainState: new EmptyStore(env.PLUGIN_CHAIN_STATE), }); bindHandlers(eventHandler); await eventHandler.webhooks.verifyAndReceive({ id, name: eventName, payload: await request.text(), signature: signatureSha256 }); diff --git a/tests/dispatch.test.ts b/tests/dispatch.test.ts index afb2b9c..38de8fc 100644 --- a/tests/dispatch.test.ts +++ b/tests/dispatch.test.ts @@ -12,11 +12,15 @@ jest.mock("@octokit/auth-app", () => ({ createAppAuth: jest.fn(() => () => jest.fn(() => "1234")), })); -jest.mock("../src/github/utils/cloudflare-kv", () => ({ +jest.mock("../src/github/utils/kv-store", () => ({ CloudflareKv: jest.fn().mockImplementation(() => ({ get: jest.fn(), put: jest.fn(), })), + EmptyStore: jest.fn().mockImplementation(() => ({ + get: jest.fn(), + put: jest.fn(), + })), })); jest.mock("../src/github/types/plugin", () => { From 4ba813817bcb5cd69e3edc631145ea41f089e96a Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Thu, 24 Oct 2024 14:13:05 +0900 Subject: [PATCH 3/8] refactor: commented unused export --- src/github/utils/kv-store.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/github/utils/kv-store.ts b/src/github/utils/kv-store.ts index 7b0a6cb..acd115d 100644 --- a/src/github/utils/kv-store.ts +++ b/src/github/utils/kv-store.ts @@ -16,21 +16,21 @@ export interface KvStore { * * @template T - The type of the values being stored. */ -export class CloudflareKv implements KvStore { - private _kv: KVNamespace; - - constructor(kv: KVNamespace) { - this._kv = kv; - } - - get(id: string): Promise { - return this._kv.get(id, "json"); - } - - put(id: string, state: T): Promise { - return this._kv.put(id, JSON.stringify(state)); - } -} +// export class CloudflareKv implements KvStore { +// private _kv: KVNamespace; +// +// constructor(kv: KVNamespace) { +// this._kv = kv; +// } +// +// get(id: string): Promise { +// return this._kv.get(id, "json"); +// } +// +// put(id: string, state: T): Promise { +// return this._kv.put(id, JSON.stringify(state)); +// } +// } /** * A class that implements the KvStore interface, representing an empty key-value store. From 6037f76c1ec2bad7abf34b6971b477b1109439c9 Mon Sep 17 00:00:00 2001 From: Mentlegen <9807008+gentlementlegen@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:29:20 +0900 Subject: [PATCH 4/8] refactor: optimize plugin chain handling with Promise.all Utilize Promise.all to concurrently handle plugin chains and improve efficiency. --- src/github/handlers/index.ts | 94 +++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/src/github/handlers/index.ts b/src/github/handlers/index.ts index 3905d0e..21876a0 100644 --- a/src/github/handlers/index.ts +++ b/src/github/handlers/index.ts @@ -69,50 +69,56 @@ async function handleEvent(event: EmitterWebhookEvent, eventHandler: InstanceTyp return; } - for (const pluginChain of pluginChains) { - if (await shouldSkipPlugin(context, pluginChain)) { - continue; - } - - // invoke the first plugin in the chain - const { plugin, with: settings } = pluginChain.uses[0]; - const isGithubPluginObject = isGithubPlugin(plugin); - console.log(`Calling handler ${JSON.stringify(plugin)} for event ${event.name}`); - - const stateId = crypto.randomUUID(); - - const state = { - eventId: context.id, - eventName: context.key, - eventPayload: event.payload, - currentPlugin: 0, - pluginChain: pluginChain.uses, - outputs: new Array(pluginChain.uses.length), - inputs: new Array(pluginChain.uses.length), - }; - - const ref = isGithubPluginObject ? (plugin.ref ?? (await getDefaultBranch(context, plugin.owner, plugin.repo))) : plugin; - const token = await eventHandler.getToken(event.payload.installation.id); - const inputs = new PluginInput(context.eventHandler, stateId, context.key, event.payload, settings, token, ref); - - state.inputs[0] = inputs; - await eventHandler.pluginChainState.put(stateId, state); + await Promise.all( + pluginChains.map(async (pluginChain) => { + if (await shouldSkipPlugin(context, pluginChain)) { + return; + } + if (!("installation" in event.payload) || event.payload.installation?.id === undefined) { + console.log(`No installation found, cannot invoke plugin`, pluginChain); + return; + } - // We wrap the dispatch so a failing plugin doesn't break the whole execution - try { - if (!isGithubPluginObject) { - await dispatchWorker(plugin, await inputs.getWorkerInputs()); - } else { - await dispatchWorkflow(context, { - owner: plugin.owner, - repository: plugin.repo, - workflowId: plugin.workflowId, - ref: plugin.ref, - inputs: await inputs.getWorkflowInputs(), - }); + // invoke the first plugin in the chain + const { plugin, with: settings } = pluginChain.uses[0]; + const isGithubPluginObject = isGithubPlugin(plugin); + console.log(`Calling handler ${JSON.stringify(plugin)} for event ${event.name}`); + + const stateId = crypto.randomUUID(); + + const state = { + eventId: context.id, + eventName: context.key, + eventPayload: event.payload, + currentPlugin: 0, + pluginChain: pluginChain.uses, + outputs: new Array(pluginChain.uses.length), + inputs: new Array(pluginChain.uses.length), + }; + + const ref = isGithubPluginObject ? (plugin.ref ?? (await getDefaultBranch(context, plugin.owner, plugin.repo))) : plugin; + const token = await eventHandler.getToken(event.payload.installation.id); + const inputs = new PluginInput(context.eventHandler, stateId, context.key, event.payload, settings, token, ref); + + state.inputs[0] = inputs; + await eventHandler.pluginChainState.put(stateId, state); + + // We wrap the dispatch so a failing plugin doesn't break the whole execution + try { + if (!isGithubPluginObject) { + await dispatchWorker(plugin, await inputs.getWorkerInputs()); + } else { + await dispatchWorkflow(context, { + owner: plugin.owner, + repository: plugin.repo, + workflowId: plugin.workflowId, + ref: plugin.ref, + inputs: await inputs.getWorkflowInputs(), + }); + } + } catch (e) { + console.error(`An error occurred while processing the plugin chain, will skip plugin ${JSON.stringify(plugin)}`, e); } - } catch (e) { - console.error(`An error occurred while processing the plugin chain, will skip plugin ${JSON.stringify(plugin)}`, e); - } - } + }) + ); } From 6941ff841db7f9a616f9e92e86f4a0dc120ea46c Mon Sep 17 00:00:00 2001 From: whilefoo Date: Thu, 24 Oct 2024 20:33:11 +0200 Subject: [PATCH 5/8] feat: fix signature and add plugin github token --- bun.lockb | Bin 434509 -> 434517 bytes src/sdk/actions.ts | 15 ++-- src/sdk/server.ts | 30 +++++--- src/sdk/signature.ts | 22 +++++- tests/sdk.test.ts | 171 ++++++++++++++++++++++++++----------------- 5 files changed, 152 insertions(+), 86 deletions(-) diff --git a/bun.lockb b/bun.lockb index 26de14f8e4e60fe8eabb621dbc082e9404b01495..2b4d96d40a2aea23fe4f8b7a6d4daf2ecceaeaad 100755 GIT binary patch delta 11681 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 CBVAem diff --git a/src/sdk/actions.ts b/src/sdk/actions.ts index 577d71d..b8af3b8 100644 --- a/src/sdk/actions.ts +++ b/src/sdk/actions.ts @@ -43,15 +43,18 @@ export async function createActionsPlugin( handler: (context: Context) => Promise | undefined>, manifest: Manifest, @@ -43,18 +54,17 @@ export async function createPlugin = { - eventName: payload.eventName, - payload: payload.eventPayload, - octokit: new customOctokit({ auth: payload.authToken }), + eventName: inputs.eventName as TSupportedEvents, + payload: inputs.eventPayload, + octokit: new customOctokit({ auth: inputs.authToken }), config: config, env: env, logger: new Logs(pluginOptions.logLevel), @@ -75,7 +85,7 @@ export async function createPlugin c.charCodeAt(0)); @@ -15,7 +33,7 @@ export async function verifySignature(publicKeyPem: string, payload: unknown, si ); const signatureArray = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)); - const dataArray = new TextEncoder().encode(JSON.stringify(payload)); + const dataArray = new TextEncoder().encode(JSON.stringify(inputsOrdered)); return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", publicKey, signatureArray, dataArray); } catch (error) { diff --git a/tests/sdk.test.ts b/tests/sdk.test.ts index 723e6dc..5f61c04 100644 --- a/tests/sdk.test.ts +++ b/tests/sdk.test.ts @@ -6,6 +6,10 @@ import * as crypto from "crypto"; import { createPlugin } from "../src/sdk/server"; import { Hono } from "hono"; import { Context } from "../src/sdk/context"; +import { GitHubEventHandler } from "../src/github/github-event-handler"; +import { CloudflareKv } from "../src/github/utils/cloudflare-kv"; +import { PluginChainState, PluginInput } from "../src/github/types/plugin"; +import { EmitterWebhookEventName } from "@octokit/webhooks"; const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { modulusLength: 2048, @@ -19,13 +23,26 @@ const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { }, }); +const issueCommentedEvent = { + eventName: issueCommented.eventName as EmitterWebhookEventName, + eventPayload: issueCommented.eventPayload, +}; + +const eventHandler = new GitHubEventHandler({ + environment: "production", + webhookSecret: "test", + appId: "1", + privateKey: privateKey, + pluginChainState: undefined as unknown as CloudflareKv, +}); + let app: Hono; beforeAll(async () => { app = await createPlugin( async (context: Context<{ shouldFail: boolean }>) => { if (context.config.shouldFail) { - throw new Error("Failed"); + throw context.logger.error("test error"); } return { success: true, @@ -37,11 +54,13 @@ beforeAll(async () => { ); server.listen(); }); + afterEach(() => { server.resetHandlers(); jest.resetModules(); jest.restoreAllMocks(); }); + afterAll(() => server.close()); describe("SDK worker tests", () => { @@ -66,21 +85,13 @@ describe("SDK worker tests", () => { expect(res.status).toEqual(400); }); it("Should deny POST request with invalid signature", async () => { - const data = { - ...issueCommented, - stateId: "stateId", - authToken: process.env.GITHUB_TOKEN, - settings: { - shouldFail: false, - }, - ref: "", - }; - const signature = "invalid"; + const inputs = new PluginInput(eventHandler, "stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, { shouldFail: false }, "test", ""); + const res = await app.request("/", { headers: { "content-type": "application/json", }, - body: JSON.stringify({ ...data, signature }), + body: JSON.stringify({ ...(await inputs.getWorkerInputs()), signature: "invalid_signature" }), method: "POST", }); expect(res.status).toEqual(400); @@ -116,25 +127,13 @@ describe("SDK worker tests", () => { { kernelPublicKey: publicKey } ); - const data = { - ...issueCommented, - stateId: "stateId", - authToken: process.env.GITHUB_TOKEN, - settings: { - shouldFail: true, - }, - ref: "", - }; - const sign = crypto.createSign("SHA256"); - sign.update(JSON.stringify(data)); - sign.end(); - const signature = sign.sign(privateKey, "base64"); + const inputs = new PluginInput(eventHandler, "stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, { shouldFail: true }, "test", ""); const res = await app.request("/", { headers: { "content-type": "application/json", }, - body: JSON.stringify({ ...data, signature }), + body: JSON.stringify(await inputs.getWorkerInputs()), method: "POST", }); expect(res.status).toEqual(500); @@ -153,25 +152,13 @@ describe("SDK worker tests", () => { }); }); it("Should accept correct request", async () => { - const data = { - ...issueCommented, - stateId: "stateId", - authToken: "test", - settings: { - shouldFail: false, - }, - ref: "", - }; - const sign = crypto.createSign("SHA256"); - sign.update(JSON.stringify(data)); - sign.end(); - const signature = sign.sign(privateKey, "base64"); + const inputs = new PluginInput(eventHandler, "stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, { shouldFail: false }, "test", ""); const res = await app.request("/", { headers: { "content-type": "application/json", }, - body: JSON.stringify({ ...data, signature }), + body: JSON.stringify(await inputs.getWorkerInputs()), method: "POST", }); expect(res.status).toEqual(200); @@ -181,32 +168,20 @@ describe("SDK worker tests", () => { }); describe("SDK actions tests", () => { + process.env.PLUGIN_GITHUB_TOKEN = "token"; const repo = { owner: "ubiquity", repo: "ubiquity-os-kernel", }; it("Should accept correct request", async () => { - const inputs = { - stateId: "stateId", - eventName: issueCommented.eventName, - settings: "{}", - eventPayload: JSON.stringify(issueCommented.eventPayload), - authToken: "test", - ref: "", - }; - const sign = crypto.createSign("SHA256"); - sign.update(JSON.stringify(inputs)).end(); - const signature = sign.sign(privateKey, "base64"); - + const inputs = new PluginInput(eventHandler, "stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, {}, "test_token", ""); + const githubInputs = await inputs.getWorkflowInputs(); jest.mock("@actions/github", () => ({ context: { runId: "1", payload: { - inputs: { - ...inputs, - signature, - }, + inputs: githubInputs, }, repo: repo, }, @@ -247,8 +222,8 @@ describe("SDK actions tests", () => { expect(setOutput).toHaveBeenCalledWith("result", { event: issueCommented.eventName }); expect(createDispatchEvent).toHaveBeenCalledWith({ event_type: "return-data-to-ubiquity-os-kernel", - owner: "ubiquity", - repo: "ubiquity-os-kernel", + owner: repo.owner, + repo: repo.repo, client_payload: { state_id: "stateId", output: JSON.stringify({ event: issueCommented.eventName }), @@ -256,22 +231,16 @@ describe("SDK actions tests", () => { }); }); it("Should deny invalid signature", async () => { - const inputs = { - stateId: "stateId", - eventName: issueCommented.eventName, - settings: "{}", - eventPayload: JSON.stringify(issueCommented.eventPayload), - authToken: "test", - ref: "", - }; + const inputs = new PluginInput(eventHandler, "stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, {}, "test_token", ""); + const githubInputs = await inputs.getWorkflowInputs(); jest.mock("@actions/github", () => ({ context: { runId: "1", payload: { inputs: { - ...inputs, - signature: "invalid", + ...githubInputs, + signature: "invalid_signature", }, }, repo: repo, @@ -298,4 +267,70 @@ describe("SDK actions tests", () => { expect(setFailed).toHaveBeenCalledWith("Error: Invalid signature"); expect(setOutput).not.toHaveBeenCalled(); }); + it("Should accept inputs in different order", async () => { + const inputs = new PluginInput(eventHandler, "stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, {}, "test_token", ""); + const githubInputs = await inputs.getWorkflowInputs(); + + jest.mock("@actions/github", () => ({ + context: { + runId: "1", + payload: { + inputs: { + // different order + signature: githubInputs.signature, + eventName: githubInputs.eventName, + settings: githubInputs.settings, + ref: githubInputs.ref, + authToken: githubInputs.authToken, + stateId: githubInputs.stateId, + eventPayload: githubInputs.eventPayload, + }, + }, + repo: repo, + }, + })); + const setOutput = jest.fn(); + const setFailed = jest.fn(); + jest.mock("@actions/core", () => ({ + setOutput, + setFailed, + })); + const createDispatchEvent = jest.fn(); + jest.mock("../src/sdk/octokit", () => ({ + customOctokit: class MockOctokit { + constructor() { + return { + rest: { + repos: { + createDispatchEvent: createDispatchEvent, + }, + }, + }; + } + }, + })); + const { createActionsPlugin } = await import("../src/sdk/actions"); + + await createActionsPlugin( + async (context: Context) => { + return { + event: context.eventName, + }; + }, + { + kernelPublicKey: publicKey, + } + ); + expect(setFailed).not.toHaveBeenCalled(); + expect(setOutput).toHaveBeenCalledWith("result", { event: issueCommentedEvent.eventName }); + expect(createDispatchEvent).toHaveBeenCalledWith({ + event_type: "return-data-to-ubiquity-os-kernel", + owner: repo.owner, + repo: repo.repo, + client_payload: { + state_id: "stateId", + output: JSON.stringify({ event: issueCommentedEvent.eventName }), + }, + }); + }); }); From ff36f5665d7661a253f20ef5598d712bbc26ac23 Mon Sep 17 00:00:00 2001 From: whilefoo Date: Fri, 25 Oct 2024 15:55:34 +0200 Subject: [PATCH 6/8] feat: make createPlugin sync function --- src/sdk/server.ts | 2 +- tests/sdk.test.ts | 54 ++++++++++++++++++++++++----------------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/sdk/server.ts b/src/sdk/server.ts index ab3d97b..9c60e6e 100644 --- a/src/sdk/server.ts +++ b/src/sdk/server.ts @@ -30,7 +30,7 @@ const inputSchema = T.Object({ signature: T.String(), }); -export async function createPlugin( +export function createPlugin( handler: (context: Context) => Promise | undefined>, manifest: Manifest, options?: Options diff --git a/tests/sdk.test.ts b/tests/sdk.test.ts index 5f61c04..0ac8e06 100644 --- a/tests/sdk.test.ts +++ b/tests/sdk.test.ts @@ -4,7 +4,6 @@ import { expect, describe, beforeAll, afterAll, afterEach, it, jest } from "@jes import * as crypto from "crypto"; import { createPlugin } from "../src/sdk/server"; -import { Hono } from "hono"; import { Context } from "../src/sdk/context"; import { GitHubEventHandler } from "../src/github/github-event-handler"; import { CloudflareKv } from "../src/github/utils/cloudflare-kv"; @@ -28,6 +27,10 @@ const issueCommentedEvent = { eventPayload: issueCommented.eventPayload, }; +const sdkOctokitImportPath = "../src/sdk/octokit"; +const githubActionImportPath = "@actions/github"; +const githubCoreImportPath = "@actions/core"; + const eventHandler = new GitHubEventHandler({ environment: "production", webhookSecret: "test", @@ -36,22 +39,21 @@ const eventHandler = new GitHubEventHandler({ pluginChainState: undefined as unknown as CloudflareKv, }); -let app: Hono; +const app = createPlugin( + async (context: Context<{ shouldFail: boolean }>) => { + if (context.config.shouldFail) { + throw context.logger.error("test error"); + } + return { + success: true, + event: context.eventName, + }; + }, + { name: "test" }, + { kernelPublicKey: publicKey } +); beforeAll(async () => { - app = await createPlugin( - async (context: Context<{ shouldFail: boolean }>) => { - if (context.config.shouldFail) { - throw context.logger.error("test error"); - } - return { - success: true, - event: context.eventName, - }; - }, - { name: "test" }, - { kernelPublicKey: publicKey } - ); server.listen(); }); @@ -98,7 +100,7 @@ describe("SDK worker tests", () => { }); it("Should handle thrown errors", async () => { const createComment = jest.fn(); - jest.mock("../src/sdk/octokit", () => ({ + jest.mock(sdkOctokitImportPath, () => ({ customOctokit: class MockOctokit { constructor() { return { @@ -113,7 +115,7 @@ describe("SDK worker tests", () => { })); const { createPlugin } = await import("../src/sdk/server"); - const app = await createPlugin( + const app = createPlugin( async (context: Context<{ shouldFail: boolean }>) => { if (context.config.shouldFail) { throw context.logger.error("test error"); @@ -177,7 +179,7 @@ describe("SDK actions tests", () => { it("Should accept correct request", async () => { const inputs = new PluginInput(eventHandler, "stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, {}, "test_token", ""); const githubInputs = await inputs.getWorkflowInputs(); - jest.mock("@actions/github", () => ({ + jest.mock(githubActionImportPath, () => ({ context: { runId: "1", payload: { @@ -188,7 +190,7 @@ describe("SDK actions tests", () => { })); const setOutput = jest.fn(); const setFailed = jest.fn(); - jest.mock("@actions/core", () => ({ + jest.mock(githubCoreImportPath, () => ({ setOutput, setFailed, })); @@ -248,7 +250,7 @@ describe("SDK actions tests", () => { })); const setOutput = jest.fn(); const setFailed = jest.fn(); - jest.mock("@actions/core", () => ({ + jest.mock(githubCoreImportPath, () => ({ setOutput, setFailed, })); @@ -271,7 +273,7 @@ describe("SDK actions tests", () => { const inputs = new PluginInput(eventHandler, "stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, {}, "test_token", ""); const githubInputs = await inputs.getWorkflowInputs(); - jest.mock("@actions/github", () => ({ + jest.mock(githubActionImportPath, () => ({ context: { runId: "1", payload: { @@ -291,18 +293,18 @@ describe("SDK actions tests", () => { })); const setOutput = jest.fn(); const setFailed = jest.fn(); - jest.mock("@actions/core", () => ({ + jest.mock(githubCoreImportPath, () => ({ setOutput, setFailed, })); - const createDispatchEvent = jest.fn(); - jest.mock("../src/sdk/octokit", () => ({ + const createDispatchEventFn = jest.fn(); + jest.mock(sdkOctokitImportPath, () => ({ customOctokit: class MockOctokit { constructor() { return { rest: { repos: { - createDispatchEvent: createDispatchEvent, + createDispatchEvent: createDispatchEventFn, }, }, }; @@ -323,7 +325,7 @@ describe("SDK actions tests", () => { ); expect(setFailed).not.toHaveBeenCalled(); expect(setOutput).toHaveBeenCalledWith("result", { event: issueCommentedEvent.eventName }); - expect(createDispatchEvent).toHaveBeenCalledWith({ + expect(createDispatchEventFn).toHaveBeenCalledWith({ event_type: "return-data-to-ubiquity-os-kernel", owner: repo.owner, repo: repo.repo, From af5da64136811c8a1d5766cf2e851a282f86af63 Mon Sep 17 00:00:00 2001 From: whilefoo Date: Sun, 27 Oct 2024 14:21:05 +0100 Subject: [PATCH 7/8] feat: use empty store --- src/github/utils/kv-store.ts | 4 ---- tests/sdk.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/github/utils/kv-store.ts b/src/github/utils/kv-store.ts index acd115d..5432d2a 100644 --- a/src/github/utils/kv-store.ts +++ b/src/github/utils/kv-store.ts @@ -39,10 +39,6 @@ export interface KvStore { * @template T - The type of values to be stored. */ export class EmptyStore implements KvStore { - constructor(kv: KVNamespace) { - console.log(`Creating empty kv`, kv); - } - get(id: string): Promise { console.log(`get KV ${id}`); return Promise.resolve(null); diff --git a/tests/sdk.test.ts b/tests/sdk.test.ts index 0ac8e06..6657231 100644 --- a/tests/sdk.test.ts +++ b/tests/sdk.test.ts @@ -6,7 +6,7 @@ import * as crypto from "crypto"; import { createPlugin } from "../src/sdk/server"; import { Context } from "../src/sdk/context"; import { GitHubEventHandler } from "../src/github/github-event-handler"; -import { CloudflareKv } from "../src/github/utils/cloudflare-kv"; +import { EmptyStore } from "../src/github/utils/kv-store"; import { PluginChainState, PluginInput } from "../src/github/types/plugin"; import { EmitterWebhookEventName } from "@octokit/webhooks"; @@ -36,7 +36,7 @@ const eventHandler = new GitHubEventHandler({ webhookSecret: "test", appId: "1", privateKey: privateKey, - pluginChainState: undefined as unknown as CloudflareKv, + pluginChainState: new EmptyStore(), }); const app = createPlugin( From 9eb10143182c70873d7ed7c92c7659eaf3fb1ad9 Mon Sep 17 00:00:00 2001 From: whilefoo Date: Sun, 27 Oct 2024 14:22:28 +0100 Subject: [PATCH 8/8] fix: empty store --- src/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker.ts b/src/worker.ts index f186bcc..00e081c 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -18,7 +18,7 @@ export default { webhookSecret: env.APP_WEBHOOK_SECRET, appId: env.APP_ID, privateKey: env.APP_PRIVATE_KEY, - pluginChainState: new EmptyStore(env.PLUGIN_CHAIN_STATE), + pluginChainState: new EmptyStore(), }); bindHandlers(eventHandler); await eventHandler.webhooks.verifyAndReceive({ id, name: eventName, payload: await request.text(), signature: signatureSha256 });