fix(formatters): localize PR buttons, cap oversized content, dedupe status logic

- PR merge/close button labels now follow the group language (actions.merge/close)
  instead of hardcoded Chinese
- clamp content to Discord embed limits (title 256, description 4096, field value
  1024, 25 fields) in formatters plus a render-layer safety net; Telegram gets a
  tag-safe 4096-char cap (capHtml closes dangling tags)
- raise commit subject truncation from 72 to 200 chars (MAX_COMMIT_SUBJECT)
- extract workflowStatus/workflowRunStatus/statusColorKey helpers shared by
  check_run/check_suite/workflow_run/workflow_job
- dedupe deployment ref/sha fields via addDeploymentRefFields
- commit_comment without a commit id uses title_plain (no dangling ???????)
- tag pushes now report 'Tag created'; zh push title includes the {ref}
- sender profile link derives from the repo's forge origin instead of github.com
- group webhook log emoji injected via emojiPrefix, removed from locale files
- dispatchEvent accepts preloaded groups (single KV read per webhook)
- webhook 401 logs distinguish 'secret not configured' from 'invalid signature'
This commit is contained in:
RhenCloud 2026-08-14 07:41:19 +08:00
parent e59b10f739
commit a5324fb0ea
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
20 changed files with 425 additions and 136 deletions

View file

@ -99,3 +99,64 @@ export function tagLink(baseUrl: string | undefined, tag: string, label?: string
? `[\`${display}\`](${baseUrl}/releases/tag/${encodeRefPath(clean)})`
: `\`${display}\``;
}
/* ---- Content size limits (mirror the Discord embed limits) ---- */
export const MAX_TITLE = 256;
export const MAX_DESCRIPTION = 4096;
export const MAX_FIELD_VALUE = 1024;
export const MAX_FIELDS = 25;
export const MAX_FOOTER = 2048;
/** First-line commit message length (stays well under the field value budget). */
export const MAX_COMMIT_SUBJECT = 200;
/** Truncate a string to `max` characters. */
export function cap(text: string, max: number): string {
return text.length > max ? text.slice(0, max) : text;
}
/**
* Normalize a check/workflow status: `queued` / `in_progress` keep their
* value (the latter becomes `running`), anything else falls back to the
* conclusion and finally `pending`.
*/
export function workflowStatus(status?: string, conclusion?: string): string {
if (status === "queued") return "queued";
if (status === "in_progress") return "running";
return conclusion ?? "pending";
}
/** workflow_run events derive their progress from the action, not `status`. */
export function workflowRunStatus(action?: string, conclusion?: string): string {
if (action === "in_progress") return "running";
if (action === "requested") return "queued";
return conclusion ?? "pending";
}
/** GITHUB_COLORS key for a success/failure/other status. */
export function statusColorKey(
prefix: "check_run" | "workflow_run",
status: string,
): `${typeof prefix}_${"success" | "failure" | "other"}` {
return status === "success"
? `${prefix}_success`
: status === "failure"
? `${prefix}_failure`
: `${prefix}_other`;
}
/**
* The sender's profile URL on the same forge as the repo (derived from the
* repo's html_url origin, e.g. `https://github.com/owner/repo`
* `https://github.com/login`). Falls back to github.com when the repo URL is
* unavailable or unparseable.
*/
export function senderProfileUrl(repoUrl: string | undefined, login: string): string {
if (repoUrl) {
try {
return `${new URL(repoUrl).origin}/${encodeURIComponent(login)}`;
} catch {
// unparseable repo URL — fall through to github.com
}
}
return `https://github.com/${login}`;
}