Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SDK and command interface #86

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"version": "0.2",
"ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log", "./src/adapters/supabase/**/**.ts"],
"ignorePaths": ["**/*.json", "**/*.css", "bun.lockb", "tests/http/**", "node_modules", "**/*.log", "./src/adapters/supabase/**/**.ts"],
"useGitignore": true,
"language": "en",
"words": [
Expand Down
2 changes: 1 addition & 1 deletion .github/knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const config: KnipConfig = {
ignore: ["src/types/config.ts", "**/__mocks__/**", "**/__fixtures__/**", "src/worker.ts"],
ignoreExportsUsedInFile: true,
// eslint can also be safely ignored as per the docs: https://knip.dev/guides/handling-issues#eslint--jest
ignoreDependencies: ["eslint-config-prettier", "eslint-plugin-prettier", "@types/jest"],
ignoreDependencies: ["eslint-config-prettier", "eslint-plugin-prettier", "ts-node", "@octokit/rest"],
eslint: true,
};

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ To stop a task, a hunter should use the `/stop` command. This will unassign them

#### Note: The command name is `"start"` when configuring your `.ubiquity-os.config.yml` file.

To configure your Ubiquibot to run this plugin, add the following to the `.ubiquity-os.config.yml` file in your organization configuration repository.
To configure your Ubiquity Kernel to run this plugin, add the following to the `.ubiquity-os.config.yml` file in your organization configuration repository.

```yml
- plugin: http://localhost:4000 # or the URL where the plugin is hosted
Expand Down
10 changes: 0 additions & 10 deletions jest.config.json

This file was deleted.

27 changes: 27 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Config } from "jest";

const cfg: Config = {
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
useESM: true,
},
],
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
coveragePathIgnorePatterns: ["node_modules", "mocks"],
collectCoverage: true,
coverageReporters: ["json", "lcov", "text", "clover", "json-summary"],
reporters: ["default", "jest-junit", "jest-md-dashboard"],
coverageDirectory: "coverage",
testTimeout: 20000,
roots: ["<rootDir>", "tests"],
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1",
},
setupFilesAfterEnv: ["dotenv/config"],
};

export default cfg;
40 changes: 23 additions & 17 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
{
"name": "Start | Stop",
"description": "Assign or un-assign yourself from an issue.",
"ubiquity:listeners": [
"issue_comment.created",
"issues.assigned",
"issues.unassigned",
"pull_request.opened",
"pull_request.edited"
],
"description": "Assign or un-assign yourself from an issue/task.",
"ubiquity:listeners": ["issue_comment.created", "issues.assigned", "issues.unassigned", "pull_request.opened", "pull_request.edited"],
"commands": {
"start": {
"ubiquity:example": "/start",
"description": "Assign yourself to the issue."
"description": "Assign yourself and/or others to the issue/task.",
"parameters": {
"type": "object",
"properties": {
"teammates": {
"description": "Users other than yourself to assign to the issue",
"type": "array",
"items": {
"description": "Github username",
"type": "string"
}
}
}
}
},
"stop": {
"ubiquity:example": "/stop",
"description": "Unassign yourself from the issue."
"description": "Unassign yourself from the issue/task.",
"parameters": {
"type": "object",
"properties": {}
}
}
},
"configuration": {
Expand Down Expand Up @@ -68,12 +79,7 @@
"type": "string"
},
"rolesWithReviewAuthority": {
"default": [
"COLLABORATOR",
"OWNER",
"MEMBER",
"ADMIN"
],
"default": ["COLLABORATOR", "OWNER", "MEMBER", "ADMIN"],
"type": "array",
"items": {
"type": "string"
Expand All @@ -98,4 +104,4 @@
"requiredLabelsToStart"
]
}
}
}
32 changes: 16 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,31 +28,30 @@
],
"dependencies": {
"@octokit/graphql-schema": "15.25.0",
"@octokit/plugin-paginate-graphql": "5.2.2",
"@octokit/rest": "20.1.1",
"@octokit/plugin-rest-endpoint-methods": "^13.2.6",
"@octokit/types": "^13.6.1",
"@octokit/webhooks": "13.2.7",
"@sinclair/typebox": "^0.32.5",
"@sinclair/typebox": "0.34.3",
"@supabase/supabase-js": "2.42.0",
"@ubiquity-os/plugin-sdk": "^1.1.0",
"@ubiquity-os/ubiquity-os-logger": "^1.3.2",
"dotenv": "^16.4.4",
"ms": "^2.1.3",
"typebox-validators": "^0.3.5"
"ms": "^2.1.3"
},
"devDependencies": {
"@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",
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@cspell/dict-node": "^5.0.5",
"@cspell/dict-software-terms": "^4.1.15",
"@cspell/dict-typescript": "^3.1.2",
"@eslint/js": "9.14.0",
"@jest/globals": "29.7.0",
"@mswjs/data": "0.16.1",
"@octokit/rest": "20.1.1",
"@types/jest": "29.5.12",
"@types/ms": "^0.7.34",
"@types/node": "20.14.5",
"cspell": "8.9.0",
"eslint": "9.5.0",
"eslint": "9.14.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-check-file": "2.8.0",
"eslint-plugin-prettier": "5.1.3",
Expand All @@ -66,10 +65,11 @@
"npm-run-all": "4.1.5",
"prettier": "3.3.2",
"ts-jest": "29.1.5",
"ts-node": "^10.9.2",
"tsx": "4.15.6",
"typescript": "5.6.2",
"typescript-eslint": "7.13.1",
"wrangler": "^3.79.0"
"typescript-eslint": "8.14.0",
"wrangler": "^3.87.0"
},
"lint-staged": {
"*.ts": [
Expand All @@ -85,5 +85,5 @@
"@commitlint/config-conventional"
]
},
"packageManager": "yarn@4.2.2"
"packageManager": "yarn@1.22.22"
}
3 changes: 2 additions & 1 deletion src/handlers/result-types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export enum HttpStatusCode {
OK = 200,
NOT_MODIFIED = 304,
BAD_REQUEST = 400,
}

export interface Result {
export interface Result extends Record<string, unknown> {
status: HttpStatusCode;
content?: string;
reason?: string;
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/shared/check-assignments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function getUserStopComments(context: Context, username: string): Promise<
const { owner, repo } = getOwnerRepoFromHtmlUrl(html_url);

try {
const comments = await octokit.paginate(octokit.issues.listComments, {
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
owner,
repo,
issue_number: number,
Expand Down Expand Up @@ -60,7 +60,7 @@ async function getAssignmentEvents(context: Context) {
}
const { repository, issue } = context.payload;
try {
const data = await context.octokit.paginate(context.octokit.issues.listEventsForTimeline, {
const data = await context.octokit.paginate(context.octokit.rest.issues.listEventsForTimeline, {
owner: repository.owner.login,
repo: repository.name,
issue_number: issue.number,
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/shared/get-user-task-limit-and-role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function getUserRoleAndTaskLimit(context: Context, user: string): P
throw new Error("Invalid organization name");
}

const response = await context.octokit.orgs.getMembershipForUser({
const response = await context.octokit.rest.orgs.getMembershipForUser({
org: orgLogin,
username: user,
});
Expand Down
19 changes: 19 additions & 0 deletions src/handlers/user-start-stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ import { getDeadline } from "./shared/generate-assignment-comment";
import { start } from "./shared/start";
import { stop } from "./shared/stop";

export async function commandHandler(context: Context): Promise<Result> {
if (!isIssueCommentEvent(context)) {
return { status: HttpStatusCode.NOT_MODIFIED };
}
if (!context.command) {
return { status: HttpStatusCode.NOT_MODIFIED };
}
const { issue, sender, repository } = context.payload;

if (context.command.name === "stop") {
return await stop(context, issue, sender, repository);
} else if (context.command.name === "start") {
const teammates = context.command.parameters.teammates ?? [];
return await start(context, issue, sender, teammates);
} else {
return { status: HttpStatusCode.BAD_REQUEST };
}
}

export async function userStartStop(context: Context): Promise<Result> {
if (!isIssueCommentEvent(context)) {
return { status: HttpStatusCode.NOT_MODIFIED };
Expand Down
80 changes: 26 additions & 54 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,32 @@
import { paginateGraphQL } from "@octokit/plugin-paginate-graphql";
import { Octokit } from "@octokit/rest";
import { createClient } from "@supabase/supabase-js";
import { LogReturn, Logs } from "@ubiquity-os/ubiquity-os-logger";
import { createAdapters } from "./adapters";
import { userPullRequest, userSelfAssign, userStartStop, userUnassigned } from "./handlers/user-start-stop";
import { Context, Env, PluginInputs } from "./types";
import { addCommentToIssue } from "./utils/issue";
import { commandHandler, userPullRequest, userSelfAssign, userStartStop, userUnassigned } from "./handlers/user-start-stop";
import { Context } from "./types";
import { listOrganizations } from "./utils/list-organizations";
import { HttpStatusCode } from "./handlers/result-types";
import { createAdapters } from "./adapters";
import { createClient } from "@supabase/supabase-js";

export async function startStopTask(inputs: PluginInputs, env: Env) {
const customOctokit = Octokit.plugin(paginateGraphQL);
const octokit = new customOctokit({ auth: inputs.authToken });
const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_KEY);

const context: Context = {
eventName: inputs.eventName,
payload: inputs.eventPayload,
config: inputs.settings,
organizations: [],
octokit,
env,
logger: new Logs("info"),
adapters: {} as ReturnType<typeof createAdapters>,
};

context.adapters = createAdapters(supabase, context);

try {
const organizations = await listOrganizations(context);
context.organizations = organizations;
export async function startStopTask(context: Context) {
context.adapters = createAdapters(createClient(context.env.SUPABASE_URL, context.env.SUPABASE_KEY), context as Context);
const organizations = await listOrganizations(context);
context.organizations = organizations;

switch (context.eventName) {
case "issue_comment.created":
return await userStartStop(context);
case "issues.assigned":
return await userSelfAssign(context as Context<"issues.assigned">);
case "pull_request.opened":
return await userPullRequest(context as Context<"pull_request.opened">);
case "pull_request.edited":
return await userPullRequest(context as Context<"pull_request.edited">);
case "issues.unassigned":
return await userUnassigned(context as Context<"issues.unassigned">);
default:
context.logger.error(`Unsupported event: ${context.eventName}`);
}
} catch (err) {
let errorMessage;
if (err instanceof LogReturn) {
errorMessage = err;
await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n<!--\n${sanitizeMetadata(errorMessage?.metadata)}\n-->`);
} else {
context.logger.error("An error occurred", { err });
}
if (context.command) {
return await commandHandler(context);
}
}

function sanitizeMetadata(obj: LogReturn["metadata"]): string {
return JSON.stringify(obj, null, 2).replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/--/g, "&#45;&#45;");
switch (context.eventName) {
case "issue_comment.created":
return await userStartStop(context as Context<"issue_comment.created">);
case "issues.assigned":
return await userSelfAssign(context as Context<"issues.assigned">);
case "pull_request.opened":
return await userPullRequest(context as Context<"pull_request.opened">);
case "pull_request.edited":
return await userPullRequest(context as Context<"pull_request.edited">);
case "issues.unassigned":
return await userUnassigned(context as Context<"issues.unassigned">);
default:
context.logger.error(`Unsupported event: ${context.eventName}`);
return { status: HttpStatusCode.BAD_REQUEST };
}
}
17 changes: 17 additions & 0 deletions src/types/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Type as T } from "@sinclair/typebox";
import { StaticDecode } from "@sinclair/typebox";

export const startCommandSchema = T.Object({
name: T.Literal("start"),
parameters: T.Object({
teammates: T.Array(T.String()),
}),
});

export const stopCommandSchema = T.Object({
name: T.Literal("stop"),
});

export const commandSchema = T.Union([startCommandSchema, stopCommandSchema]);

export type Command = StaticDecode<typeof commandSchema>;
22 changes: 5 additions & 17 deletions src/types/context.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
import { paginateGraphQLInterface } from "@octokit/plugin-paginate-graphql";
import { Octokit } from "@octokit/rest";
import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { Logs } from "@ubiquity-os/ubiquity-os-logger";
import { Context as PluginContext } from "@ubiquity-os/plugin-sdk";
import { createAdapters } from "../adapters";
import { Env } from "./env";
import { PluginSettings } from "./plugin-input";
import { Command } from "./command";

export type SupportedEventsU = "issue_comment.created" | "issues.assigned" | "pull_request.opened" | "pull_request.edited" | "issues.unassigned";

export type SupportedEvents = {
[K in SupportedEventsU]: K extends WebhookEventName ? WebhookEvent<K> : never;
};
export type SupportedEvents = "issue_comment.created" | "issues.assigned" | "pull_request.opened" | "pull_request.edited" | "issues.unassigned";

export function isIssueCommentEvent(context: Context): context is Context<"issue_comment.created"> {
return "issue" in context.payload;
}

export interface Context<T extends SupportedEventsU = SupportedEventsU, TU extends SupportedEvents[T] = SupportedEvents[T]> {
eventName: T;
payload: TU["payload"];
octokit: InstanceType<typeof Octokit> & paginateGraphQLInterface;
export type Context<TEvents extends SupportedEvents = SupportedEvents> = PluginContext<PluginSettings, Env, Command, TEvents> & {
adapters: ReturnType<typeof createAdapters>;
config: PluginSettings;
organizations: string[];
env: Env;
logger: Logs;
}
};
Loading