diff --git a/.cspell.json b/.cspell.json index e2ac2df..4aa417b 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,7 +4,7 @@ "ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log"], "useGitignore": true, "language": "en", - "words": ["dataurl", "devpool", "fkey", "outdir", "servedir", "supabase", "typebox", "ubiquibot", "smee", "tomlify"], + "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}"] diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 338c3bb..0000000 --- a/.eslintrc +++ /dev/null @@ -1,53 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": ["./tsconfig.json"] - }, - "plugins": ["@typescript-eslint", "sonarjs"], - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:sonarjs/recommended"], - "ignorePatterns": ["**/*.js"], - "rules": { - "prefer-arrow-callback": ["warn", { "allowNamedFunctions": true }], - "func-style": ["warn", "declaration", { "allowArrowFunctions": false }], - "@typescript-eslint/no-floating-promises": "error", - "@typescript-eslint/no-non-null-assertion": "error", - "constructor-super": "error", - "no-invalid-this": "off", - "@typescript-eslint/no-invalid-this": ["error"], - "no-restricted-syntax": ["error"], - "use-isnan": "error", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "args": "after-used", - "ignoreRestSiblings": true, - "vars": "all", - "varsIgnorePattern": "^_", - "argsIgnorePattern": "^_" - } - ], - "@typescript-eslint/await-thenable": "error", - "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/restrict-plus-operands": "error", - "sonarjs/no-all-duplicated-branches": "error", - "sonarjs/no-collection-size-mischeck": "error", - "sonarjs/no-duplicated-branches": "error", - "sonarjs/no-element-overwrite": "error", - "sonarjs/no-identical-conditions": "error", - "sonarjs/no-identical-expressions": "error", - "@typescript-eslint/naming-convention": [ - "error", - { "selector": "interface", "format": ["PascalCase"], "custom": { "regex": "^I[A-Z]", "match": false } }, - { "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require" }, - { "selector": "typeLike", "format": ["PascalCase"] }, - { "selector": "typeParameter", "format": ["PascalCase"], "prefix": ["T"] }, - { "selector": "variable", "format": ["camelCase", "UPPER_CASE"], "leadingUnderscore": "allow", "trailingUnderscore": "allow" }, - { "selector": "variable", "format": ["camelCase"], "leadingUnderscore": "allow", "trailingUnderscore": "allow" }, - { "selector": "variable", "modifiers": ["destructured"], "format": null }, - { "selector": "variable", "types": ["boolean"], "format": ["PascalCase"], "prefix": ["is", "should", "has", "can", "did", "will", "does"] }, - { "selector": "variableLike", "format": ["camelCase"] }, - { "selector": ["function", "variable"], "format": ["camelCase"] } - ] - } -} diff --git a/.github/knip.ts b/.github/knip.ts index 6223f97..df72ff7 100644 --- a/.github/knip.ts +++ b/.github/knip.ts @@ -3,9 +3,9 @@ import type { KnipConfig } from "knip"; const config: KnipConfig = { entry: ["src/worker.ts"], project: ["src/**/*.ts"], - ignoreBinaries: ["build", "i", "format:cspell", "awk", "lsof"], + ignoreBinaries: ["i"], 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; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b276b13..0bbfe66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,11 +30,13 @@ jobs: bun setup-kv env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - uses: cloudflare/wrangler-action@v3 with: - wranglerVersion: '3.57.0' + wranglerVersion: "3.57.0" apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} secrets: | WEBHOOK_PROXY_URL WEBHOOK_SECRET diff --git a/.github/workflows/bun-testing.yml b/.github/workflows/bun-testing.yml index 43c3e54..45a6e5a 100644 --- a/.github/workflows/bun-testing.yml +++ b/.github/workflows/bun-testing.yml @@ -2,7 +2,7 @@ name: Run Bun testing suite on: workflow_dispatch: pull_request: - types: [ opened, synchronize ] + types: [opened, synchronize] env: NODE_ENV: "test" @@ -18,7 +18,7 @@ jobs: - uses: oven-sh/setup-bun@v1 - uses: actions/setup-node@v4 with: - node-version: '20.10.0' + node-version: "20.10.0" - uses: actions/checkout@master with: fetch-depth: 0 diff --git a/.github/workflows/cspell.yml b/.github/workflows/format.yml similarity index 53% rename from .github/workflows/cspell.yml rename to .github/workflows/format.yml index 5825b92..d56968a 100644 --- a/.github/workflows/cspell.yml +++ b/.github/workflows/format.yml @@ -1,11 +1,11 @@ -name: Spell Check +name: Format Checks on: push: jobs: - spellcheck: - name: Check for spelling errors + formatCheck: + name: Check for formatting issues runs-on: ubuntu-latest steps: @@ -17,8 +17,10 @@ jobs: with: node-version: "20.10.0" - - name: Install bun & CSpell - run: npm i -g bun cspell + - uses: oven-sh/setup-bun@v1 - - name: Run cspell - run: bun format:cspell + - name: Install deps + run: bun i + + - name: Run checks + run: bun format diff --git a/.github/workflows/knip-reporter.yml b/.github/workflows/knip-reporter.yml new file mode 100644 index 0000000..282c9a8 --- /dev/null +++ b/.github/workflows/knip-reporter.yml @@ -0,0 +1,38 @@ +name: Knip-reporter + +on: + workflow_run: + workflows: ["Knip"] + types: + - completed + +jobs: + knip-reporter: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion != 'success' }} + steps: + - uses: actions/download-artifact@v4 + with: + name: knip-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read pr number + id: pr-number + uses: juliangruber/read-file-action@v1 + with: + path: ./pr-number.txt + trim: true + + - name: Report knip results to pull request + uses: gitcoindev/knip-reporter@main + with: + verbose: true + comment_id: ${{ github.workflow }}-reporter + command_script_name: knip-ci + annotations: true + ignore_results: false + json_input: true + json_input_file_name: knip-results.json + pull_request_number: ${{ steps.pr-number.outputs.content }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/knip.yml b/.github/workflows/knip.yml index 8abcedf..e8f2bd4 100644 --- a/.github/workflows/knip.yml +++ b/.github/workflows/knip.yml @@ -1,10 +1,7 @@ name: Knip on: - pull_request_target: - workflow_dispatch: - -permissions: write-all + pull_request: jobs: run-knip: @@ -23,11 +20,17 @@ jobs: npm i -g bun bun install - - name: Report knip results to pull request - uses: Codex-/knip-reporter@v2 + - name: Store PR number + run: echo ${{ github.event.number }} > pr-number.txt + + - name: Run Knip + run: bun knip || bun knip --reporter json > knip-results.json + + - name: Upload knip result + if: failure() + uses: actions/upload-artifact@v4 with: - verbose: true - comment_id: ${{ github.workflow }}-reporter - command_script_name: knip-ci - annotations: true - ignore_results: false + name: knip-results + path: | + knip-results.json + pr-number.txt diff --git a/README.md b/README.md index 25c7484..ad390e2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # @ubiquity/ubiquibot-kernel The kernel is designed to: @@ -11,11 +10,11 @@ The kernel is designed to: - **`PRIVATE_KEY`** Obtain a private key from your GitHub App settings and convert it to the Public-Key Cryptography Standards #8 (PKCS#8) format. Use the following command to perform this conversion and append the result to your `.dev.vars` file: - ```sh - echo "PRIVATE_KEY=\"$(openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in YOUR_PRIVATE_KEY.PEM | awk 'BEGIN{ORS="\\n"} 1')\"" >> .dev.vars - ``` + ```sh + echo "PRIVATE_KEY=\"$(openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in YOUR_PRIVATE_KEY.PEM | awk 'BEGIN{ORS="\\n"} 1')\"" >> .dev.vars + ``` - **Note:** Replace `YOUR_PRIVATE_KEY.PEM` with the path to your actual PEM file when running the command. + **Note:** Replace `YOUR_PRIVATE_KEY.PEM` with the path to your actual PEM file when running the command. - **`WEBHOOK_SECRET`** Set this value in both your GitHub App settings and here. @@ -26,7 +25,6 @@ The kernel is designed to: - **`WEBHOOK_PROXY_URL` (only for development)** Obtain a webhook URL at [smee.io](https://smee.io/) and set it in your GitHub App settings. - ### Quick Start ```bash @@ -39,31 +37,38 @@ bun dev ### Deploying to Cloudflare Workers 1. **Install Dependencies:** + - Execute `bun install` to install the required dependencies. 2. **Create a Github App:** + - Generate a Github App and configure its settings. - Navigate to app settings and click `Permissions & events`. - Ensure the app is subscribed to all events with the following permissions: Repository permissions: + - Actions: Read & Write - Contents: Read & Write - Issues: Read & Write - Pull Requests: Read & Write Organization permissions: + - Members: Read only 3. **Cloudflare Account Setup:** + - If not done already, create a Cloudflare account. - Run `npx wrangler login` to log in. 4. **Create a KV Namespace:** + - Generate a KV namespace using `npx wrangler kv:namespace create PLUGIN_CHAIN_STATE`. - Copy the generated ID and paste it under `[env.dev]` in `wrangler.toml`. 5. **Manage Secrets:** + - Add (env) secrets using `npx wrangler secret put --env dev`. - For the private key, execute the following (replace `YOUR_PRIVATE_KEY.PEM` with the actual PEM file path): @@ -97,7 +102,9 @@ Example usage: const input: PluginInput = { stateId: "abc123", eventName: "issue_comment.created", - eventPayload: { /* ... */ }, + eventPayload: { + /* ... */ + }, settings: '{ "key": "value" }', authToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", ref: "refs/heads/main", @@ -126,6 +133,45 @@ 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 diff --git a/bun.lockb b/bun.lockb index 500a3a1..506158a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/deploy/setup_kv_namespace.ts b/deploy/setup-kv-namespace.ts similarity index 96% rename from deploy/setup_kv_namespace.ts rename to deploy/setup-kv-namespace.ts index 2707048..5f146b7 100644 --- a/deploy/setup_kv_namespace.ts +++ b/deploy/setup-kv-namespace.ts @@ -77,6 +77,7 @@ async function main() { let namespaceId: string; try { const res = execSync(`wrangler kv:namespace create ${NAMESPACE_TITLE}`).toString(); + console.log(res); const newId = res.match(/id = \s*"([^"]+)"/)?.[1]; if (!newId) { throw new Error(`The new ID could not be found.`); @@ -84,6 +85,7 @@ async function main() { namespaceId = newId; console.log(`Namespace created with ID: ${namespaceId}`); } catch (error) { + console.error(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) { @@ -99,6 +101,6 @@ async function main() { main() .then(() => console.log("Successfully bound namespace.")) .catch((e) => { - console.error("Error checking or creating namespace:", e); + console.error("Error checking or creating namespace:\n", e); process.exit(1); }); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..acbd01f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,126 @@ +// @ts-check +import tsEslint from "typescript-eslint"; +import eslint from "@eslint/js"; +import sonarjs from "eslint-plugin-sonarjs"; +import checkFile from "eslint-plugin-check-file"; + +export default tsEslint.config({ + plugins: { + "@typescript-eslint": tsEslint.plugin, + "check-file": checkFile, + }, + ignores: [".github/knip.ts", "**/.wrangler/**"], + extends: [eslint.configs.recommended, ...tsEslint.configs.recommended, sonarjs.configs.recommended], + languageOptions: { + parser: tsEslint.parser, + parserOptions: { + project: ["./tsconfig.json"], + }, + }, + rules: { + "check-file/filename-naming-convention": [ + "error", + { + "**/*.{js,ts}": "+([-.a-z0-9])", + }, + ], + "prefer-arrow-callback": [ + "warn", + { + allowNamedFunctions: true, + }, + ], + "func-style": [ + "warn", + "declaration", + { + allowArrowFunctions: false, + }, + ], + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "constructor-super": "error", + "no-invalid-this": "off", + "@typescript-eslint/no-invalid-this": ["error"], + "no-restricted-syntax": ["error", "ForInStatement"], + "use-isnan": "error", + "no-unneeded-ternary": "error", + "no-nested-ternary": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "after-used", + ignoreRestSiblings: true, + vars: "all", + varsIgnorePattern: "^_", + argsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "sonarjs/no-all-duplicated-branches": "error", + "sonarjs/no-collection-size-mischeck": "error", + "sonarjs/no-duplicated-branches": "error", + "sonarjs/no-element-overwrite": "error", + "sonarjs/no-identical-conditions": "error", + "sonarjs/no-identical-expressions": "error", + "@typescript-eslint/naming-convention": [ + "error", + { + selector: "interface", + format: ["StrictPascalCase"], + custom: { + regex: "^I[A-Z]", + match: false, + }, + }, + { + selector: "memberLike", + modifiers: ["private"], + format: ["strictCamelCase"], + leadingUnderscore: "require", + }, + { + selector: "typeLike", + format: ["StrictPascalCase"], + }, + { + selector: "typeParameter", + format: ["StrictPascalCase"], + prefix: ["T"], + }, + { + selector: "variable", + format: ["strictCamelCase", "UPPER_CASE"], + leadingUnderscore: "allow", + trailingUnderscore: "allow", + }, + { + selector: "variable", + format: ["strictCamelCase"], + leadingUnderscore: "allow", + trailingUnderscore: "allow", + }, + { + selector: "variable", + modifiers: ["destructured"], + format: null, + }, + { + selector: "variable", + types: ["boolean"], + format: ["StrictPascalCase"], + prefix: ["is", "should", "has", "can", "did", "will", "does"], + }, + { + selector: "variableLike", + format: ["strictCamelCase"], + }, + { + selector: ["function", "variable"], + format: ["strictCamelCase"], + }, + ], + }, +}); diff --git a/package.json b/package.json index 4cf41fe..5a4c0d7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "dev": "run-p worker proxy", "predev": "tsx predev.ts", - "format": "run-s format:lint format:prettier format:cspell", + "format": "run-s format:*", "format:lint": "eslint --fix .", "format:prettier": "prettier --write .", "format:cspell": "cspell **/*", @@ -24,7 +24,8 @@ "knip": "knip --config .github/knip.ts", "knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts", "test": "bun test", - "setup-kv": "bun --env-file=.dev.vars deploy/setup_kv_namespace.ts" + "plugin:hello-world": "tsx tests/__mocks__/hello-world-plugin.ts", + "setup-kv": "bun --env-file=.dev.vars deploy/setup-kv-namespace.ts" }, "keywords": [ "typescript", @@ -34,49 +35,51 @@ "open-source" ], "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-retry": "^7.1.1", - "@octokit/plugin-throttling": "^9.3.0", - "@octokit/types": "^13.5.0", - "@octokit/webhooks": "^13.2.7", - "@octokit/webhooks-types": "^7.5.1", - "@sinclair/typebox": "^0.32.5", - "dotenv": "^16.4.4", - "smee-client": "^2.0.0", + "@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-retry": "7.1.1", + "@octokit/plugin-throttling": "9.3.0", + "@octokit/types": "13.5.0", + "@octokit/webhooks": "13.2.7", + "@octokit/webhooks-types": "7.5.1", + "@sinclair/typebox": "0.32.33", + "dotenv": "16.4.5", + "smee-client": "2.0.1", "typebox-validators": "0.3.5", - "yaml": "^2.4.1" + "yaml": "2.4.5" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240117.0", - "@commitlint/cli": "^18.6.1", - "@commitlint/config-conventional": "^18.6.2", - "@cspell/dict-node": "^4.0.3", - "@cspell/dict-software-terms": "^3.3.18", - "@cspell/dict-typescript": "^3.1.2", + "@commitlint/cli": "19.3.0", + "@commitlint/config-conventional": "19.2.2", + "@cspell/dict-node": "5.0.1", + "@cspell/dict-software-terms": "3.4.6", + "@cspell/dict-typescript": "3.1.5", + "@eslint/js": "9.5.0", "@mswjs/data": "0.16.1", - "@types/bun": "1.1.3", - "@types/node": "^20.11.19", - "@typescript-eslint/eslint-plugin": "^7.0.1", - "@typescript-eslint/parser": "^7.0.1", - "cspell": "^8.4.0", - "esbuild": "^0.20.1", - "eslint": "^8.56.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-sonarjs": "^0.24.0", - "husky": "^9.0.11", - "knip": "^5.0.1", - "lint-staged": "^15.2.2", - "npm-run-all": "^4.1.5", - "prettier": "^3.2.5", + "@mswjs/http-middleware": "0.10.1", + "@types/bun": "1.1.5", + "@types/node": "20.14.6", + "cspell": "8.9.0", + "esbuild": "0.21.5", + "eslint": "9.5.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", + "lint-staged": "15.2.7", + "npm-run-all": "4.1.5", + "prettier": "3.3.2", "toml": "3.0.0", "tomlify-j0.4": "3.0.0", - "tsx": "^4.7.1", - "typescript": "^5.3.3", - "wrangler": "3.58.0" + "tsx": "4.15.6", + "typescript": "5.4.5", + "typescript-eslint": "7.13.1", + "wrangler": "3.61.0" }, "lint-staged": { "*.ts": [ @@ -92,5 +95,5 @@ "@commitlint/config-conventional" ] }, - "packageManager": "bun@1.1.0" + "packageManager": "bun@1.1.15" } diff --git a/src/github/github-event-handler.ts b/src/github/github-event-handler.ts index 8cb1d01..4f7895e 100644 --- a/src/github/github-event-handler.ts +++ b/src/github/github-event-handler.ts @@ -2,14 +2,14 @@ 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 { CloudflareKv } from "./utils/cloudflare-kv"; import { PluginChainState } from "./types/plugin"; export type Options = { webhookSecret: string; appId: string | number; privateKey: string; - pluginChainState: CloudflareKV; + pluginChainState: CloudflareKv; }; export class GitHubEventHandler { @@ -17,7 +17,7 @@ export class GitHubEventHandler { public on: Webhooks["on"]; public onAny: Webhooks["onAny"]; public onError: Webhooks["onError"]; - public pluginChainState: CloudflareKV; + public pluginChainState: CloudflareKv; private _webhookSecret: string; private _privateKey: string; diff --git a/src/github/utils/cloudflare-kv.ts b/src/github/utils/cloudflare-kv.ts index f6dc1a2..f5407a3 100644 --- a/src/github/utils/cloudflare-kv.ts +++ b/src/github/utils/cloudflare-kv.ts @@ -1,4 +1,4 @@ -export class CloudflareKV { +export class CloudflareKv { private _kv: KVNamespace; constructor(kv: KVNamespace) { diff --git a/src/github/utils/config.ts b/src/github/utils/config.ts index f21d9b5..c3a563a 100644 --- a/src/github/utils/config.ts +++ b/src/github/utils/config.ts @@ -105,7 +105,7 @@ function checkPluginChainExpressions(plugin: PluginConfiguration["plugins"]["*"] const calledIds = new Set(); for (const use of plugin.uses) { if (!use.id) continue; - for (const key in use.with) { + for (const key of Object.keys(use.with)) { const value = use.with[key]; if (typeof value !== "string") continue; checkExpression(value, allIds, calledIds); diff --git a/src/worker.ts b/src/worker.ts index 4ccc3ac..2fc7a63 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 { CloudflareKv } from "./github/utils/cloudflare-kv"; import { WebhookEventName } from "@octokit/webhooks-types"; export default { @@ -11,16 +11,16 @@ export default { try { validateEnv(env); const eventName = getEventName(request); - const signatureSHA256 = getSignature(request); + const signatureSha256 = getSignature(request); const id = getId(request); const eventHandler = new GitHubEventHandler({ webhookSecret: env.WEBHOOK_SECRET, appId: env.APP_ID, privateKey: env.PRIVATE_KEY, - pluginChainState: new CloudflareKV(env.PLUGIN_CHAIN_STATE), + pluginChainState: new CloudflareKv(env.PLUGIN_CHAIN_STATE), }); bindHandlers(eventHandler); - await eventHandler.webhooks.verifyAndReceive({ id, name: eventName, payload: await request.text(), signature: signatureSHA256 }); + await eventHandler.webhooks.verifyAndReceive({ id, name: eventName, payload: await request.text(), signature: signatureSha256 }); return new Response("ok\n", { status: 200, headers: { "content-type": "text/plain" } }); } catch (error) { return handleUncaughtError(error); @@ -57,11 +57,11 @@ function getEventName(request: Request): WebhookEventName { } function getSignature(request: Request): string { - const signatureSHA256 = request.headers.get("x-hub-signature-256"); - if (!signatureSHA256) { + const signatureSha256 = request.headers.get("x-hub-signature-256"); + if (!signatureSha256) { throw new Error(`Missing "x-hub-signature-256" header`); } - return signatureSHA256; + return signatureSha256; } function getId(request: Request): string { diff --git a/tests/__mocks__/hello-world-plugin.ts b/tests/__mocks__/hello-world-plugin.ts new file mode 100644 index 0000000..3d59e2b --- /dev/null +++ b/tests/__mocks__/hello-world-plugin.ts @@ -0,0 +1,51 @@ +import { createServer } from "@mswjs/http-middleware"; +import { Octokit } from "@octokit/core"; +import { http, HttpResponse } from "msw"; + +type KernelInput = { + authToken: string; + eventPayload: { + issue: { + number: number; + }; + organization: { + login: string; + }; + repository: { + name: string; + }; + }; + settings: { + response: string; + }; +}; + +const PORT = 9090; + +const handlers = [ + http.post("/", async (data) => { + const body: KernelInput = (await data.request.json()) as KernelInput; + + const octokit = new Octokit({ auth: body.authToken }); + + await octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", { + owner: body.eventPayload.organization.login, + repo: body.eventPayload.repository.name, + issue_number: body.eventPayload.issue.number, + body: body.settings.response, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + }); + + return HttpResponse.json({ + state_id: "state_id_uuid_1", + output: `{ "result": "success", "message": "${body.settings.response}" }`, + }); + }), +]; + +const httpServer = createServer(...handlers); + +httpServer.listen(PORT); +console.log(`hello-world-plugin is listening on http://127.0.0.1:${PORT}`); diff --git a/tsconfig.json b/tsconfig.json index 25beebd..76d1e39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,8 +37,8 @@ // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ - "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, - "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, + // "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, + // "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ /* Emit */ @@ -69,7 +69,7 @@ /* Interop Constraints */ "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, - // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, @@ -96,6 +96,6 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */, - }, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } }