diff --git a/AGENTS.md b/AGENTS.md index d119558..2738969 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,7 +163,7 @@ tests/__snapshots__/ # formatter snapshot golden files (toMatchSnapshot) - Format 28 GitHub/Gitea event types plus `custom` webhooks as platform-neutral messages (Discord embeds + Telegram HTML) - Show the forge source (named per `Group.forgeSources` host entries) in the message footer when the group defines a host matching the event's repository - Route messages to Discord channels/threads and Telegram chats/topics via REST -- Edit already-sent messages in place for `workflow_run` / `check_run` progress (stable `updateKey`, `message_tracking` via D1 with KV `msg:*` fallback) +- Edit already-sent messages in place for `workflow_run` / `check_run` progress (stable `updateKey`, `message_tracking` via D1 with KV `msg:*` fallback); `workflow_run` keys on the workflow id while `check_run` keys on check name + commit SHA (falling back to the run id) so providers that emit a fresh run id per phase — e.g. Cloudflare Pages — still edit the same message - Record every dispatch attempt to D1 `send_logs` (route id, event, target, ok/error, duration, error code) - Aggregate delivery metrics (`server/lib/observability/metrics.ts`) from `send_logs` — totals, ok/failed counts + failure rate, per-platform/per-event/per-status breakdowns, average duration and attempts, recent failures; `getDeliveryMetrics(db, groupId?)` scopes every query by `group_id` when a group is passed - Expose admin observability endpoints — `GET /admin/api/metrics?groupId=` (delivery metrics, optional group scope; recent failures group-scoped for non-super) and `GET /admin/api/delivery/:deliveryId` (all send-log attempts for one delivery, group-scoped) — through the `/admin/api/[...slug]` catch-all route (`server/routes/admin/api/[...slug].ts`) that wires every admin API handler to its method+path diff --git a/README.md b/README.md index 88e357d..261f370 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ GitHub / Gitea webhook → Discord / Telegram / Feishu dispatcher. Receives webh - Combine filters into a boolean AST (`all` / `any` / `not` nodes) via the route editor's visual builder; reuse named filter fragments and dry-run any filter against a pasted JSON payload without storing it - Rich messages with color coding, author avatars, fields, and timestamps — rendered as Discord embeds and Telegram HTML - Route to Discord channels/threads, Telegram chats/topics, and Feishu group chats (multi-target routes) -- `workflow_run` / `check_run` progress is edited **in place** (single message updated as the run advances) on Discord, Telegram, and Feishu +- `workflow_run` / `check_run` progress is edited **in place** (single message updated as the run advances) on Discord, Telegram, and Feishu — `check_run` tracks by check name + commit SHA so deployments that issue a fresh run id per phase (e.g. Cloudflare Pages) still edit the same message - **Per-group webhook log channel** — point a group at a Discord channel/thread, Telegram chat/topic, or Feishu chat and every webhook the group's routes dispatch is summarized there (✅/❌ per route × target) - GitHub OAuth for user actions (comment, edit comment, delete comment, merge, close, react) - **Web UI config console** (`/admin`) — manage routes and groups with GitHub OAuth + admin whitelist, view send logs diff --git a/README.zh.md b/README.zh.md index 81df76a..ce93e4a 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,7 +13,7 @@ GitHub / Gitea webhook → Discord / Telegram / 飞书 分发服务。通过 Clo - 在路由编辑器的可视化构建器中把过滤器组合成布尔 AST(`all` / `any` / `not` 节点);可复用命名过滤器片段,并可对粘贴的 JSON 载荷做无存储的试匹配 - 富消息:颜色编码、作者头像、字段、时间戳——渲染为 Discord embed 与 Telegram HTML - 路由到 Discord 频道/子区、Telegram 群组/话题与飞书群聊(一条路由可多目标) -- `workflow_run` / `check_run` 进度**原地编辑**同一条消息(运行推进时更新),Discord、Telegram 与飞书均支持 +- `workflow_run` / `check_run` 进度**原地编辑**同一条消息(运行推进时更新),Discord、Telegram 与飞书均支持 —— `check_run` 以 check 名 + commit SHA 追踪,因此每个阶段都换新 run id 的部署(如 Cloudflare Pages)仍会编辑同一条消息 - **分组级 Webhook 日志频道** —— 为分组指定一个 Discord 频道/子区、Telegram 群组/话题或飞书群聊,该分组路由每次分发 webhook 都会向其中发送摘要(每条「路由 × 目标」一行,✅/❌ 结果) - GitHub OAuth 用户授权(评论、编辑评论、删除评论、合并、关闭、反应) - **Web 配置控制台**(`/admin`)— 通过 GitHub OAuth + 管理员白名单管理路由与分组、查看发送日志 diff --git a/docs/events/supported.md b/docs/events/supported.md index 7e40db1..9b413e6 100644 --- a/docs/events/supported.md +++ b/docs/events/supported.md @@ -60,7 +60,7 @@ Any event type without a dedicated formatter falls through to the generic format ## In-Place Message Updates -`workflow_run` and `check_run` events (queued → running → success/failure) are sent once and then **edited in place** for each subsequent phase instead of posting a new message. The original message's link preview, author, and field layout are preserved; only the status, conclusion emoji, duration, and title are refreshed. Supported on both Discord (`editMessage`) and Telegram (`editMessageText` / `editMessageCaption`). +`workflow_run` and `check_run` events (queued → running → success/failure) are sent once and then **edited in place** for each subsequent phase instead of posting a new message. The original message's link preview, author, and field layout are preserved; only the status, conclusion emoji, duration, and title are refreshed. Supported on both Discord (`editMessage`) and Telegram (`editMessageText` / `editMessageCaption`). `check_run` tracks by check name + commit SHA (falling back to the run id), so providers that issue a new run id per phase — e.g. Cloudflare Pages — still edit the same message. ## Filter Compatibility diff --git a/docs/guide/message-format.md b/docs/guide/message-format.md index 9bd79f3..1a1fdc8 100644 --- a/docs/guide/message-format.md +++ b/docs/guide/message-format.md @@ -33,4 +33,4 @@ With `Group.forgeSources` (a list of `{ host, type, name? }` entries) the messag ## In-Place Updates -`workflow_run` and `check_run` messages are sent once and edited in place as the run progresses (queued → running → success/failure) — no duplicate messages. Tracking uses KV `msg:*` with a stable `updateKey` per run. +`workflow_run` and `check_run` messages are sent once and edited in place as the run progresses (queued → running → success/failure) — no duplicate messages. Tracking uses KV `msg:*` with a stable `updateKey` per run: `workflow_run` keys on the workflow id, while `check_run` keys on the check name + commit SHA (falling back to the run id) so deployments that emit a fresh run id per phase — e.g. Cloudflare Pages — still edit the same message. diff --git a/docs/zh/events/supported.md b/docs/zh/events/supported.md index 0f22953..e5ab66d 100644 --- a/docs/zh/events/supported.md +++ b/docs/zh/events/supported.md @@ -60,7 +60,7 @@ WebHooker 支持 28 种 GitHub webhook 事件类型,每种都有专用的格 ## 原地消息更新 -`workflow_run` / `check_run` 事件(queued → running → success/failure)只发送一条消息,后续每个阶段会**原地编辑**该消息,而不是发送新消息。消息的链接预览、作者和字段布局保持不变,仅刷新状态、结论 emoji、耗时和标题。Discord(`editMessage`)和 Telegram(`editMessageText` / `editMessageCaption`)均支持。 +`workflow_run` / `check_run` 事件(queued → running → success/failure)只发送一条消息,后续每个阶段会**原地编辑**该消息,而不是发送新消息。消息的链接预览、作者和字段布局保持不变,仅刷新状态、结论 emoji、耗时和标题。Discord(`editMessage`)和 Telegram(`editMessageText` / `editMessageCaption`)均支持。`check_run` 以 check 名 + commit SHA 追踪(回退到 run id),因此每个阶段都会换新 run id 的提供方(例如 Cloudflare Pages)仍会编辑同一条消息。 ## 过滤器兼容性 diff --git a/docs/zh/guide/message-format.md b/docs/zh/guide/message-format.md index 28bb87b..fb0dd65 100644 --- a/docs/zh/guide/message-format.md +++ b/docs/zh/guide/message-format.md @@ -33,4 +33,4 @@ ## 原地更新 -`workflow_run` 与 `check_run` 消息只发送一次,随运行进度原地编辑(queued → running → success/failure),不会重复发消息。追踪使用 KV `msg:*` 与每次运行的稳定 `updateKey`。 +`workflow_run` 与 `check_run` 消息只发送一次,随运行进度原地编辑(queued → running → success/failure),不会重复发消息。追踪使用 KV `msg:*` 与每次运行的稳定 `updateKey`:`workflow_run` 以 workflow id 为键,`check_run` 则以 check 名 + commit SHA 为键(回退到 run id),因此每个阶段都换新 run id 的部署(例如 Cloudflare Pages)仍会编辑同一条消息。 diff --git a/server/lib/formatters/check.ts b/server/lib/formatters/check.ts index 31965d7..ef2f55e 100644 --- a/server/lib/formatters/check.ts +++ b/server/lib/formatters/check.ts @@ -178,6 +178,7 @@ export function formatCheckRun( const checkRun = payload.check_run as { id?: number; name?: string; + head_sha?: string; conclusion?: string; html_url?: string; details_url?: string; @@ -225,7 +226,12 @@ export function formatCheckRun( url: checkRun.html_url, color: GITHUB_COLORS[colorKey], fields, - updateKey: repo && checkRun.id != null ? `check_run:${repo}:${checkRun.id}` : undefined, + updateKey: + repo && checkRun.name && checkRun.head_sha + ? `check_run:${repo}:${checkRun.name}:${checkRun.head_sha}` + : repo && checkRun.id != null + ? `check_run:${repo}:${checkRun.id}` + : undefined, }, t, repo, diff --git a/tests/formatter.test.ts b/tests/formatter.test.ts index e326cdc..8abad49 100644 --- a/tests/formatter.test.ts +++ b/tests/formatter.test.ts @@ -183,6 +183,39 @@ describe("message title spec", () => { expect(msg.updateKey).toBeUndefined(); }); + it("check_run keys on name + head_sha for stable in-place edits", () => { + const running = formatEvent( + route, + event("check_run", { + check_run: { + id: 42, + name: "Cloudflare Pages", + head_sha: "abc123def456", + status: "in_progress", + conclusion: null, + }, + repository: repo, + sender, + }), + ); + const failed = formatEvent( + route, + event("check_run", { + check_run: { + id: 43, + name: "Cloudflare Pages", + head_sha: "abc123def456", + status: "completed", + conclusion: "failure", + }, + repository: repo, + sender, + }), + ); + expect(running.updateKey).toBe("check_run:acme/widget:Cloudflare Pages:abc123def456"); + expect(failed.updateKey).toBe(running.updateKey); + }); + it("check_suite shows conclusion, service, branch and commit", () => { const suite = { html_url: "https://github.com/acme/widget/runs/2",