diff --git a/.cspell.json b/.cspell.json index ae85a2a..3612769 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,7 +4,7 @@ "ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log"], "useGitignore": true, "language": "en", - "words": ["dataurl", "devpool", "outdir", "servedir", "typebox"], + "words": ["dataurl", "devpool", "outdir", "servedir", "typebox", "gentlementlegen", "workerd"], "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/README.md b/README.md index 11eb0da..fdcb0dc 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ The `createActionsPlugin` function allows users to create plugins that will be a The `createPlugin` function enables users to create a plugin that will run on Cloudflare Workers environment. +### `postComment` + +The `postComment` function enables users to easily post a comment to an issue, a pull-request, or a pull request review thread. + ## Getting Started To set up the project locally, `bun` is the preferred package manager. diff --git a/bun.lockb b/bun.lockb index 148525e..dda0025 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 809df9c..671016a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ubiquity-os/plugin-sdk", - "version": "0.0.0", + "version": "1.1.1", "description": "SDK for plugin support.", "author": "Ubiquity DAO", "license": "MIT", @@ -110,7 +110,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-check-file": "^2.8.0", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-sonarjs": "^2.0.4", + "eslint-plugin-sonarjs": "^3.0.1", "husky": "^9.0.11", "jest": "^29.7.0", "jest-junit": "^16.0.0", @@ -129,7 +129,7 @@ }, "lint-staged": { "*.ts": [ - "bun prettier --write", + "prettier --write", "eslint --fix" ], "src/**.{ts,json}": [ diff --git a/src/actions.ts b/src/actions.ts index 8772c61..3ffd894 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -5,13 +5,14 @@ import { Type as T } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; import { LogReturn, Logs } from "@ubiquity-os/ubiquity-os-logger"; import { config } from "dotenv"; +import { postComment } from "./comment"; import { Context } from "./context"; import { customOctokit } from "./octokit"; import { verifySignature } from "./signature"; import { commandCallSchema } from "./types/command"; import { HandlerReturn } from "./types/sdk"; import { jsonType } from "./types/util"; -import { getPluginOptions, Options, sanitizeMetadata } from "./util"; +import { getPluginOptions, Options } from "./util"; config(); @@ -120,28 +121,11 @@ export async function createActionsPlugin`, - }); - } else { - context.logger.info("Cannot post error 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: HandlerReturn) { const octokit = new customOctokit({ auth: repoToken }); await octokit.rest.repos.createDispatchEvent({ diff --git a/src/comment.ts b/src/comment.ts index cf87399..33bfa6a 100644 --- a/src/comment.ts +++ b/src/comment.ts @@ -1,35 +1,106 @@ +import { LogReturn, Metadata } from "@ubiquity-os/ubiquity-os-logger"; import { Context } from "./context"; -import { LogReturn } from "@ubiquity-os/ubiquity-os-logger"; +import { PluginRuntimeInfo } from "./helpers/runtime-info"; import { sanitizeMetadata } from "./util"; -const HEADER_NAME = "Ubiquity"; +const HEADER_NAME = "UbiquityOS"; + +export interface CommentOptions { + /* + * Should the comment be posted as send within the log, without adding any sort of formatting. + */ + raw?: boolean; + /* + * Should the previously posted comment be reused instead of posting a new comment. + */ + updateComment?: boolean; +} + +export type PostComment = { + (context: Context, message: LogReturn | Error, options?: CommentOptions): Promise; + lastCommentId?: number; +}; /** * Posts a comment on a GitHub issue if the issue exists in the context payload, embedding structured metadata to it. */ -export async function postComment(context: Context, message: LogReturn) { - if ("issue" in context.payload && context.payload.repository?.owner?.login) { - const metadata = createStructuredMetadata(message.metadata?.name, message); - await context.octokit.rest.issues.createComment({ - owner: context.payload.repository.owner.login, - repo: context.payload.repository.name, - issue_number: context.payload.issue.number, - body: [message.logMessage.diff, metadata].join("\n"), - }); +export const postComment: PostComment = async function ( + context: Context, + message: LogReturn | Error, + options: CommentOptions = { updateComment: true, raw: false } +) { + let issueNumber; + + if ("issue" in context.payload) { + issueNumber = context.payload.issue.number; + } else if ("pull_request" in context.payload) { + issueNumber = context.payload.pull_request.number; + } else if ("discussion" in context.payload) { + issueNumber = context.payload.discussion.number; } else { - context.logger.info("Cannot post comment because issue is not found in the payload"); + context.logger.info("Cannot post comment because issue is not found in the payload."); + return; } -} -function createStructuredMetadata(className: string | undefined, logReturn: LogReturn) { - const logMessage = logReturn.logMessage; - const metadata = logReturn.metadata; + if ("repository" in context.payload && context.payload.repository?.owner?.login) { + const body = await createStructuredMetadataWithMessage(context, message, options); + if (options.updateComment && postComment.lastCommentId) { + await context.octokit.rest.issues.updateComment({ + owner: context.payload.repository.owner.login, + repo: context.payload.repository.name, + comment_id: postComment.lastCommentId, + body: body, + }); + } else { + const commentData = await context.octokit.rest.issues.createComment({ + owner: context.payload.repository.owner.login, + repo: context.payload.repository.name, + issue_number: issueNumber, + body: body, + }); + postComment.lastCommentId = commentData.data.id; + } + } else { + context.logger.info("Cannot post comment because repository is not found in the payload.", { payload: context.payload }); + } +}; + +async function createStructuredMetadataWithMessage(context: Context, message: LogReturn | Error, options: CommentOptions) { + let logMessage; + let callingFnName; + let instigatorName; + let metadata: Metadata; + if (message instanceof Error) { + metadata = { + message: message.message, + name: message.name, + stack: message.stack, + }; + callingFnName = message.stack?.split("\n")[2]?.match(/at (\S+)/)?.[1] ?? "anonymous"; + logMessage = context.logger.error(message.message).logMessage; + } else if (message.metadata) { + metadata = { + message: message.metadata.message, + stack: message.metadata.stack || message.metadata.error?.stack, + caller: message.metadata.caller || message.metadata.error?.stack?.split("\n")[2]?.match(/at (\S+)/)?.[1], + }; + logMessage = message.logMessage; + callingFnName = metadata.caller; + } else { + metadata = { ...message }; + } const jsonPretty = sanitizeMetadata(metadata); - const stack = logReturn.metadata?.stack; - const stackLine = (Array.isArray(stack) ? stack.join("\n") : stack)?.split("\n")[2] ?? ""; - const caller = stackLine.match(/at (\S+)/)?.[1] ?? ""; - const ubiquityMetadataHeader = ` `, }); }); it("Should accept correct request", async () => { - const inputs = await getWorkerInputs("stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, { shouldFail: false }, "test", "main", { - name: "test", - parameters: { param1: "test" }, - }); + const inputs = await getWorkerInputs( + "stateId", + issueCommentedEvent.eventName, + issueCommentedEvent.eventPayload, + { shouldFail: false }, + "test", + "http://localhost:4000", + { + name: "test", + parameters: { param1: "test" }, + } + ); const res = await app.request("/", { headers: { @@ -250,12 +270,14 @@ describe("SDK actions tests", () => { parameters: { param1: "test" }, }); jest.unstable_mockModule(githubActionImportPath, () => ({ + default: {}, context: { runId: "1", payload: { inputs: githubInputs, }, repo: repo, + sha: "1234", }, })); const setOutput = jest.fn(); @@ -308,6 +330,7 @@ describe("SDK actions tests", () => { const githubInputs = await getWorkflowInputs("stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, {}, "test_token", "main", null); jest.unstable_mockModule("@actions/github", () => ({ + default: {}, context: { runId: "1", payload: { @@ -344,6 +367,7 @@ describe("SDK actions tests", () => { const githubInputs = await getWorkflowInputs("stateId", issueCommentedEvent.eventName, issueCommentedEvent.eventPayload, {}, "test_token", "main", null); jest.unstable_mockModule(githubActionImportPath, () => ({ + default: {}, context: { runId: "1", payload: {