Skip to content

Commit

Permalink
Merge pull request #114 from ubiquity/development
Browse files Browse the repository at this point in the history
Merge development into main
  • Loading branch information
gentlementlegen authored Sep 25, 2024
2 parents 7f1a0a1 + d2c75cd commit ff45960
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 95 deletions.
49 changes: 49 additions & 0 deletions .github/workflows/sync-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Sync branch to template

on:
workflow_dispatch:
schedule:
- cron: '14 0 1 * *'

jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Get GitHub App token
uses: tibdex/[email protected]
id: get_installation_token
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Sync branch to template
env:
GH_TOKEN: ${{ steps.get_installation_token.outputs.token }}
IGNORE_FILES: "README.md another-file.txt"
run: |
branch_name=$(git rev-parse --abbrev-ref HEAD)
original_remote=$(git remote get-url origin)
pr_branch="sync-template/${branch_name}"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git checkout -b "$pr_branch"
git clone https://github.com/ubiquity/ts-template
for file in $IGNORE_FILES; do
rm -rf "ts-template/$file"
done
cp -rf ts-template/* .
rm -rf ts-template/
git add .
git commit -m "chore: sync template"
git push "$original_remote" "$pr_branch"
gh pr create --title "Sync branch to template" --body "This pull request merges changes from the template repository." --head "$pr_branch" --base "$branch_name"
Binary file modified bun.lockb
Binary file not shown.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
"open-source"
],
"dependencies": {
"@actions/core": "1.10.1",
"@actions/github": "6.0.0",
"@octokit/auth-app": "7.1.0",
"@octokit/core": "6.1.2",
"@octokit/plugin-paginate-rest": "11.3.3",
Expand All @@ -58,7 +60,7 @@
"@octokit/webhooks": "13.2.8",
"@octokit/webhooks-types": "7.5.1",
"@sinclair/typebox": "0.32.35",
"@ubiquity-dao/ubiquibot-logger": "1.3.0",
"@ubiquity-dao/ubiquibot-logger": "^1.3.1",
"dotenv": "16.4.5",
"hono": "4.4.13",
"smee-client": "2.0.1",
Expand Down
6 changes: 3 additions & 3 deletions src/github/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export function bindHandlers(eventHandler: GitHubEventHandler) {
eventHandler.onAny(tryCatchWrapper((event) => handleEvent(event, eventHandler))); // onAny should also receive GithubContext but the types in octokit/webhooks are weird
}

async function shouldSkipPlugin(event: EmitterWebhookEvent, context: GitHubContext, pluginChain: PluginConfiguration["plugins"][0]) {
if (pluginChain.skipBotEvents && "sender" in event.payload && event.payload.sender?.type === "Bot") {
export async function shouldSkipPlugin(context: GitHubContext, pluginChain: PluginConfiguration["plugins"][0]) {
if (pluginChain.skipBotEvents && "sender" in context.payload && context.payload.sender?.type === "Bot") {
console.log("Skipping plugin chain because sender is a bot");
return true;
}
Expand Down Expand Up @@ -68,7 +68,7 @@ async function handleEvent(event: EmitterWebhookEvent, eventHandler: InstanceTyp
}

for (const pluginChain of pluginChains) {
if (await shouldSkipPlugin(event, context, pluginChain)) {
if (await shouldSkipPlugin(context, pluginChain)) {
continue;
}

Expand Down
119 changes: 119 additions & 0 deletions src/sdk/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as core from "@actions/core";
import * as github from "@actions/github";
import { Context } from "./context";
import { customOctokit } from "./octokit";
import { EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { Logs, LogLevel, LOG_LEVEL, LogReturn } from "@ubiquity-dao/ubiquibot-logger";
import { config } from "dotenv";
import { Type as T, TAnySchema } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
import { sanitizeMetadata } from "./util";

config();

interface Options {
logLevel?: LogLevel;
postCommentOnError?: boolean;
settingsSchema?: TAnySchema;
envSchema?: TAnySchema;
}

const inputSchema = T.Object({
stateId: T.String(),
eventName: T.String(),
eventPayload: T.String(),
authToken: T.String(),
settings: T.String(),
ref: T.String(),
});

export async function createActionsPlugin<TConfig = unknown, TEnv = unknown, TSupportedEvents extends WebhookEventName = WebhookEventName>(
handler: (context: Context<TConfig, TEnv, TSupportedEvents>) => Promise<Record<string, unknown> | undefined>,
options?: Options
) {
const pluginOptions = {
logLevel: options?.logLevel || LOG_LEVEL.INFO,
postCommentOnError: options?.postCommentOnError || true,
settingsSchema: options?.settingsSchema,
envSchema: options?.envSchema,
};

const inputs = Value.Decode(inputSchema, github.context.payload.inputs);

let config: TConfig;
if (pluginOptions.settingsSchema) {
config = Value.Decode(pluginOptions.settingsSchema, JSON.parse(inputs.settings));
} else {
config = JSON.parse(inputs.settings) as TConfig;
}

let env: TEnv;
if (pluginOptions.envSchema) {
env = Value.Decode(pluginOptions.envSchema, process.env);
} else {
env = process.env as TEnv;
}

const context: Context<TConfig, TEnv, TSupportedEvents> = {
eventName: inputs.eventName as TSupportedEvents,
payload: JSON.parse(inputs.eventPayload),
octokit: new customOctokit({ auth: inputs.authToken }),
config: config,
env: env,
logger: new Logs(pluginOptions.logLevel),
};

try {
const result = await handler(context);
core.setOutput("result", result);
await returnDataToKernel(inputs.authToken, inputs.stateId, result);
} catch (error) {
console.error(error);

let loggerError: LogReturn | null;
if (error instanceof Error) {
core.setFailed(error);
loggerError = context.logger.error(`Error: ${error}`, { error: error });
} else if (error instanceof LogReturn) {
core.setFailed(error.logMessage.raw);
loggerError = error;
} else {
core.setFailed(`Error: ${error}`);
loggerError = context.logger.error(`Error: ${error}`);
}

if (pluginOptions.postCommentOnError && loggerError) {
await postComment(context, loggerError);
}
}
}

async function postComment(context: Context, error: LogReturn) {
if ("issue" in context.payload && context.payload.repository?.owner?.login) {
await context.octokit.rest.issues.createComment({
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
issue_number: context.payload.issue.number,
body: `${error.logMessage.diff}\n<!--\n${getGithubWorkflowRunUrl()}\n${sanitizeMetadata(error.metadata)}\n-->`,
});
} else {
context.logger.info("Cannot post comment because issue is not found in the payload");
}
}

function getGithubWorkflowRunUrl() {
return `${github.context.payload.repository?.html_url}/actions/runs/${github.context.runId}`;
}

async function returnDataToKernel(repoToken: string, stateId: string, output: object | undefined) {
const octokit = new customOctokit({ auth: repoToken });
await octokit.rest.repos.createDispatchEvent({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
event_type: "return_data_to_ubiquibot_kernel",
client_payload: {
state_id: stateId,
output: output ? JSON.stringify(output) : null,
},
});
}
2 changes: 2 additions & 0 deletions src/sdk/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ dkRj2Je2kag9b3FMxskv1npNSrPVcSc5lGNYlnZnfxIAnCknOC118JjitlrpT6wd
8wIDAQAB
-----END PUBLIC KEY-----
`;
export const KERNEL_APP_ID = 975031;
export const BOT_USER_ID = 178941584;
2 changes: 2 additions & 0 deletions src/sdk/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { createPlugin } from "./server";
export { createActionsPlugin } from "./actions";
export type { Context } from "./context";
export * from "./constants";
70 changes: 59 additions & 11 deletions src/sdk/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,33 @@ import { customOctokit } from "./octokit";
import { EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { verifySignature } from "./signature";
import { KERNEL_PUBLIC_KEY } from "./constants";
import { Logs, LogLevel, LOG_LEVEL } from "@ubiquity-dao/ubiquibot-logger";
import { Logs, LogLevel, LOG_LEVEL, LogReturn } from "@ubiquity-dao/ubiquibot-logger";
import { Manifest } from "../types/manifest";
import { TAnySchema } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
import { sanitizeMetadata } from "./util";

interface Options {
kernelPublicKey?: string;
logLevel?: LogLevel;
postCommentOnError?: boolean;
settingsSchema?: TAnySchema;
envSchema?: TAnySchema;
}

export async function createPlugin<TConfig = unknown, TEnv = unknown, TSupportedEvents extends WebhookEventName = WebhookEventName>(
handler: (context: Context<TConfig, TEnv, TSupportedEvents>) => Promise<Record<string, unknown> | undefined>,
manifest: Manifest,
options?: Options
) {
const pluginOptions = {
kernelPublicKey: options?.kernelPublicKey || KERNEL_PUBLIC_KEY,
logLevel: options?.logLevel || LOG_LEVEL.INFO,
postCommentOnError: options?.postCommentOnError || true,
settingsSchema: options?.settingsSchema,
envSchema: options?.envSchema,
};

const app = new Hono();

app.get("/manifest.json", (ctx) => {
Expand All @@ -32,34 +46,68 @@ export async function createPlugin<TConfig = unknown, TEnv = unknown, TSupported
const payload = await ctx.req.json();
const signature = payload.signature;
delete payload.signature;
if (!(await verifySignature(options?.kernelPublicKey || KERNEL_PUBLIC_KEY, payload, signature))) {
if (!(await verifySignature(pluginOptions.kernelPublicKey, payload, signature))) {
throw new HTTPException(400, { message: "Invalid signature" });
}

try {
new customOctokit({ auth: payload.authToken });
} catch (error) {
console.error("SDK ERROR", error);
throw new HTTPException(500, { message: "Unexpected error" });
let config: TConfig;
if (pluginOptions.settingsSchema) {
config = Value.Decode(pluginOptions.settingsSchema, payload.settings);
} else {
config = payload.settings as TConfig;
}

let env: TEnv;
if (pluginOptions.envSchema) {
env = Value.Decode(pluginOptions.envSchema, process.env);
} else {
env = process.env as TEnv;
}

const context: Context<TConfig, TEnv, TSupportedEvents> = {
eventName: payload.eventName,
payload: payload.payload,
payload: payload.eventPayload,
octokit: new customOctokit({ auth: payload.authToken }),
config: payload.settings as TConfig,
env: ctx.env as TEnv,
logger: new Logs(options?.logLevel || LOG_LEVEL.INFO),
config: config,
env: env,
logger: new Logs(pluginOptions.logLevel),
};

try {
const result = await handler(context);
return ctx.json({ stateId: payload.stateId, output: result });
} catch (error) {
console.error(error);

let loggerError: LogReturn | null;
if (error instanceof Error) {
loggerError = context.logger.error(`Error: ${error}`, { error: error });
} else if (error instanceof LogReturn) {
loggerError = error;
} else {
loggerError = context.logger.error(`Error: ${error}`);
}

if (pluginOptions.postCommentOnError && loggerError) {
await postComment(context, loggerError);
}

throw new HTTPException(500, { message: "Unexpected error" });
}
});

return app;
}

async function postComment(context: Context, error: LogReturn) {
if ("issue" in context.payload && context.payload.repository?.owner?.login) {
await context.octokit.rest.issues.createComment({
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
issue_number: context.payload.issue.number,
body: `${error.logMessage.diff}\n<!--\n${sanitizeMetadata(error.metadata)}\n-->`,
});
} else {
context.logger.info("Cannot post comment because issue is not found in the payload");
}
}
5 changes: 5 additions & 0 deletions src/sdk/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LogReturn } from "@ubiquity-dao/ubiquibot-logger";

export function sanitizeMetadata(obj: LogReturn["metadata"]): string {
return JSON.stringify(obj, null, 2).replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/--/g, "&#45;&#45;");
}
Loading

0 comments on commit ff45960

Please sign in to comment.