Skip to content

Commit

Permalink
feat: actions sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
whilefoo committed Aug 29, 2024
1 parent c91cce2 commit c4160c6
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 10 deletions.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions 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 Down
115 changes: 115 additions & 0 deletions src/sdk/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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";
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${JSON.stringify(error.metadata, null, 2)}\n-->`,
});
}
}

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,
},
});
}
1 change: 1 addition & 0 deletions src/sdk/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ dkRj2Je2kag9b3FMxskv1npNSrPVcSc5lGNYlnZnfxIAnCknOC118JjitlrpT6wd
8wIDAQAB
-----END PUBLIC KEY-----
`;
export const KERNEL_APP_ID = 0;
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 { KERNEL_APP_ID, KERNEL_PUBLIC_KEY } from "./constants";
65 changes: 55 additions & 10 deletions src/sdk/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,32 @@ 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";

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 +45,66 @@ 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,
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${JSON.stringify(error.metadata, null, 2)}\n-->`,
});
}
}

0 comments on commit c4160c6

Please sign in to comment.