feat(groups): optional forge source label in message footers

- Group.forgeLabel (default off, editable per group in the console) shows the
  source forge on every message the group's routes dispatch
- Discord embed footer: forge name next to the repo + the site's favicon as
  icon_url (Gitea instance's own favicon from its origin)
- Telegram footer: hyperlinked site name (GitHub or Gitea hostname); custom
  webhooks render a plain 'Custom' label
- forgeInfo() derives branding from event.provider + repository.html_url;
  dispatch attaches it to the NeutralMessage like mentionRoleIds
- validateGroups accepts forgeLabel booleans; GroupEditor gains the toggle
- tests: forgeInfo unit, discord/telegram footer render, dispatch on/off
- docs: groups.md + message-format.md (en/zh), config.example.yaml, AGENTS.md
- fix: wrangler.jsonc compatibility_date was an incomplete '2026-'
This commit is contained in:
RhenCloud 2026-08-14 08:10:04 +08:00
parent a5324fb0ea
commit 3dce114cec
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
19 changed files with 273 additions and 16 deletions

View file

@ -83,6 +83,22 @@ describe("discord role mentions", () => {
const out = renderNeutralMessage({ title: "T" });
expect(out.content).toBeUndefined();
});
it("renders the forge source in the embed footer", () => {
const out = renderNeutralMessage({
title: "acme/widget: Add feature",
footer: "acme/widget",
forge: {
name: "GitHub",
url: "https://github.com",
iconUrl: "https://github.com/favicon.ico",
},
});
expect(out.embeds?.[0]?.footer).toEqual({
text: "GitHub · acme/widget",
icon_url: "https://github.com/favicon.ico",
});
});
});
describe("dispatchEvent fallback routing", () => {
@ -369,6 +385,72 @@ describe("dispatchEvent fallback routing", () => {
expect(logBody.embeds?.[0]?.fields?.[0]?.value).toContain("❌ Push Route → 111");
});
it("attaches the forge label when the group enables it", async () => {
const bodies: string[] = [];
mockFetch((url, init) => {
bodies.push(String(init?.body ?? ""));
return new Response("{}", { status: 200 });
});
const kv = createMockKV();
await kv.put(
"config:groups",
JSON.stringify([
{ id: "gh", name: "GH", adminIds: [], forgeLabel: true },
{ id: "plain", name: "Plain", adminIds: [] },
]),
);
const env = createEnv({ KV: kv, DB: createMockDB() });
const routes: Route[] = [
{
id: "gitea-route",
name: "Gitea Route",
enabled: true,
groupId: "gh",
filters: [{ type: "event", match: "push" }],
targets: [{ channelId: "111" }],
},
{
id: "plain-route",
name: "Plain Route",
enabled: true,
groupId: "plain",
filters: [{ type: "event", match: "push" }],
targets: [{ channelId: "222" }],
},
];
await dispatchEvent(
{ ...baseConfig, routes },
{
event: "push",
provider: "gitea",
payload: {
repository: {
full_name: "owner/repo",
html_url: "https://git.example.com/owner/repo",
},
ref: "refs/heads/main",
commits: [{ id: "abc", message: "fix" }],
sender: { login: "octo" },
},
},
env,
);
expect(bodies).toHaveLength(2);
const footers = bodies.map((b) => {
const parsed = JSON.parse(b) as {
embeds?: Array<{ footer?: { text: string; icon_url?: string } }>;
};
return parsed.embeds?.[0]?.footer;
});
expect(footers).toContainEqual({
text: "git.example.com · owner/repo",
icon_url: "https://git.example.com/favicon.ico",
});
expect(footers).toContainEqual({ text: "owner/repo" });
});
it("sends no group log when no route matched the event", async () => {
const sent: string[] = [];
mockFetch((url, init) => {

View file

@ -1,6 +1,6 @@
import { describe, it, expect } from "bun:test";
import { formatEvent } from "../server/lib/formatters";
import { MAX_COMMIT_SUBJECT } from "../server/lib/formatters/helpers";
import { forgeInfo, MAX_COMMIT_SUBJECT } from "../server/lib/formatters/helpers";
import type { Route, WebhookEvent } from "../server/lib/types";
const route: Route = {
@ -518,3 +518,38 @@ describe("limits and localization", () => {
expect(msg.description!.length).toBe(4096);
});
});
describe("forge source branding", () => {
it("github events brand as GitHub with the favicon", () => {
expect(
forgeInfo({ event: "push", provider: "github", payload: {} }),
).toEqual({
name: "GitHub",
url: "https://github.com",
iconUrl: "https://github.com/favicon.ico",
});
});
it("gitea events brand as the instance hostname derived from the repo url", () => {
expect(
forgeInfo({
event: "push",
provider: "gitea",
payload: { repository: { html_url: "https://git.example.com/org/repo" } },
}),
).toEqual({
name: "git.example.com",
url: "https://git.example.com",
iconUrl: "https://git.example.com/favicon.ico",
});
expect(
forgeInfo({ event: "push", provider: "gitea", payload: {} }),
).toBeUndefined();
});
it("custom events brand as a plain label without links", () => {
expect(forgeInfo({ event: "custom", provider: "custom", payload: {} })).toEqual({
name: "Custom",
});
});
});

View file

@ -31,6 +31,23 @@ describe("telegram renderNeutralMessage", () => {
expect(out).toContain("<i>acme/widget</i>");
});
it("renders the forge source as a linked name in the footer", () => {
const out = renderNeutralMessage({
title: "acme/widget: Add feature",
footer: "acme/widget",
forge: { name: "GitHub", url: "https://github.com", iconUrl: "https://github.com/favicon.ico" },
});
expect(out).toContain(
'<i><a href="https://github.com">GitHub</a> · acme/widget</i>',
);
const gitea = renderNeutralMessage({
title: "org/repo: Add feature",
forge: { name: "git.example.com", url: "https://git.example.com" },
});
expect(gitea).toContain('<i><a href="https://git.example.com">git.example.com</a></i>');
});
it("escapes HTML special characters", () => {
const out = renderNeutralMessage({
title: 'a <b> & "c"',