mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
feat: migrate to Nuxt 4 (Nitro) and Tailwind CSS v3
This commit is contained in:
parent
f4959eebf8
commit
b139712a91
166 changed files with 19790 additions and 5539 deletions
222
server/lib/formatters/check.ts
Normal file
222
server/lib/formatters/check.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS, WORKFLOW_CONCLUSION_EMOJI } from "./colors";
|
||||
import { branchLink, commitLink, emojiPrefix, type T, buildMessage, repoBaseUrl } from "./helpers";
|
||||
|
||||
export function formatCheckSuite(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const suite = payload.check_suite as {
|
||||
head_branch?: string;
|
||||
head_sha?: string;
|
||||
conclusion?: string;
|
||||
status?: string;
|
||||
app?: { name?: string };
|
||||
html_url?: string;
|
||||
};
|
||||
|
||||
const status =
|
||||
suite.status === "queued"
|
||||
? "queued"
|
||||
: suite.status === "in_progress"
|
||||
? "running"
|
||||
: (suite.conclusion ?? "pending");
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
const emoji = WORKFLOW_CONCLUSION_EMOJI[status] ?? "⏳";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const colorKey =
|
||||
status === "success"
|
||||
? "check_run_success"
|
||||
: status === "failure"
|
||||
? "check_run_failure"
|
||||
: "check_run_other";
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.status"),
|
||||
value: `${em(emoji)}${status}`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
if (suite.app?.name) {
|
||||
fields.push({
|
||||
name: t("fields.service"),
|
||||
value: suite.app.name,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (suite.head_branch) {
|
||||
fields.push({
|
||||
name: t("fields.branch"),
|
||||
value: branchLink(baseUrl, suite.head_branch),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (suite.head_sha) {
|
||||
fields.push({
|
||||
name: t("fields.commit"),
|
||||
value: commitLink(baseUrl, suite.head_sha),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.check_suite.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
conclusion: status,
|
||||
}),
|
||||
url:
|
||||
suite.html_url ??
|
||||
(baseUrl && suite.head_sha ? `${baseUrl}/commit/${suite.head_sha}/checks` : undefined),
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
fields,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatStatus(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const state = (payload.state as string) ?? "pending";
|
||||
const context = (payload.context as string) ?? "";
|
||||
const description = payload.description as string | undefined;
|
||||
const targetUrl = payload.target_url as string | undefined;
|
||||
const sha = payload.sha as string | undefined;
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
|
||||
const emoji = state === "success" ? "✅" : state === "failure" || state === "error" ? "❌" : "⏳";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const colorKey =
|
||||
state === "success"
|
||||
? "check_run_success"
|
||||
: state === "failure" || state === "error"
|
||||
? "check_run_failure"
|
||||
: "check_run_other";
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.status"),
|
||||
value: `${em(emoji)}${state}`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
if (context) {
|
||||
fields.push({
|
||||
name: t("fields.context"),
|
||||
value: context,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (sha) {
|
||||
fields.push({
|
||||
name: t("fields.commit"),
|
||||
value: commitLink(baseUrl, sha),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (description) {
|
||||
fields.push({
|
||||
name: t("fields.description"),
|
||||
value: description,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.status.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
context: context || t("common.unknown"),
|
||||
state,
|
||||
}),
|
||||
url: targetUrl,
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
fields,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatCheckRun(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const checkRun = payload.check_run as {
|
||||
id?: number;
|
||||
name?: string;
|
||||
conclusion?: string;
|
||||
html_url?: string;
|
||||
status?: string;
|
||||
output?: { title?: string; summary?: string };
|
||||
};
|
||||
|
||||
const status =
|
||||
checkRun.status === "queued"
|
||||
? "queued"
|
||||
: checkRun.status === "in_progress"
|
||||
? "running"
|
||||
: (checkRun.conclusion ?? "pending");
|
||||
const emoji = WORKFLOW_CONCLUSION_EMOJI[status] ?? "⏳";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const colorKey =
|
||||
status === "success"
|
||||
? "check_run_success"
|
||||
: status === "failure"
|
||||
? "check_run_failure"
|
||||
: "check_run_other";
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.status"),
|
||||
value: `${em(emoji)}${status}`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
if (checkRun.output?.title) {
|
||||
fields.push({
|
||||
name: t("fields.details"),
|
||||
value: checkRun.output.title,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.check_run.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
name: checkRun.name ?? "Check Run",
|
||||
conclusion: status,
|
||||
}),
|
||||
url: checkRun.html_url,
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
fields,
|
||||
updateKey: repo && checkRun.id != null ? `check_run:${repo}:${checkRun.id}` : undefined,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
64
server/lib/formatters/colors.ts
Normal file
64
server/lib/formatters/colors.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
export const GITHUB_COLORS = {
|
||||
push: 0x2ea44f,
|
||||
pull_request_opened: 0x2da44e,
|
||||
pull_request_closed: 0xf85149,
|
||||
pull_request_merged: 0x8957e5,
|
||||
pull_request_ready_for_review: 0x2da44e,
|
||||
pull_request_other: 0x1f6feb,
|
||||
issues_opened: 0x2da44e,
|
||||
issues_closed: 0xf85149,
|
||||
issues_reopened: 0x1f6feb,
|
||||
issues_other: 0x8957e5,
|
||||
issue_comment: 0x6e7681,
|
||||
workflow_run_success: 0x2da44e,
|
||||
workflow_run_failure: 0xf85149,
|
||||
workflow_run_other: 0xd29922,
|
||||
release_published: 0x2da44e,
|
||||
release_prerelease: 0xd29922,
|
||||
release_deleted: 0xf85149,
|
||||
create: 0x3fb950,
|
||||
delete: 0xf85149,
|
||||
star: 0xd29922,
|
||||
fork: 0x1f6feb,
|
||||
discussion: 0x8957e5,
|
||||
check_run_success: 0x2da44e,
|
||||
check_run_failure: 0xf85149,
|
||||
check_run_other: 0xd29922,
|
||||
pull_request_review_approved: 0x2da44e,
|
||||
pull_request_review_changes: 0xf85149,
|
||||
pull_request_review_commented: 0x8b949e,
|
||||
commit_comment: 0x6e7681,
|
||||
deployment_success: 0x2da44e,
|
||||
deployment_failure: 0xf85149,
|
||||
deployment_pending: 0xd29922,
|
||||
member_added: 0x2da44e,
|
||||
member_removed: 0xf85149,
|
||||
label: 0x8957e5,
|
||||
milestone_opened: 0x1f6feb,
|
||||
milestone_closed: 0x2da44e,
|
||||
discussion_created: 0x8957e5,
|
||||
discussion_answered: 0x2da44e,
|
||||
discussion_comment: 0x6e7681,
|
||||
repository: 0x8b949e,
|
||||
code_scanning_critical: 0xf85149,
|
||||
code_scanning_high: 0xf85149,
|
||||
code_scanning_medium: 0xd29922,
|
||||
code_scanning_low: 0x8b949e,
|
||||
dependabot_critical: 0xf85149,
|
||||
dependabot_high: 0xf85149,
|
||||
dependabot_medium: 0xd29922,
|
||||
dependabot_low: 0x8b949e,
|
||||
default: 0x8b949e,
|
||||
} as const;
|
||||
|
||||
export const WORKFLOW_CONCLUSION_EMOJI: Record<string, string> = {
|
||||
success: "✅",
|
||||
failure: "❌",
|
||||
cancelled: "🚫",
|
||||
timed_out: "⏱️",
|
||||
action_required: "⚠️",
|
||||
neutral: "➖",
|
||||
stale: "♻️",
|
||||
queued: "⏳",
|
||||
running: "🔄",
|
||||
};
|
||||
43
server/lib/formatters/comments.ts
Normal file
43
server/lib/formatters/comments.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatIssueComment(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const issue = payload.issue as {
|
||||
number?: number;
|
||||
title?: string;
|
||||
html_url?: string;
|
||||
};
|
||||
const comment = payload.comment as {
|
||||
body?: string;
|
||||
html_url?: string;
|
||||
};
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const commentBody = comment.body?.slice(0, 500) ?? "";
|
||||
const truncated = comment.body && comment.body.length > 500;
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.issue_comment.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
number: issue.number ?? "?",
|
||||
title: issue.title ?? t("common.untitled"),
|
||||
}),
|
||||
url: comment.html_url ?? issue.html_url,
|
||||
color: GITHUB_COLORS.issue_comment,
|
||||
description: `${t("events.issue_comment.action_comment", { emoji: em("💬"), action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
51
server/lib/formatters/commit-comment.ts
Normal file
51
server/lib/formatters/commit-comment.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { commitLink, emojiPrefix, type T, buildMessage, repoBaseUrl } from "./helpers";
|
||||
|
||||
export function formatCommitComment(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const comment = payload.comment as {
|
||||
body?: string;
|
||||
commit_id?: string;
|
||||
html_url?: string;
|
||||
};
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const commentBody = comment.body?.slice(0, 500) ?? "";
|
||||
const truncated = comment.body && comment.body.length > 500;
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
const shortSha = comment.commit_id?.slice(0, 7) ?? "???????";
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
if (comment.commit_id) {
|
||||
fields.push({
|
||||
name: t("fields.commit"),
|
||||
value: commitLink(baseUrl, comment.commit_id, shortSha),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.commit_comment.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
sha: commitLink(baseUrl, comment.commit_id ?? "", shortSha),
|
||||
}),
|
||||
url: comment.html_url,
|
||||
color: GITHUB_COLORS.commit_comment,
|
||||
description: `${t("events.commit_comment.action_comment", { emoji: em("💬"), action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
||||
fields: fields.length > 0 ? fields : undefined,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
88
server/lib/formatters/create.ts
Normal file
88
server/lib/formatters/create.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { branchLink, emojiPrefix, tagLink, type T, buildMessage, repoBaseUrl } from "./helpers";
|
||||
|
||||
export function formatCreate(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const refType = (payload.ref_type as string) ?? "branch";
|
||||
const ref = (payload.ref as string) ?? t("common.unknown");
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
const refText = refType === "tag" ? tagLink(baseUrl, ref) : branchLink(baseUrl, ref);
|
||||
|
||||
const emoji = refType === "tag" ? "🏷️" : "🌿";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.type"),
|
||||
value: refType,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
fields.push({
|
||||
name: t("fields.name"),
|
||||
value: refText,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
if (payload.description) {
|
||||
fields.push({
|
||||
name: t("fields.description"),
|
||||
value: payload.description as string,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.create.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
emoji: em(emoji),
|
||||
type: refType,
|
||||
ref: refText,
|
||||
}),
|
||||
color: GITHUB_COLORS.create,
|
||||
fields,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatDelete(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const refType = (payload.ref_type as string) ?? "branch";
|
||||
const ref = (payload.ref as string) ?? t("common.unknown");
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
const refText = refType === "tag" ? tagLink(baseUrl, ref) : branchLink(baseUrl, ref);
|
||||
|
||||
const emoji = refType === "tag" ? "🏷️" : "🌿";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.delete.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
emoji: em(emoji),
|
||||
type: refType,
|
||||
ref: refText,
|
||||
}),
|
||||
color: GITHUB_COLORS.delete,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
89
server/lib/formatters/custom.ts
Normal file
89
server/lib/formatters/custom.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import type { NeutralAuthor, NeutralField, NeutralMessage } from "../types";
|
||||
import { type T, buildMessage } from "./helpers";
|
||||
|
||||
const COLOR_WORDS: Record<string, number> = {
|
||||
red: 0xf85149,
|
||||
green: 0x3fb950,
|
||||
yellow: 0xd29922,
|
||||
blue: 0x58a6ff,
|
||||
purple: 0xbc8cff,
|
||||
orange: 0xdb6d28,
|
||||
cyan: 0x39c5cf,
|
||||
gray: 0x6e7681,
|
||||
};
|
||||
|
||||
function parseColor(color: unknown): number | undefined {
|
||||
if (typeof color !== "string") return undefined;
|
||||
const key = color.trim().toLowerCase();
|
||||
if (COLOR_WORDS[key]) return COLOR_WORDS[key];
|
||||
const hex = /^#?([0-9a-f]{6})$/i.exec(key);
|
||||
return hex ? parseInt(hex[1]!, 16) : undefined;
|
||||
}
|
||||
|
||||
function parseFields(raw: unknown): NeutralField[] | undefined {
|
||||
if (!Array.isArray(raw)) return undefined;
|
||||
const fields: NeutralField[] = [];
|
||||
for (const f of raw) {
|
||||
if (!f || typeof f !== "object") continue;
|
||||
const name = (f as Record<string, unknown>).name;
|
||||
const value = (f as Record<string, unknown>).value;
|
||||
if (typeof name !== "string" || typeof value !== "string") continue;
|
||||
fields.push({
|
||||
name: name || "\u200b",
|
||||
value,
|
||||
inline: (f as Record<string, unknown>).inline === true,
|
||||
});
|
||||
}
|
||||
return fields.length > 0 ? fields : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a `custom` webhook payload (the message schema documented in
|
||||
* docs/guide/configuration.md) into a NeutralMessage. Unlike forge events the
|
||||
* title is not required to start with a repo; an optional `repo` field is
|
||||
* used as the `{repo}: ` prefix and footer.
|
||||
*/
|
||||
export function formatCustom(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
_showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const title =
|
||||
typeof payload.title === "string" && payload.title.trim()
|
||||
? payload.title.trim()
|
||||
: t("custom.title_fallback");
|
||||
const payloadRepo =
|
||||
typeof payload.repo === "string" && payload.repo.trim() ? payload.repo.trim() : undefined;
|
||||
const effectiveRepo = payloadRepo ?? repo;
|
||||
const fullTitle = effectiveRepo ? `${effectiveRepo}: ${title}` : title;
|
||||
const description = typeof payload.description === "string" ? payload.description : undefined;
|
||||
const url = typeof payload.url === "string" ? payload.url : undefined;
|
||||
const footer = typeof payload.footer === "string" ? payload.footer : undefined;
|
||||
|
||||
const rawAuthor = payload.author as Record<string, unknown> | undefined;
|
||||
const customAuthor: NeutralAuthor | undefined =
|
||||
rawAuthor && typeof rawAuthor === "object"
|
||||
? {
|
||||
name: typeof rawAuthor.name === "string" && rawAuthor.name ? rawAuthor.name : author.name,
|
||||
iconUrl: typeof rawAuthor.iconUrl === "string" ? rawAuthor.iconUrl : author.iconUrl,
|
||||
url: typeof rawAuthor.url === "string" ? rawAuthor.url : author.url,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author: customAuthor,
|
||||
title: fullTitle,
|
||||
url,
|
||||
color: parseColor(payload.color) ?? 0x6e7681,
|
||||
description,
|
||||
fields: parseFields(payload.fields),
|
||||
// A custom message without a repo gets no `{repo}` footer at all.
|
||||
footer: footer ?? (effectiveRepo ? undefined : ""),
|
||||
},
|
||||
t,
|
||||
effectiveRepo,
|
||||
);
|
||||
}
|
||||
179
server/lib/formatters/deployment.ts
Normal file
179
server/lib/formatters/deployment.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import {
|
||||
branchLink,
|
||||
commitLink,
|
||||
emojiPrefix,
|
||||
tagLink,
|
||||
type T,
|
||||
buildMessage,
|
||||
repoBaseUrl,
|
||||
} from "./helpers";
|
||||
|
||||
export function formatDeployment(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const deployment = payload.deployment as {
|
||||
environment?: string;
|
||||
ref?: string;
|
||||
sha?: string;
|
||||
description?: string;
|
||||
html_url?: string;
|
||||
statuses_url?: string;
|
||||
};
|
||||
|
||||
const emoji = "🚀";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const env = deployment.environment ?? t("common.unknown");
|
||||
const shortSha = deployment.sha?.slice(0, 7) ?? "???????";
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.status"),
|
||||
value: `${em(emoji)}created`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
fields.push({
|
||||
name: t("fields.environment"),
|
||||
value: env,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
if (deployment.ref) {
|
||||
const isTag = deployment.ref.startsWith("refs/tags/");
|
||||
fields.push({
|
||||
name: t("fields.branch_tag"),
|
||||
value: isTag ? tagLink(baseUrl, deployment.ref) : branchLink(baseUrl, deployment.ref),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (deployment.sha) {
|
||||
fields.push({
|
||||
name: t("fields.commit"),
|
||||
value: commitLink(baseUrl, deployment.sha, shortSha),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (deployment.description) {
|
||||
fields.push({
|
||||
name: t("fields.description"),
|
||||
value: deployment.description,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.deployment.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
env,
|
||||
state: "created",
|
||||
}),
|
||||
url: deployment.html_url ?? deployment.statuses_url,
|
||||
color: GITHUB_COLORS.deployment_pending,
|
||||
fields,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatDeploymentStatus(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const status = payload.deployment_status as {
|
||||
state?: string;
|
||||
environment?: string;
|
||||
environment_url?: string;
|
||||
description?: string;
|
||||
};
|
||||
const deployment = payload.deployment as {
|
||||
sha?: string;
|
||||
ref?: string;
|
||||
environment?: string;
|
||||
};
|
||||
|
||||
const state = status.state ?? "pending";
|
||||
const colorKey =
|
||||
state === "success"
|
||||
? "deployment_success"
|
||||
: state === "failure"
|
||||
? "deployment_failure"
|
||||
: "deployment_pending";
|
||||
const emoji = state === "success" ? "✅" : state === "failure" ? "❌" : "⏳";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const env = status.environment ?? deployment.environment ?? t("common.unknown");
|
||||
const shortSha = deployment.sha?.slice(0, 7) ?? "???????";
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.status"),
|
||||
value: `${em(emoji)}${status}`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
fields.push({
|
||||
name: t("fields.environment"),
|
||||
value: env,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
if (deployment.ref) {
|
||||
const isTag = deployment.ref.startsWith("refs/tags/");
|
||||
fields.push({
|
||||
name: t("fields.branch_tag"),
|
||||
value: isTag ? tagLink(baseUrl, deployment.ref) : branchLink(baseUrl, deployment.ref),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (deployment.sha) {
|
||||
fields.push({
|
||||
name: t("fields.commit"),
|
||||
value: commitLink(baseUrl, deployment.sha, shortSha),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (status.environment_url) {
|
||||
fields.push({
|
||||
name: t("fields.url"),
|
||||
value: status.environment_url,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (status.description) {
|
||||
fields.push({
|
||||
name: t("fields.description"),
|
||||
value: status.description,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.deployment.title", { repo: repo ?? t("common.repository"), env, state }),
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
fields,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
96
server/lib/formatters/discussion.ts
Normal file
96
server/lib/formatters/discussion.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatDiscussion(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const discussion = payload.discussion as {
|
||||
number?: number;
|
||||
title?: string;
|
||||
html_url?: string;
|
||||
category?: { name?: string };
|
||||
};
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const stateEmoji =
|
||||
action === "answered"
|
||||
? "✅"
|
||||
: action === "closed"
|
||||
? "🔴"
|
||||
: action === "created"
|
||||
? "🟢"
|
||||
: action === "deleted"
|
||||
? "🗑️"
|
||||
: "💬";
|
||||
|
||||
const category = discussion.category?.name;
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.discussion.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
number: discussion.number ?? "?",
|
||||
title: discussion.title ?? t("common.untitled"),
|
||||
}),
|
||||
url: discussion.html_url,
|
||||
color:
|
||||
action === "answered"
|
||||
? GITHUB_COLORS.discussion_answered
|
||||
: GITHUB_COLORS.discussion_created,
|
||||
description: t("events.discussion.action_discussion", {
|
||||
emoji: em(stateEmoji),
|
||||
action: al,
|
||||
category: category ? ` in **${category}**` : "",
|
||||
}),
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatDiscussionComment(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const comment = payload.comment as {
|
||||
body?: string;
|
||||
html_url?: string;
|
||||
};
|
||||
const discussion = payload.discussion as {
|
||||
number?: number;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const commentBody = comment.body?.slice(0, 500) ?? "";
|
||||
const truncated = comment.body && comment.body.length > 500;
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.discussion_comment.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
number: discussion.number ?? "?",
|
||||
title: discussion.title ?? t("common.untitled"),
|
||||
}),
|
||||
url: comment.html_url,
|
||||
color: GITHUB_COLORS.discussion_comment,
|
||||
description: `${t("events.discussion_comment.action_comment", { emoji: em("💬"), action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
26
server/lib/formatters/generic.ts
Normal file
26
server/lib/formatters/generic.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatGeneric(
|
||||
eventType: string,
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
_showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.generic.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
event: eventType,
|
||||
action: payload.action ? `: ${payload.action}` : "",
|
||||
}),
|
||||
color: GITHUB_COLORS.default,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
65
server/lib/formatters/helpers.ts
Normal file
65
server/lib/formatters/helpers.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import type { NeutralMessage } from "../types";
|
||||
import { t as translate } from "../lib/i18n";
|
||||
import type { Translations } from "../lib/i18n";
|
||||
|
||||
export type T = (key: string, params?: Record<string, string | number>) => string;
|
||||
|
||||
export function emojiPrefix(emoji: string, show: boolean): string {
|
||||
return show ? `${emoji} ` : "";
|
||||
}
|
||||
|
||||
export function makeT(tr?: Translations): T {
|
||||
return (key, params) => translate(key, params, undefined, tr);
|
||||
}
|
||||
|
||||
export function buildMessage(
|
||||
partial: Omit<Partial<NeutralMessage>, "title"> & { title: string },
|
||||
t: T,
|
||||
repo?: string,
|
||||
): NeutralMessage {
|
||||
return {
|
||||
...partial,
|
||||
footer: partial.footer ?? t("common.footer", { repo: repo ?? t("common.github") }),
|
||||
timestamp: partial.timestamp ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Base URL of the forge repo (e.g. `https://github.com/owner/repo` or a Gitea
|
||||
* instance URL). Derived from `repository.html_url` in the payload so it works
|
||||
* for any provider; falls back to github.com for legacy payloads.
|
||||
*/
|
||||
export function repoBaseUrl(payload: Record<string, unknown>, repo?: string): string | undefined {
|
||||
const html = (payload.repository as { html_url?: string } | undefined)?.html_url;
|
||||
if (html) return html;
|
||||
return repo ? `https://github.com/${repo}` : undefined;
|
||||
}
|
||||
|
||||
function encodeRefPath(ref: string): string {
|
||||
return ref
|
||||
.split("/")
|
||||
.map((seg) => encodeURIComponent(seg))
|
||||
.join("/");
|
||||
}
|
||||
|
||||
/** Inline code + hyperlink for a commit, e.g. [`abc123d`](.../commit/abc123def456). */
|
||||
export function commitLink(baseUrl: string | undefined, sha: string, short?: string): string {
|
||||
const label = short ?? sha.slice(0, 7);
|
||||
return baseUrl ? `[\`${label}\`](${baseUrl}/commit/${encodeRefPath(sha)})` : `\`${label}\``;
|
||||
}
|
||||
|
||||
/** Inline code + hyperlink for a branch (or bare ref), e.g. [`main`](.../tree/main). */
|
||||
export function branchLink(baseUrl: string | undefined, branch: string, label?: string): string {
|
||||
const clean = branch.replace("refs/heads/", "").replace("refs/tags/", "");
|
||||
const display = label ?? clean;
|
||||
return baseUrl ? `[\`${display}\`](${baseUrl}/tree/${encodeRefPath(clean)})` : `\`${display}\``;
|
||||
}
|
||||
|
||||
/** Inline code + hyperlink for a tag, e.g. [`v1.0`](.../releases/tag/v1.0). */
|
||||
export function tagLink(baseUrl: string | undefined, tag: string, label?: string): string {
|
||||
const clean = tag.replace("refs/tags/", "");
|
||||
const display = label ?? clean;
|
||||
return baseUrl
|
||||
? `[\`${display}\`](${baseUrl}/releases/tag/${encodeRefPath(clean)})`
|
||||
: `\`${display}\``;
|
||||
}
|
||||
109
server/lib/formatters/index.ts
Normal file
109
server/lib/formatters/index.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import type { Route, WebhookEvent, NeutralMessage, NeutralAuthor } from "../types";
|
||||
import type { Translations } from "../lib/i18n";
|
||||
import { makeT, type T } from "./helpers";
|
||||
import { formatPush } from "./push";
|
||||
import { formatPullRequest } from "./pull-request";
|
||||
import { formatPullRequestReview, formatPullRequestReviewComment } from "./review";
|
||||
import { formatIssues } from "./issues";
|
||||
import { formatIssueComment } from "./comments";
|
||||
import { formatWorkflowRun, formatWorkflowJob } from "./workflow";
|
||||
import { formatRelease } from "./release";
|
||||
import { formatCreate, formatDelete } from "./create";
|
||||
import { formatStar, formatFork } from "./repo";
|
||||
import { formatCheckRun, formatCheckSuite, formatStatus } from "./check";
|
||||
import { formatCommitComment } from "./commit-comment";
|
||||
import { formatDeployment, formatDeploymentStatus } from "./deployment";
|
||||
import { formatMember } from "./member";
|
||||
import { formatLabel } from "./label";
|
||||
import { formatMilestone } from "./milestone";
|
||||
import { formatDiscussion, formatDiscussionComment } from "./discussion";
|
||||
import { formatRepository } from "./repository";
|
||||
import { formatCodeScanningAlert, formatDependabotAlert } from "./security";
|
||||
import { formatGeneric } from "./generic";
|
||||
import { formatPing } from "./ping";
|
||||
import { formatCustom } from "./custom";
|
||||
|
||||
export function formatEvent(
|
||||
route: Route,
|
||||
event: WebhookEvent,
|
||||
tr?: Translations,
|
||||
showEmoji = true,
|
||||
): NeutralMessage {
|
||||
const { event: eventType, payload } = event;
|
||||
const repo = (payload.repository as { full_name?: string })?.full_name;
|
||||
const sender = (payload.sender as { login?: string })?.login;
|
||||
const senderAvatar = (payload.sender as { avatar_url?: string })?.avatar_url;
|
||||
const senderUrl = (payload.sender as { html_url?: string })?.html_url;
|
||||
const repoUrl = (payload.repository as { html_url?: string })?.html_url;
|
||||
|
||||
const t: T = makeT(tr);
|
||||
|
||||
const author: NeutralAuthor = {
|
||||
name: sender ?? t("common.unknown"),
|
||||
iconUrl: senderAvatar,
|
||||
url: senderUrl ?? (sender ? `https://github.com/${sender}` : undefined),
|
||||
};
|
||||
|
||||
switch (eventType) {
|
||||
case "push":
|
||||
return formatPush(payload, repo, author, t, showEmoji);
|
||||
case "pull_request":
|
||||
return formatPullRequest(payload, repo, author, t, showEmoji);
|
||||
case "pull_request_review":
|
||||
return formatPullRequestReview(payload, repo, author, t, showEmoji);
|
||||
case "pull_request_review_comment":
|
||||
return formatPullRequestReviewComment(payload, repo, author, t, showEmoji);
|
||||
case "issues":
|
||||
return formatIssues(payload, repo, author, t, showEmoji);
|
||||
case "issue_comment":
|
||||
return formatIssueComment(payload, repo, author, t, showEmoji);
|
||||
case "workflow_run":
|
||||
return formatWorkflowRun(payload, repo, author, t, showEmoji);
|
||||
case "workflow_job":
|
||||
return formatWorkflowJob(payload, repo, author, t, showEmoji);
|
||||
case "status":
|
||||
return formatStatus(payload, repo, author, t, showEmoji);
|
||||
case "deployment":
|
||||
return formatDeployment(payload, repo, author, t, showEmoji);
|
||||
case "ping":
|
||||
return formatPing(payload, repo, author, t, showEmoji);
|
||||
case "release":
|
||||
return formatRelease(payload, repo, author, t, showEmoji);
|
||||
case "create":
|
||||
return formatCreate(payload, repo, author, t, showEmoji);
|
||||
case "delete":
|
||||
return formatDelete(payload, repo, author, t, showEmoji);
|
||||
case "star":
|
||||
return formatStar(payload, repo, repoUrl, author, t, showEmoji);
|
||||
case "fork":
|
||||
return formatFork(payload, repo, repoUrl, author, t, showEmoji);
|
||||
case "check_run":
|
||||
return formatCheckRun(payload, repo, author, t, showEmoji);
|
||||
case "check_suite":
|
||||
return formatCheckSuite(payload, repo, author, t, showEmoji);
|
||||
case "commit_comment":
|
||||
return formatCommitComment(payload, repo, author, t, showEmoji);
|
||||
case "deployment_status":
|
||||
return formatDeploymentStatus(payload, repo, author, t, showEmoji);
|
||||
case "member":
|
||||
return formatMember(payload, repo, author, t, showEmoji);
|
||||
case "label":
|
||||
return formatLabel(payload, repo, author, t, showEmoji);
|
||||
case "milestone":
|
||||
return formatMilestone(payload, repo, author, t, showEmoji);
|
||||
case "discussion":
|
||||
return formatDiscussion(payload, repo, author, t, showEmoji);
|
||||
case "discussion_comment":
|
||||
return formatDiscussionComment(payload, repo, author, t, showEmoji);
|
||||
case "repository":
|
||||
return formatRepository(payload, repo, repoUrl, author, t, showEmoji);
|
||||
case "code_scanning_alert":
|
||||
return formatCodeScanningAlert(payload, repo, author, t, showEmoji);
|
||||
case "dependabot_alert":
|
||||
return formatDependabotAlert(payload, repo, author, t, showEmoji);
|
||||
case "custom":
|
||||
return formatCustom(payload, repo, author, t, showEmoji);
|
||||
default:
|
||||
return formatGeneric(eventType, payload, repo, author, t, showEmoji);
|
||||
}
|
||||
}
|
||||
87
server/lib/formatters/issues.ts
Normal file
87
server/lib/formatters/issues.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatIssues(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "opened";
|
||||
const issue = payload.issue as {
|
||||
number?: number;
|
||||
title?: string;
|
||||
html_url?: string;
|
||||
state?: string;
|
||||
body?: string;
|
||||
labels?: Array<{ name?: string; color?: string }>;
|
||||
assignees?: Array<{ login?: string }>;
|
||||
milestone?: { title?: string };
|
||||
};
|
||||
|
||||
const colorKey =
|
||||
action === "closed"
|
||||
? "issues_closed"
|
||||
: action === "reopened"
|
||||
? "issues_reopened"
|
||||
: action === "opened"
|
||||
? "issues_opened"
|
||||
: "issues_other";
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const stateEmoji = action === "closed" ? "🔴" : action === "opened" ? "🟢" : "🟣";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const descriptionParts: string[] = [];
|
||||
descriptionParts.push(t("events.issues.action_issue", { emoji: em(stateEmoji), action: al }));
|
||||
|
||||
if (issue.body) {
|
||||
const truncated = issue.body.slice(0, 300);
|
||||
descriptionParts.push(`\n${truncated}${issue.body.length > 300 ? "..." : ""}`);
|
||||
}
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
if (issue.labels && issue.labels.length > 0) {
|
||||
fields.push({
|
||||
name: t("fields.labels"),
|
||||
value: issue.labels.map((l) => l.name).join(", "),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (issue.assignees && issue.assignees.length > 0) {
|
||||
fields.push({
|
||||
name: t("fields.assignees"),
|
||||
value: issue.assignees.map((a) => a.login).join(", "),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (issue.milestone) {
|
||||
fields.push({
|
||||
name: t("fields.milestone"),
|
||||
value: issue.milestone.title ?? t("common.unknown"),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.issues.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
number: issue.number ?? "?",
|
||||
title: issue.title ?? t("common.untitled"),
|
||||
}),
|
||||
url: issue.html_url,
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
description: descriptionParts.join("\n"),
|
||||
fields: fields.length > 0 ? fields : undefined,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
64
server/lib/formatters/label.ts
Normal file
64
server/lib/formatters/label.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatLabel(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const label = payload.label as {
|
||||
name?: string;
|
||||
color?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const emoji = action === "deleted" ? "🗑️" : action === "edited" ? "✏️" : "🏷️";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
if (label.name) {
|
||||
fields.push({
|
||||
name: t("fields.label"),
|
||||
value: label.name,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (label.color) {
|
||||
fields.push({
|
||||
name: t("fields.color"),
|
||||
value: `#${label.color}`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (label.description) {
|
||||
fields.push({
|
||||
name: t("fields.description"),
|
||||
value: label.description,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.label.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
emoji: em(emoji),
|
||||
action: al,
|
||||
name: label.name ?? t("common.unknown"),
|
||||
}),
|
||||
color: GITHUB_COLORS.label,
|
||||
fields: fields.length > 0 ? fields : undefined,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
34
server/lib/formatters/member.ts
Normal file
34
server/lib/formatters/member.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatMember(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "added";
|
||||
const member = payload.member as { login?: string } | undefined;
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const emoji = action === "added" ? "➕" : action === "removed" ? "➖" : "👤";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const memberName = member?.login ?? t("common.unknown");
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.member.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
emoji: em(emoji),
|
||||
action: al,
|
||||
name: memberName,
|
||||
}),
|
||||
color: action === "added" ? GITHUB_COLORS.member_added : GITHUB_COLORS.member_removed,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
83
server/lib/formatters/milestone.ts
Normal file
83
server/lib/formatters/milestone.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatMilestone(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const milestone = payload.milestone as {
|
||||
title?: string;
|
||||
number?: number;
|
||||
state?: string;
|
||||
open_issues?: number;
|
||||
closed_issues?: number;
|
||||
due_on?: string;
|
||||
html_url?: string;
|
||||
};
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const stateEmoji = milestone.state === "closed" ? "✅" : "🔵";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
if (milestone.title) {
|
||||
fields.push({
|
||||
name: t("fields.milestone"),
|
||||
value: milestone.title,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (milestone.number) {
|
||||
fields.push({
|
||||
name: t("fields.number"),
|
||||
value: `#${milestone.number}`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (milestone.open_issues != null && milestone.closed_issues != null) {
|
||||
const total = milestone.open_issues + milestone.closed_issues;
|
||||
const pct = total > 0 ? Math.round((milestone.closed_issues / total) * 100) : 0;
|
||||
const bar = pct >= 75 ? "🟢🟢🟢" : pct >= 50 ? "🟡🟡" : pct > 0 ? "🟠" : "⬜";
|
||||
fields.push({
|
||||
name: t("fields.progress"),
|
||||
value: `${bar} ${milestone.closed_issues}/${total} (${pct}%)`,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (milestone.due_on) {
|
||||
fields.push({
|
||||
name: t("fields.due"),
|
||||
value: milestone.due_on.split("T")[0] ?? "",
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.milestone.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
emoji: em(stateEmoji),
|
||||
action: al,
|
||||
title: milestone.title ?? t("common.unknown"),
|
||||
}),
|
||||
url: milestone.html_url,
|
||||
color:
|
||||
milestone.state === "closed"
|
||||
? GITHUB_COLORS.milestone_closed
|
||||
: GITHUB_COLORS.milestone_opened,
|
||||
fields: fields.length > 0 ? fields : undefined,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
36
server/lib/formatters/ping.ts
Normal file
36
server/lib/formatters/ping.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatPing(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
_showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const zen = payload.zen as string | undefined;
|
||||
const hookId = payload.hook_id as number | undefined;
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
if (hookId != null) {
|
||||
fields.push({
|
||||
name: t("fields.details"),
|
||||
value: String(hookId),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.ping.title", { repo: repo ?? t("common.repository") }),
|
||||
color: GITHUB_COLORS.default,
|
||||
description: zen,
|
||||
fields,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
116
server/lib/formatters/pull-request.ts
Normal file
116
server/lib/formatters/pull-request.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import type { NeutralMessage, NeutralAuthor, NeutralAction } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { branchLink, emojiPrefix, type T, buildMessage, repoBaseUrl } from "./helpers";
|
||||
|
||||
export function formatPullRequest(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "opened";
|
||||
const pr = payload.pull_request as {
|
||||
number?: number;
|
||||
title?: string;
|
||||
html_url?: string;
|
||||
state?: string;
|
||||
draft?: boolean;
|
||||
merged?: boolean;
|
||||
head?: { ref?: string; sha?: string; repo?: { html_url?: string } };
|
||||
base?: { ref?: string; repo?: { html_url?: string } };
|
||||
body?: string;
|
||||
labels?: Array<{ name?: string; color?: string }>;
|
||||
changed_files?: number;
|
||||
additions?: number;
|
||||
deletions?: number;
|
||||
};
|
||||
|
||||
const colorKey = pr.merged
|
||||
? "pull_request_merged"
|
||||
: action === "closed"
|
||||
? "pull_request_closed"
|
||||
: action === "ready_for_review"
|
||||
? "pull_request_ready_for_review"
|
||||
: action === "opened"
|
||||
? "pull_request_opened"
|
||||
: "pull_request_other";
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const stateEmoji = pr.merged
|
||||
? "🟣"
|
||||
: action === "closed"
|
||||
? "🔴"
|
||||
: action === "opened"
|
||||
? "🟢"
|
||||
: "🔵";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const descriptionParts: string[] = [];
|
||||
descriptionParts.push(t("events.pr.action_pr", { emoji: em(stateEmoji), action: al }));
|
||||
|
||||
if (pr.body) {
|
||||
const truncated = pr.body.slice(0, 300);
|
||||
descriptionParts.push(`\n${truncated}${pr.body.length > 300 ? "..." : ""}`);
|
||||
}
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
if (pr.head?.ref && pr.base?.ref) {
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
const headUrl = pr.head.repo?.html_url ?? baseUrl;
|
||||
const baseRepoUrl = pr.base.repo?.html_url ?? baseUrl;
|
||||
fields.push({
|
||||
name: t("fields.branch"),
|
||||
value: `${branchLink(headUrl, pr.head.ref)} → ${branchLink(baseRepoUrl, pr.base.ref)}`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (pr.changed_files != null || pr.additions != null || pr.deletions != null) {
|
||||
const parts: string[] = [];
|
||||
if (pr.additions != null) parts.push(`+${pr.additions}`);
|
||||
if (pr.deletions != null) parts.push(`-${pr.deletions}`);
|
||||
if (pr.changed_files != null) parts.push(t("common.n_files", { count: pr.changed_files }));
|
||||
fields.push({
|
||||
name: t("fields.changes"),
|
||||
value: parts.join(" | "),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (pr.labels && pr.labels.length > 0) {
|
||||
fields.push({
|
||||
name: t("fields.labels"),
|
||||
value: pr.labels.map((l) => l.name).join(", "),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
const [repoOwner, repoName] = (repo ?? "/").split("/", 2);
|
||||
const actionable = pr.state === "open" && !!repoOwner && !!repoName && pr.number != null;
|
||||
const actions: NeutralAction[] | undefined = actionable
|
||||
? [
|
||||
{ id: `ghpr|merge|${repoOwner}|${repoName}|${pr.number}`, label: "合并", style: "primary" },
|
||||
{ id: `ghpr|close|${repoOwner}|${repoName}|${pr.number}`, label: "关闭", style: "danger" },
|
||||
]
|
||||
: undefined;
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.pr.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
number: pr.number ?? "?",
|
||||
title: pr.title ?? t("common.untitled"),
|
||||
}),
|
||||
url: pr.html_url,
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
description: descriptionParts.join("\n"),
|
||||
fields: fields.length > 0 ? fields : undefined,
|
||||
actions,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
109
server/lib/formatters/push.ts
Normal file
109
server/lib/formatters/push.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { branchLink, emojiPrefix, tagLink, type T, buildMessage, repoBaseUrl } from "./helpers";
|
||||
|
||||
export function formatPush(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const rawRef = (payload.ref as string) ?? "";
|
||||
const isTagPush = rawRef.startsWith("refs/tags/");
|
||||
const ref = rawRef.replace("refs/heads/", "").replace("refs/tags/", "tag: ");
|
||||
const commits = (payload.commits ?? []) as Array<{
|
||||
id?: string;
|
||||
message?: string;
|
||||
author?: { name?: string; email?: string };
|
||||
added?: string[];
|
||||
removed?: string[];
|
||||
modified?: string[];
|
||||
}>;
|
||||
const count = commits.length;
|
||||
const compareUrl = payload.compare as string | undefined;
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
const forced = payload.forced as boolean | undefined;
|
||||
const created = payload.created as boolean | undefined;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const descriptionParts: string[] = [];
|
||||
|
||||
if (forced) {
|
||||
descriptionParts.push(em("⚠️") + t("events.push.force_push"));
|
||||
}
|
||||
if (created) {
|
||||
descriptionParts.push(em("🆕") + t("events.push.branch_created"));
|
||||
}
|
||||
|
||||
descriptionParts.push(
|
||||
t("events.push.commits_pushed", {
|
||||
count,
|
||||
s: count !== 1 ? "s" : "",
|
||||
ref: isTagPush ? tagLink(baseUrl, rawRef, ref) : branchLink(baseUrl, rawRef, ref),
|
||||
}),
|
||||
);
|
||||
|
||||
if (compareUrl) {
|
||||
descriptionParts.push(t("events.push.view_comparison", { url: compareUrl }));
|
||||
}
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
const commitField = (c: (typeof commits)[number]): { name: string; value: string } => {
|
||||
const shortId = c.id?.slice(0, 7) ?? "???????";
|
||||
const msg = (c.message?.split("\n")[0] ?? "").slice(0, 72) || t("common.no_message");
|
||||
const url = baseUrl && c.id ? `${baseUrl}/commit/${c.id}` : null;
|
||||
const hash = url ? `[\`${shortId}\`](${url})` : `\`${shortId}\``;
|
||||
return { name: `\u200b`, value: `${hash} ${msg}` };
|
||||
};
|
||||
|
||||
if (count <= 5) {
|
||||
for (const c of commits) {
|
||||
fields.push({ ...commitField(c), inline: false });
|
||||
}
|
||||
} else {
|
||||
const first3 = commits.slice(0, 3);
|
||||
for (const c of first3) {
|
||||
fields.push({ ...commitField(c), inline: false });
|
||||
}
|
||||
fields.push({
|
||||
name: `\u200b`,
|
||||
value: t("common.and_n_more", { count: count - 3 }),
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
const added = commits.flatMap((c) => c.added ?? []);
|
||||
const removed = commits.flatMap((c) => c.removed ?? []);
|
||||
const modified = commits.flatMap((c) => c.modified ?? []);
|
||||
|
||||
if (added.length > 0 || removed.length > 0 || modified.length > 0) {
|
||||
const changes: string[] = [];
|
||||
if (added.length > 0) changes.push(t("events.push.added", { count: added.length }));
|
||||
if (removed.length > 0) changes.push(t("events.push.removed", { count: removed.length }));
|
||||
if (modified.length > 0) changes.push(t("events.push.modified", { count: modified.length }));
|
||||
fields.push({
|
||||
name: t("fields.changes"),
|
||||
value: changes.join(" | "),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.push.title", {
|
||||
count,
|
||||
s: count !== 1 ? "s" : "",
|
||||
repo: repo ?? t("common.repository"),
|
||||
}),
|
||||
url: compareUrl,
|
||||
color: GITHUB_COLORS.push,
|
||||
description: descriptionParts.join("\n"),
|
||||
fields: fields.length > 0 ? fields : undefined,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
62
server/lib/formatters/release.ts
Normal file
62
server/lib/formatters/release.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatRelease(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "published";
|
||||
const release = payload.release as {
|
||||
tag_name?: string;
|
||||
name?: string;
|
||||
html_url?: string;
|
||||
body?: string;
|
||||
prerelease?: boolean;
|
||||
draft?: boolean;
|
||||
author?: { login?: string };
|
||||
};
|
||||
|
||||
const isPrerelease = release.prerelease;
|
||||
const colorKey =
|
||||
action === "deleted"
|
||||
? "release_deleted"
|
||||
: isPrerelease
|
||||
? "release_prerelease"
|
||||
: "release_published";
|
||||
const al = t("actions." + action) ?? action;
|
||||
const emoji = action === "deleted" ? "🗑️" : isPrerelease ? "⚠️" : "🚀";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const descriptionParts: string[] = [];
|
||||
descriptionParts.push(
|
||||
t("events.release.action_release", {
|
||||
emoji: em(emoji),
|
||||
action: al,
|
||||
tag: release.tag_name ?? t("common.unknown"),
|
||||
}),
|
||||
);
|
||||
|
||||
if (release.body) {
|
||||
const truncated = release.body.slice(0, 300);
|
||||
descriptionParts.push(`\n${truncated}${release.body.length > 300 ? "..." : ""}`);
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.release.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
name: release.name ?? release.tag_name ?? "Release",
|
||||
}),
|
||||
url: release.html_url,
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
description: descriptionParts.join("\n"),
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
58
server/lib/formatters/repo.ts
Normal file
58
server/lib/formatters/repo.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatStar(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
repoUrl: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const actionLabel = action === "created" ? t("events.star.starred") : t("events.star.unstarred");
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.star.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
emoji: em(action === "created" ? "⭐️" : "💫"),
|
||||
label: actionLabel,
|
||||
}),
|
||||
url: repoUrl,
|
||||
color: GITHUB_COLORS.star,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatFork(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
repoUrl: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const forkee = payload.forkee as { full_name?: string; html_url?: string } | undefined;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.fork.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
emoji: em("🍴"),
|
||||
forkee: forkee?.full_name ?? t("common.unknown"),
|
||||
}),
|
||||
url: forkee?.html_url ?? repoUrl,
|
||||
color: GITHUB_COLORS.fork,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
92
server/lib/formatters/repository.ts
Normal file
92
server/lib/formatters/repository.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatRepository(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
repoUrl: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
if (action === "renamed") {
|
||||
const changes = payload.changes as { name?: { from?: string } } | undefined;
|
||||
const newName =
|
||||
(payload.repository as { full_name?: string })?.full_name ?? t("common.unknown");
|
||||
fields.push({
|
||||
name: t("fields.renamed"),
|
||||
value: changes?.name?.from ? `${changes.name.from} → ${newName}` : newName,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (action === "transferred") {
|
||||
const changes = payload.changes as { owner?: { from?: { login?: string } } } | undefined;
|
||||
const newOwner =
|
||||
(payload.repository as { owner?: { login?: string } })?.owner?.login ?? t("common.unknown");
|
||||
fields.push({
|
||||
name: t("fields.transferred"),
|
||||
value: changes?.owner?.from?.login ? `${changes.owner.from.login} → ${newOwner}` : newOwner,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
const repoData = payload.repository as {
|
||||
visibility?: string;
|
||||
fork?: boolean;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
const isCreateOrVisibility =
|
||||
action === "created" || action === "publicized" || action === "privatized";
|
||||
|
||||
if (isCreateOrVisibility) {
|
||||
if (repoData.visibility) {
|
||||
fields.push({
|
||||
name: t("events.repository.visibility"),
|
||||
value: t("events.repository." + repoData.visibility) ?? repoData.visibility,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
if (repoData.fork) {
|
||||
fields.push({
|
||||
name: t("common.repository"),
|
||||
value: t("events.repository.is_fork"),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const descriptionParts: string[] = [];
|
||||
if (isCreateOrVisibility && repoUrl) {
|
||||
descriptionParts.push(`[${em("🔗")}${t("events.repository.open")}](${repoUrl})`);
|
||||
}
|
||||
if (isCreateOrVisibility && repoData.description) {
|
||||
descriptionParts.push(`> ${repoData.description}`);
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.repository.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
emoji: em("📦"),
|
||||
action: al,
|
||||
}),
|
||||
url: repoUrl,
|
||||
color: GITHUB_COLORS.repository,
|
||||
description: descriptionParts.length > 0 ? descriptionParts.join("\n") : undefined,
|
||||
fields: fields.length > 0 ? fields : undefined,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
114
server/lib/formatters/review.ts
Normal file
114
server/lib/formatters/review.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatPullRequestReview(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "submitted";
|
||||
const review = payload.review as {
|
||||
state?: string;
|
||||
body?: string;
|
||||
html_url?: string;
|
||||
};
|
||||
const pr = payload.pull_request as {
|
||||
number?: number;
|
||||
title?: string;
|
||||
html_url?: string;
|
||||
};
|
||||
|
||||
const state = review.state ?? "commented";
|
||||
const colorKey =
|
||||
state === "approved"
|
||||
? "pull_request_review_approved"
|
||||
: state === "changes_requested"
|
||||
? "pull_request_review_changes"
|
||||
: "pull_request_review_commented";
|
||||
|
||||
const stateEmoji = state === "approved" ? "✅" : state === "changes_requested" ? "🔴" : "💬";
|
||||
const al = t("actions." + action) ?? state;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const descriptionParts: string[] = [];
|
||||
descriptionParts.push(t("events.pr_review.action_review", { emoji: em(stateEmoji), action: al }));
|
||||
|
||||
if (review.body) {
|
||||
const truncated = review.body.slice(0, 500);
|
||||
descriptionParts.push(`\n> ${truncated}${review.body.length > 500 ? "..." : ""}`);
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.pr_review.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
number: pr.number ?? "?",
|
||||
title: pr.title ?? t("common.untitled"),
|
||||
}),
|
||||
url: review.html_url,
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
description: descriptionParts.join("\n"),
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatPullRequestReviewComment(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const comment = payload.comment as {
|
||||
body?: string;
|
||||
path?: string;
|
||||
position?: number | null;
|
||||
html_url?: string;
|
||||
};
|
||||
const pr = payload.pull_request as {
|
||||
number?: number;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const al = t("actions." + action) ?? action;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const commentBody = comment.body?.slice(0, 400) ?? "";
|
||||
const truncated = comment.body && comment.body.length > 400;
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
if (comment.path) {
|
||||
const loc =
|
||||
comment.position != null
|
||||
? t("events.pr_review_comment.line", { position: comment.position })
|
||||
: "";
|
||||
fields.push({
|
||||
name: t("fields.file"),
|
||||
value: `\`${comment.path}\`${loc}`,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.pr_review_comment.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
number: pr.number ?? "?",
|
||||
title: pr.title ?? t("common.untitled"),
|
||||
}),
|
||||
url: comment.html_url,
|
||||
color: GITHUB_COLORS.pull_request_review_commented,
|
||||
description: `${t("events.pr_review_comment.action_inline", { emoji: em("💬"), action: al })}\n\n> ${commentBody}${truncated ? "..." : ""}`,
|
||||
fields: fields.length > 0 ? fields : undefined,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
173
server/lib/formatters/security.ts
Normal file
173
server/lib/formatters/security.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage } from "./helpers";
|
||||
|
||||
export function formatCodeScanningAlert(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const alert = payload.alert as {
|
||||
rule?: { id?: string; severity?: string; description?: string };
|
||||
most_recent_instance?: { location?: { path?: string } };
|
||||
state?: string;
|
||||
};
|
||||
|
||||
const severity = alert.rule?.severity ?? "warning";
|
||||
const colorKey =
|
||||
severity === "critical"
|
||||
? "code_scanning_critical"
|
||||
: severity === "high"
|
||||
? "code_scanning_high"
|
||||
: severity === "medium"
|
||||
? "code_scanning_medium"
|
||||
: "code_scanning_low";
|
||||
|
||||
const severityEmoji =
|
||||
severity === "critical"
|
||||
? "🔴"
|
||||
: severity === "high"
|
||||
? "🟠"
|
||||
: severity === "medium"
|
||||
? "🟡"
|
||||
: "⚪";
|
||||
const al = t("actions." + action) ?? action;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.severity"),
|
||||
value: `${em(severityEmoji)}${severity}`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
if (alert.rule?.id) {
|
||||
fields.push({
|
||||
name: t("fields.rule"),
|
||||
value: alert.rule.id,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (alert.most_recent_instance?.location?.path) {
|
||||
fields.push({
|
||||
name: t("fields.file"),
|
||||
value: `\`${alert.most_recent_instance.location.path}\``,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (alert.rule?.description) {
|
||||
fields.push({
|
||||
name: t("fields.description"),
|
||||
value: alert.rule.description,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.code_scanning.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
action: al,
|
||||
}),
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
fields,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatDependabotAlert(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const action = (payload.action as string) ?? "created";
|
||||
const alert = payload.alert as {
|
||||
security_advisory?: { severity?: string; summary?: string; description?: string };
|
||||
security_vulnerability?: {
|
||||
package?: { name?: string };
|
||||
vulnerable_version_range?: string;
|
||||
first_patched_version?: { identifier?: string };
|
||||
};
|
||||
state?: string;
|
||||
dependency?: { package?: { name?: string } };
|
||||
html_url?: string;
|
||||
};
|
||||
|
||||
const severity = alert.security_advisory?.severity ?? "medium";
|
||||
const colorKey =
|
||||
severity === "critical"
|
||||
? "dependabot_critical"
|
||||
: severity === "high"
|
||||
? "dependabot_high"
|
||||
: severity === "medium"
|
||||
? "dependabot_medium"
|
||||
: "dependabot_low";
|
||||
|
||||
const severityEmoji =
|
||||
severity === "critical"
|
||||
? "🔴"
|
||||
: severity === "high"
|
||||
? "🟠"
|
||||
: severity === "medium"
|
||||
? "🟡"
|
||||
: "⚪";
|
||||
const al = t("actions." + action) ?? action;
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.severity"),
|
||||
value: `${em(severityEmoji)}${severity}`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
const pkgName = alert.security_vulnerability?.package?.name ?? alert.dependency?.package?.name;
|
||||
if (pkgName) {
|
||||
fields.push({
|
||||
name: t("fields.package"),
|
||||
value: pkgName,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (alert.security_vulnerability?.vulnerable_version_range) {
|
||||
const patched = alert.security_vulnerability.first_patched_version?.identifier;
|
||||
fields.push({
|
||||
name: t("fields.vulnerable_range"),
|
||||
value: `${alert.security_vulnerability.vulnerable_version_range}${patched ? ` → fix: \`${patched}\`` : ""}`,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (alert.security_advisory?.summary) {
|
||||
fields.push({
|
||||
name: t("fields.summary"),
|
||||
value: alert.security_advisory.summary,
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.dependabot.title", { repo: repo ?? t("common.repository"), action: al }),
|
||||
url: alert.html_url,
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
fields,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
194
server/lib/formatters/workflow.ts
Normal file
194
server/lib/formatters/workflow.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import type { NeutralMessage, NeutralAuthor } from "../types";
|
||||
import { GITHUB_COLORS, WORKFLOW_CONCLUSION_EMOJI } from "./colors";
|
||||
import { emojiPrefix, type T, buildMessage, branchLink, commitLink, repoBaseUrl } from "./helpers";
|
||||
|
||||
export function formatWorkflowJob(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const job = payload.workflow_job as {
|
||||
name?: string;
|
||||
status?: string;
|
||||
conclusion?: string | null;
|
||||
head_branch?: string;
|
||||
head_sha?: string;
|
||||
html_url?: string;
|
||||
workflow_name?: string;
|
||||
run_id?: number;
|
||||
};
|
||||
|
||||
const status =
|
||||
job.status === "queued"
|
||||
? "queued"
|
||||
: job.status === "in_progress"
|
||||
? "running"
|
||||
: (job.conclusion ?? "pending");
|
||||
const emoji = WORKFLOW_CONCLUSION_EMOJI[status] ?? "⏳";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
const colorKey =
|
||||
status === "success"
|
||||
? "workflow_run_success"
|
||||
: status === "failure"
|
||||
? "workflow_run_failure"
|
||||
: "workflow_run_other";
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.status"),
|
||||
value: `${em(emoji)}${status}`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
if (job.name) {
|
||||
fields.push({
|
||||
name: t("fields.job"),
|
||||
value: job.name,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (job.workflow_name) {
|
||||
fields.push({
|
||||
name: t("fields.workflow"),
|
||||
value: job.workflow_name,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (job.head_branch) {
|
||||
fields.push({
|
||||
name: t("fields.branch"),
|
||||
value: branchLink(baseUrl, job.head_branch),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (job.head_sha) {
|
||||
fields.push({
|
||||
name: t("fields.commit"),
|
||||
value: commitLink(baseUrl, job.head_sha),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.workflow_job.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
name: job.name ?? "Job",
|
||||
conclusion: status,
|
||||
}),
|
||||
url: job.html_url,
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
fields,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
export function formatWorkflowRun(
|
||||
payload: Record<string, unknown>,
|
||||
repo: string | undefined,
|
||||
author: NeutralAuthor,
|
||||
t: T,
|
||||
showEmoji: boolean,
|
||||
): NeutralMessage {
|
||||
const workflow = payload.workflow_run as {
|
||||
id?: number;
|
||||
name?: string;
|
||||
conclusion?: string;
|
||||
html_url?: string;
|
||||
head_branch?: string;
|
||||
run_number?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
elapsed_seconds?: number;
|
||||
jobs?: Array<{ name?: string; conclusion?: string }>;
|
||||
};
|
||||
|
||||
const action = payload.action as string | undefined;
|
||||
const status =
|
||||
action === "in_progress"
|
||||
? "running"
|
||||
: action === "requested"
|
||||
? "queued"
|
||||
: (workflow.conclusion ?? "pending");
|
||||
const emoji = WORKFLOW_CONCLUSION_EMOJI[status] ?? "⏳";
|
||||
const em = (e: string): string => emojiPrefix(e, showEmoji);
|
||||
const baseUrl = repoBaseUrl(payload, repo);
|
||||
const colorKey =
|
||||
status === "success"
|
||||
? "workflow_run_success"
|
||||
: status === "failure"
|
||||
? "workflow_run_failure"
|
||||
: "workflow_run_other";
|
||||
|
||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
||||
|
||||
fields.push({
|
||||
name: t("fields.status"),
|
||||
value: `${em(emoji)}${status}`,
|
||||
inline: true,
|
||||
});
|
||||
|
||||
if (workflow.jobs?.length) {
|
||||
const jobLines = workflow.jobs.map(
|
||||
(j) => `${em(WORKFLOW_CONCLUSION_EMOJI[j.conclusion ?? ""] ?? "⏳")}${j.name ?? ""}`,
|
||||
);
|
||||
fields.push({
|
||||
name: t("fields.job"),
|
||||
value: jobLines.join("\n"),
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (workflow.head_branch) {
|
||||
fields.push({
|
||||
name: t("fields.branch"),
|
||||
value: branchLink(baseUrl, workflow.head_branch),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (workflow.run_number) {
|
||||
fields.push({
|
||||
name: t("fields.run"),
|
||||
value: `#${workflow.run_number}`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (workflow.elapsed_seconds != null) {
|
||||
const mins = Math.floor(workflow.elapsed_seconds / 60);
|
||||
const secs = workflow.elapsed_seconds % 60;
|
||||
fields.push({
|
||||
name: t("fields.duration"),
|
||||
value: `${mins}m ${secs}s`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return buildMessage(
|
||||
{
|
||||
author,
|
||||
title: t("events.workflow_run.title", {
|
||||
repo: repo ?? t("common.repository"),
|
||||
name: workflow.name ?? "Workflow",
|
||||
conclusion: status,
|
||||
}),
|
||||
url: workflow.html_url,
|
||||
color: GITHUB_COLORS[colorKey],
|
||||
fields,
|
||||
updateKey: repo && workflow.id != null ? `workflow_run:${repo}:${workflow.id}` : undefined,
|
||||
},
|
||||
t,
|
||||
repo,
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue