Skip to content

Commit

Permalink
fix!: postComment is now wrapped inside a class to avoid instance col…
Browse files Browse the repository at this point in the history
…lisions
  • Loading branch information
gentlementlegen committed Feb 6, 2025
1 parent 7a18787 commit da526ff
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 156 deletions.
5 changes: 3 additions & 2 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { Value } from "@sinclair/typebox/value";
import { LogReturn, Logs } from "@ubiquity-os/ubiquity-os-logger";
import { config } from "dotenv";
import { postComment } from "./comment";
import { CommentHandler } from "./comment";
import { Context } from "./context";
import { customOctokit } from "./octokit";
import { verifySignature } from "./signature";
Expand Down Expand Up @@ -86,6 +86,7 @@ export async function createActionsPlugin<TConfig = unknown, TEnv = unknown, TCo
config: config,
env: env,
logger: new Logs(pluginOptions.logLevel),
commentHandler: new CommentHandler(),
};

try {
Expand All @@ -108,7 +109,7 @@ export async function createActionsPlugin<TConfig = unknown, TEnv = unknown, TCo
}

if (pluginOptions.postCommentOnError && loggerError) {
await postComment(context, loggerError);
await context.commentHandler.postComment(context, loggerError);
}
}
}
Expand Down
286 changes: 144 additions & 142 deletions src/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import { Context } from "./context";
import { PluginRuntimeInfo } from "./helpers/runtime-info";
import { sanitizeMetadata } from "./util";

const HEADER_NAME = "UbiquityOS";
const lastCommentId = { reviewCommentId: null as number | null, issueCommentId: null as number | null };

export interface CommentOptions {
/*
* Should the comment be posted as send within the log, without adding any sort of formatting.
Expand All @@ -18,7 +15,7 @@ export interface CommentOptions {
updateComment?: boolean;
}

type PostedGithubComment =
export type PostedGithubComment =
| RestEndpointMethodTypes["issues"]["updateComment"]["response"]["data"]
| RestEndpointMethodTypes["issues"]["createComment"]["response"]["data"]
| RestEndpointMethodTypes["pulls"]["createReplyForReviewComment"]["response"]["data"];
Expand All @@ -34,177 +31,182 @@ interface IssueContext {
repo: string;
}

async function updateIssueComment(
context: Context,
params: { owner: string; repo: string; body: string; issueNumber: number }
): Promise<WithIssueNumber<PostedGithubComment>> {
if (!lastCommentId.issueCommentId) {
throw context.logger.error("issueCommentId is missing");
}
const commentData = await context.octokit.rest.issues.updateComment({
owner: params.owner,
repo: params.repo,
comment_id: lastCommentId.issueCommentId,
body: params.body,
});
return { ...commentData.data, issueNumber: params.issueNumber };
}
export class CommentHandler {
public static readonly HEADER_NAME = "UbiquityOS";
private _lastCommentId = { reviewCommentId: null as number | null, issueCommentId: null as number | null };

async function updateReviewComment(
context: Context,
params: { owner: string; repo: string; body: string; issueNumber: number }
): Promise<WithIssueNumber<PostedGithubComment>> {
if (!lastCommentId.reviewCommentId) {
throw context.logger.error("reviewCommentId is missing");
async _updateIssueComment(
context: Context,
params: { owner: string; repo: string; body: string; issueNumber: number }
): Promise<WithIssueNumber<PostedGithubComment>> {
if (!this._lastCommentId.issueCommentId) {
throw context.logger.error("issueCommentId is missing");
}
const commentData = await context.octokit.rest.issues.updateComment({
owner: params.owner,
repo: params.repo,
comment_id: this._lastCommentId.issueCommentId,
body: params.body,
});
return { ...commentData.data, issueNumber: params.issueNumber };
}
const commentData = await context.octokit.rest.pulls.updateReviewComment({
owner: params.owner,
repo: params.repo,
comment_id: lastCommentId.reviewCommentId,
body: params.body,
});
return { ...commentData.data, issueNumber: params.issueNumber };
}

async function createNewComment(
context: Context,
params: { owner: string; repo: string; body: string; issueNumber: number; commentId?: number }
): Promise<WithIssueNumber<PostedGithubComment>> {
if (params.commentId) {
const commentData = await context.octokit.rest.pulls.createReplyForReviewComment({
async _updateReviewComment(
context: Context,
params: { owner: string; repo: string; body: string; issueNumber: number }
): Promise<WithIssueNumber<PostedGithubComment>> {
if (!this._lastCommentId.reviewCommentId) {
throw context.logger.error("reviewCommentId is missing");
}
const commentData = await context.octokit.rest.pulls.updateReviewComment({
owner: params.owner,
repo: params.repo,
pull_number: params.issueNumber,
comment_id: params.commentId,
comment_id: this._lastCommentId.reviewCommentId,
body: params.body,
});
lastCommentId.reviewCommentId = commentData.data.id;
return { ...commentData.data, issueNumber: params.issueNumber };
}

const commentData = await context.octokit.rest.issues.createComment({
owner: params.owner,
repo: params.repo,
issue_number: params.issueNumber,
body: params.body,
});
lastCommentId.issueCommentId = commentData.data.id;
return { ...commentData.data, issueNumber: params.issueNumber };
}
async _createNewComment(
context: Context,
params: { owner: string; repo: string; body: string; issueNumber: number; commentId?: number }
): Promise<WithIssueNumber<PostedGithubComment>> {
if (params.commentId) {
const commentData = await context.octokit.rest.pulls.createReplyForReviewComment({
owner: params.owner,
repo: params.repo,
pull_number: params.issueNumber,
comment_id: params.commentId,
body: params.body,
});
this._lastCommentId.reviewCommentId = commentData.data.id;
return { ...commentData.data, issueNumber: params.issueNumber };
}

function getIssueNumber(context: Context): number | undefined {
if ("issue" in context.payload) return context.payload.issue.number;
if ("pull_request" in context.payload) return context.payload.pull_request.number;
if ("discussion" in context.payload) return context.payload.discussion.number;
return undefined;
}
const commentData = await context.octokit.rest.issues.createComment({
owner: params.owner,
repo: params.repo,
issue_number: params.issueNumber,
body: params.body,
});
this._lastCommentId.issueCommentId = commentData.data.id;
return { ...commentData.data, issueNumber: params.issueNumber };
}

function getCommentId(context: Context): number | undefined {
return "pull_request" in context.payload && "comment" in context.payload ? context.payload.comment.id : undefined;
}
_getIssueNumber(context: Context): number | undefined {
if ("issue" in context.payload) return context.payload.issue.number;
if ("pull_request" in context.payload) return context.payload.pull_request.number;
if ("discussion" in context.payload) return context.payload.discussion.number;
return undefined;
}

function extractIssueContext(context: Context): IssueContext | null {
if (!("repository" in context.payload) || !context.payload.repository?.owner?.login) {
return null;
_getCommentId(context: Context): number | undefined {
return "pull_request" in context.payload && "comment" in context.payload ? context.payload.comment.id : undefined;
}

const issueNumber = getIssueNumber(context);
if (!issueNumber) return null;
_extractIssueContext(context: Context): IssueContext | null {
if (!("repository" in context.payload) || !context.payload.repository?.owner?.login) {
return null;
}

return {
issueNumber,
commentId: getCommentId(context),
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
};
}
const issueNumber = this._getIssueNumber(context);
if (!issueNumber) return null;

async function processMessage(context: Context, message: LogReturn | Error) {
if (message instanceof Error) {
const metadata = {
message: message.message,
name: message.name,
stack: message.stack,
return {
issueNumber,
commentId: this._getCommentId(context),
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
};
return { metadata, logMessage: context.logger.error(message.message).logMessage };
}

const metadata = message.metadata
? {
...message.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],
}
: { ...message };
async _processMessage(context: Context, message: LogReturn | Error) {
if (message instanceof Error) {
const metadata = {
message: message.message,
name: message.name,
stack: message.stack,
};
return { metadata, logMessage: context.logger.error(message.message).logMessage };
}

return { metadata, logMessage: message.logMessage };
}
const metadata = message.metadata
? {
...message.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],
}
: { ...message };

function getInstigatorName(context: Context): string {
if (
"installation" in context.payload &&
context.payload.installation &&
"account" in context.payload.installation &&
context.payload.installation?.account?.name
) {
return context.payload.installation?.account?.name;
return { metadata, logMessage: message.logMessage };
}
return context.payload.sender?.login || HEADER_NAME;
}

async function createMetadataContent(context: Context, metadata: Metadata) {
const jsonPretty = sanitizeMetadata(metadata);
const instigatorName = getInstigatorName(context);
const runUrl = PluginRuntimeInfo.getInstance().runUrl;
const version = await PluginRuntimeInfo.getInstance().version;
const callingFnName = metadata.caller || "anonymous";

return {
header: `<!-- ${HEADER_NAME} - ${callingFnName} - ${version} - @${instigatorName} - ${runUrl}`,
jsonPretty,
};
}

function formatMetadataContent(logMessage: LogReturn["logMessage"], header: string, jsonPretty: string): string {
const metadataVisible = ["```json", jsonPretty, "```"].join("\n");
const metadataHidden = [header, jsonPretty, "-->"].join("\n");
_getInstigatorName(context: Context): string {
if (
"installation" in context.payload &&
context.payload.installation &&
"account" in context.payload.installation &&
context.payload.installation?.account?.name
) {
return context.payload.installation?.account?.name;
}
return context.payload.sender?.login || CommentHandler.HEADER_NAME;
}

return logMessage?.type === "fatal" ? [metadataVisible, metadataHidden].join("\n") : metadataHidden;
}
async _createMetadataContent(context: Context, metadata: Metadata) {
const jsonPretty = sanitizeMetadata(metadata);
const instigatorName = this._getInstigatorName(context);
const runUrl = PluginRuntimeInfo.getInstance().runUrl;
const version = await PluginRuntimeInfo.getInstance().version;
const callingFnName = metadata.caller || "anonymous";

async function createCommentBody(context: Context, message: LogReturn | Error, options: CommentOptions): Promise<string> {
const { metadata, logMessage } = await processMessage(context, message);
const { header, jsonPretty } = await createMetadataContent(context, metadata);
const metadataContent = formatMetadataContent(logMessage, header, jsonPretty);
return {
header: `<!-- ${CommentHandler.HEADER_NAME} - ${callingFnName} - ${version} - @${instigatorName} - ${runUrl}`,
jsonPretty,
};
}

return `${options.raw ? logMessage?.raw : logMessage?.diff}\n\n${metadataContent}\n`;
}
_formatMetadataContent(logMessage: LogReturn["logMessage"], header: string, jsonPretty: string): string {
const metadataVisible = ["```json", jsonPretty, "```"].join("\n");
const metadataHidden = [header, jsonPretty, "-->"].join("\n");

export async function postComment(
context: Context,
message: LogReturn | Error,
options: CommentOptions = { updateComment: true, raw: false }
): Promise<WithIssueNumber<PostedGithubComment> | null> {
const issueContext = extractIssueContext(context);
if (!issueContext) {
context.logger.info("Cannot post comment: missing issue context in payload");
return null;
return logMessage?.type === "fatal" ? [metadataVisible, metadataHidden].join("\n") : metadataHidden;
}

const body = await createCommentBody(context, message, options);
const { issueNumber, commentId, owner, repo } = issueContext;
const params = { owner, repo, body, issueNumber };
async _createCommentBody(context: Context, message: LogReturn | Error, options: CommentOptions): Promise<string> {
const { metadata, logMessage } = await this._processMessage(context, message);
const { header, jsonPretty } = await this._createMetadataContent(context, metadata);
const metadataContent = this._formatMetadataContent(logMessage, header, jsonPretty);

if (options.updateComment) {
if (lastCommentId.issueCommentId && !("pull_request" in context.payload && "comment" in context.payload)) {
return updateIssueComment(context, params);
return `${options.raw ? logMessage?.raw : logMessage?.diff}\n\n${metadataContent}\n`;
}

async postComment(
context: Context,
message: LogReturn | Error,
options: CommentOptions = { updateComment: true, raw: false }
): Promise<WithIssueNumber<PostedGithubComment> | null> {
const issueContext = this._extractIssueContext(context);
if (!issueContext) {
context.logger.info("Cannot post comment: missing issue context in payload");
return null;
}

if (lastCommentId.reviewCommentId && "pull_request" in context.payload && "comment" in context.payload) {
return updateReviewComment(context, params);
const body = await this._createCommentBody(context, message, options);
const { issueNumber, commentId, owner, repo } = issueContext;
const params = { owner, repo, body, issueNumber };

if (options.updateComment) {
if (this._lastCommentId.issueCommentId && !("pull_request" in context.payload && "comment" in context.payload)) {
return this._updateIssueComment(context, params);
}

if (this._lastCommentId.reviewCommentId && "pull_request" in context.payload && "comment" in context.payload) {
return this._updateReviewComment(context, params);
}
}
}

return createNewComment(context, { ...params, commentId });
return this._createNewComment(context, { ...params, commentId });
}
}
2 changes: 2 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks";
import { Logs } from "@ubiquity-os/ubiquity-os-logger";
import { CommentHandler } from "./comment";
import { customOctokit } from "./octokit";

export interface Context<TConfig = unknown, TEnv = unknown, TCommand = unknown, TSupportedEvents extends WebhookEventName = WebhookEventName> {
Expand All @@ -12,4 +13,5 @@ export interface Context<TConfig = unknown, TEnv = unknown, TCommand = unknown,
config: TConfig;
env: TEnv;
logger: Logs;
commentHandler: CommentHandler;
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { createPlugin } from "./server";
export { createActionsPlugin } from "./actions";
export { postComment } from "./comment";
export { CommentHandler } from "./comment";
export type { Context } from "./context";
Loading

0 comments on commit da526ff

Please sign in to comment.