Skip to content

Commit

Permalink
Merge pull request #127 from ubiquity-os/development
Browse files Browse the repository at this point in the history
Merge development into main
  • Loading branch information
gentlementlegen authored Oct 11, 2024
2 parents a9c965d + 70561e9 commit 02dbe86
Show file tree
Hide file tree
Showing 29 changed files with 616 additions and 353 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", "mswjs", "outdir", "servedir", "supabase", "typebox", "ubiquibot", "smee", "tomlify", "hono"],
"words": ["dataurl", "devpool", "fkey", "mswjs", "outdir", "servedir", "supabase", "typebox", "ubiquity-os", "smee", "tomlify", "hono", "cfworker"],
"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 @@ -4,7 +4,7 @@ const config: KnipConfig = {
entry: ["src/worker.ts", "deploy/setup-kv-namespace.ts"],
project: ["src/**/*.ts"],
ignore: ["jest.config.ts"],
ignoreBinaries: ["i"],
ignoreBinaries: ["i", "publish"],
ignoreExportsUsedInFile: true,
ignoreDependencies: ["@mswjs/data", "esbuild", "eslint-config-prettier", "eslint-plugin-prettier", "msw", "ts-node"],
};
Expand Down
19 changes: 18 additions & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
release-type: simple
release-type: node
target-branch: main
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20.10.0"
registry-url: https://registry.npmjs.org/

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

- run: |
bun install -p --frozen-lockfile
bun sdk:build
if: ${{ steps.release.outputs.release_created }}
- run: bun publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
if: ${{ steps.release.outputs.release_created }}
1 change: 1 addition & 0 deletions .github/workflows/worker-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
- uses: cloudflare/wrangler-action@v3
id: wrangler_deploy
with:
wranglerVersion: "3.79.0"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
Expand Down
316 changes: 158 additions & 158 deletions CHANGELOG.md

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @ubiquity/ubiquibot-kernel
# @ubiquity-os/ubiquity-os-kernel

The kernel is designed to:

Expand Down Expand Up @@ -28,8 +28,8 @@ The kernel is designed to:
### Quick Start

```bash
git clone https://github.com/ubiquity/ubiquibot-kernel
cd ubiquibot-kernel
git clone https://github.com/ubiquity-os/ubiquity-os-kernel
cd ubiquity-os-kernel
bun install
bun dev
```
Expand Down Expand Up @@ -115,7 +115,7 @@ const input: PluginInput = {
Data is returned using the `repository_dispatch` event on the plugin's repository, and the output is structured within the `client_payload`.
The `event_type` must be set to `return_data_to_ubiquibot_kernel`.
The `event_type` must be set to `return-data-to-ubiquity-os-kernel`.
```typescript
interface PluginOutput {
Expand All @@ -137,14 +137,14 @@ const output: PluginOutput = {
The kernel supports 2 types of plugins:
1. GitHub actions ([wiki](https://github.com/ubiquity/ubiquibot-kernel/wiki/How-it-works))
1. GitHub actions ([wiki](https://github.com/ubiquity-os/ubiquity-os-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`):
3. Update the bot's config file in the repository where you use the bot (`OWNER/REPOSITORY/.github/.ubiquity-os.config.yml`):
```yml
plugins:
Expand All @@ -164,12 +164,12 @@ 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
3. The kernel passes GitHub event payload, bot's access token and plugin settings (from `.ubiquity-os.config.yml`) to the plugin endpoint
4. The plugin performs all the required actions and returns the result
## Hello world plugin tutorial
A screencast tutorial on how to set up and run a hello world plugin is available at [wiki](https://github.com/ubiquity/ubiquibot-kernel/wiki/Hello-world-plugin-onboarding-tutorial).
A screencast tutorial on how to set up and run a hello world plugin is available at [wiki](https://github.com/ubiquity-os/ubiquity-os-kernel/wiki/Hello-world-plugin-onboarding-tutorial).
## Testing
Expand Down
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions deploy/setup-kv-namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ 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 res = execSync(`wrangler kv namespace create ${NAMESPACE_TITLE}`).toString();
console.log(res);
const newId = res.match(/id = \s*"([^"]+)"/)?.[1];
if (!newId) {
Expand All @@ -86,7 +86,7 @@ async function main() {
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 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}`);
Expand Down
25 changes: 13 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "@ubiquity-dao/ubiquibot-kernel",
"name": "@ubiquity-dao/ubiquity-os-kernel",
"version": "0.0.1",
"private": false,
"description": "The kernel for UbiquiBot.",
"description": "The kernel for UbiquityOS.",
"module": "dist/esm/index.js",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down Expand Up @@ -50,6 +50,7 @@
"dependencies": {
"@actions/core": "1.10.1",
"@actions/github": "6.0.0",
"@cfworker/json-schema": "2.0.1",
"@octokit/auth-app": "7.1.0",
"@octokit/core": "6.1.2",
"@octokit/plugin-paginate-rest": "11.3.3",
Expand All @@ -60,7 +61,7 @@
"@octokit/webhooks": "13.2.8",
"@octokit/webhooks-types": "7.5.1",
"@sinclair/typebox": "0.32.35",
"@ubiquity-dao/ubiquibot-logger": "^1.3.1",
"@ubiquity-os/ubiquity-os-logger": "^1.3.2",
"dotenv": "16.4.5",
"hono": "4.4.13",
"smee-client": "2.0.1",
Expand All @@ -69,13 +70,13 @@
"yaml": "2.4.5"
},
"devDependencies": {
"@swc/core": "1.6.5",
"@swc/jest": "0.2.36",
"tsup": "8.1.0",
"@jest/globals": "29.7.0",
"@types/jest": "29.5.12",
"jest": "29.7.0",
"jest-junit": "16.0.0",
"@swc/core": "1.6.5",
"@swc/jest": "0.2.36",
"tsup": "8.1.0",
"@jest/globals": "29.7.0",
"@types/jest": "29.5.12",
"jest": "29.7.0",
"jest-junit": "16.0.0",
"@cloudflare/workers-types": "4.20240117.0",
"@commitlint/cli": "19.3.0",
"@commitlint/config-conventional": "19.2.2",
Expand All @@ -101,9 +102,9 @@
"toml": "3.0.0",
"tomlify-j0.4": "3.0.0",
"tsx": "4.16.2",
"typescript": "5.5.3",
"typescript": "5.6.2",
"typescript-eslint": "7.16.0",
"wrangler": "3.78.10"
"wrangler": "3.79.0"
},
"lint-staged": {
"*.ts": [
Expand Down
2 changes: 0 additions & 2 deletions predev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,5 @@ exec(command, (error, stdout) => {
}
console.log(`Process ${pid} killed successfully.`);
});
} else {
console.log("No process found listening on port 8787.");
}
});
3 changes: 3 additions & 0 deletions src/github/github-event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { CloudflareKv } from "./utils/cloudflare-kv";
import { PluginChainState } from "./types/plugin";

export type Options = {
environment: "production" | "development";
webhookSecret: string;
appId: string | number;
privateKey: string;
Expand All @@ -19,11 +20,13 @@ export class GitHubEventHandler {
public onError: Webhooks<SimplifiedContext>["onError"];
public pluginChainState: CloudflareKv<PluginChainState>;

readonly environment: "production" | "development";
private readonly _webhookSecret: string;
private readonly _privateKey: string;
private readonly _appId: number;

constructor(options: Options) {
this.environment = options.environment;
this._privateKey = options.privateKey;
this._appId = Number(options.appId);
this._webhookSecret = options.webhookSecret;
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 @@ -8,6 +8,7 @@ import { dispatchWorker, dispatchWorkflow, getDefaultBranch } from "../utils/wor
import { PluginInput } from "../types/plugin";
import { isGithubPlugin, PluginConfiguration } from "../types/plugin-configuration";
import { getManifest, getPluginsForEvent } from "../utils/plugins";
import handlePushEvent from "./push-event";

function tryCatchWrapper(fn: (event: EmitterWebhookEvent) => unknown) {
return async (event: EmitterWebhookEvent) => {
Expand All @@ -22,6 +23,7 @@ function tryCatchWrapper(fn: (event: EmitterWebhookEvent) => unknown) {
export function bindHandlers(eventHandler: GitHubEventHandler) {
eventHandler.on("repository_dispatch", repositoryDispatch);
eventHandler.on("issue_comment.created", issueCommentCreated);
eventHandler.on("push", handlePushEvent);
eventHandler.onAny(tryCatchWrapper((event) => handleEvent(event, eventHandler))); // onAny should also receive GithubContext but the types in octokit/webhooks are weird
}

Expand Down
161 changes: 161 additions & 0 deletions src/github/handlers/push-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Validator } from "@cfworker/json-schema";
import { ValueErrorType } from "@sinclair/typebox/value";
import { ValueError } from "typebox-validators";
import YAML, { LineCounter, Node, YAMLError } from "yaml";
import { GitHubContext } from "../github-context";
import { configSchema, PluginConfiguration } from "../types/plugin-configuration";
import { CONFIG_FULL_PATH, getConfigurationFromRepo } from "../utils/config";
import { getManifest } from "../utils/plugins";

function constructErrorBody(
errors: Iterable<ValueError> | (YAML.YAMLError | ValueError)[],
rawData: string | null,
repository: GitHubContext<"push">["payload"]["repository"],
after: string
) {
const body = [];
if (errors) {
for (const error of errors) {
body.push("> [!CAUTION]\n");
if (error instanceof YAMLError) {
body.push(`> https://github.com/${repository.owner?.login}/${repository.name}/blob/${after}/${CONFIG_FULL_PATH}#L${error.linePos?.[0].line || 0}`);
} else if (rawData) {
const lineCounter = new LineCounter();
const doc = YAML.parseDocument(rawData, { lineCounter });
const path = error.path.split("/").filter((o) => o);
if (error.type === ValueErrorType.ObjectRequiredProperty) {
path.splice(path.length - 1, 1);
}
const node = doc.getIn(path, true) as Node;
const linePosStart = lineCounter.linePos(node?.range?.[0] || 0);
body.push(`> https://github.com/${repository.owner?.login}/${repository.name}/blob/${after}/${CONFIG_FULL_PATH}#L${linePosStart.line}`);
}
const message = [];
if (error instanceof YAMLError) {
message.push(error.message);
} else {
message.push(`path: ${error.path}\n`);
message.push(`value: ${error.value}\n`);
message.push(`message: ${error.message}`);
}
body.push(`\n> \`\`\`yml\n`);
body.push(`> ${message.join("").replaceAll("\n", "\n> ")}`);
body.push(`\n> \`\`\`\n\n`);
}
}
return body;
}

async function createCommitComment(
context: GitHubContext,
{ owner, repo, commitSha, userLogin }: { owner: string; repo: string; commitSha: string; userLogin?: string },
body: string[]
) {
const { octokit } = context;

const comment = (
await octokit.rest.repos.listCommentsForCommit({
owner: owner,
repo: repo,
commit_sha: commitSha,
})
).data
.filter((o) => o.user?.type === "Bot")
.pop();
if (comment) {
await octokit.rest.repos.updateCommitComment({
owner: owner,
repo: repo,
commit_sha: commitSha,
comment_id: comment.id,
body: `${comment.body}\n${body.join("")}`,
});
} else {
body.unshift(`@${userLogin} Configuration is invalid.\n`);
await octokit.rest.repos.createCommitComment({
owner: owner,
repo: repo,
commit_sha: commitSha,
body: body.join(""),
});
}
}

async function checkPluginConfigurations(context: GitHubContext<"push">, config: PluginConfiguration, rawData: string | null) {
const errors: (ValueError | YAML.YAMLError)[] = [];
const doc = rawData ? YAML.parseDocument(rawData) : null;

for (let i = 0; i < config.plugins.length; ++i) {
const { uses } = config.plugins[i];
for (let j = 0; j < uses.length; ++j) {
const { plugin, with: args } = uses[j];
const manifest = await getManifest(context, plugin);
if (!manifest?.configuration) {
errors.push({
path: `plugins/${i}/uses/${j}`,
message: `Failed to fetch the manifest configuration.`,
value: plugin,
type: 0,
schema: configSchema,
});
} else {
const validator = new Validator(manifest.configuration, "7", false);
const result = validator.validate(args);

if (!result.valid) {
for (const error of result.errors) {
const path = error.instanceLocation.replace("#", `plugins/${i}/uses/${j}/with`);
const value = doc?.getIn(path.split("/").filter((o) => o));
errors.push({
path,
message: error.error,
value,
type: 0,
schema: configSchema,
});
}
}
}
}
}
return errors;
}

export default async function handlePushEvent(context: GitHubContext<"push">) {
const { payload } = context;
const { repository, commits, after } = payload;

const didConfigurationFileChange = commits.some((commit) => commit.modified?.includes(CONFIG_FULL_PATH) || commit.added?.includes(CONFIG_FULL_PATH));

if (!didConfigurationFileChange || !repository.owner) {
return;
}

console.log("Configuration file changed, will run configuration checks.");

const { config, errors: configurationErrors, rawData } = await getConfigurationFromRepo(context, repository.name, repository.owner.login);
const errors: (ValueError | YAML.YAMLError)[] = [];
if (!configurationErrors && config) {
errors.push(...(await checkPluginConfigurations(context, config, rawData)));
} else if (configurationErrors) {
errors.push(...configurationErrors);
}
try {
if (errors.length) {
const body = [];
body.push(...constructErrorBody(errors, rawData, repository, after));
await createCommitComment(
context,
{
owner: repository.owner.login,
repo: repository.name,
commitSha: after,
userLogin: payload.sender?.login,
},
body
);
}
} catch (e) {
console.error("handlePushEventError", e);
}
}
Loading

0 comments on commit 02dbe86

Please sign in to comment.