fix(check): key check_run in-place edits on check name + head_sha

Providers that emit a fresh check run id per phase (e.g. Cloudflare Pages)
no longer post a new message for each status change; same name + commit
SHA now edits the existing message. Falls back to the run id when name or
head_sha is absent. Docs updated in sync.
This commit is contained in:
RhenCloud 2026-08-31 01:17:13 +08:00
parent cc8bb4182f
commit fd6ccda411
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
9 changed files with 47 additions and 8 deletions

View file

@ -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

View file

@ -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

View file

@ -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 + 管理员白名单管理路由与分组、查看发送日志

View file

@ -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

View file

@ -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.

View file

@ -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仍会编辑同一条消息。
## 过滤器兼容性

View file

@ -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仍会编辑同一条消息

View file

@ -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,

View file

@ -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",