CI/CD 跑完了结果要在后台刷半天?GitHub Actions 这次把通知直接弹到桌面上了
你的 CI/CD 流水线跑完了,你在哪一步知道结果的?
大多数人的答案是:打开邮箱。或者打开 Slack 等通知。或者——什么通知都没有,跑完自己去平台上看。
这几个方案都有问题。邮箱有延迟,而且容易被当作垃圾邮件。Slack 消息太多,CI 成功的消息很快就被顶上去了。自己去平台上看,最不靠谱,等你发现失败的时候,可能已经过了两个小时。
现在,GitHub Actions 的工作流状态可以直接弹到你桌面上了。跑完一条流水线,Mac/Windows 的通知中心直接弹出来告诉你成功还是失败。不用打开任何页面,不用刷新邮箱,不用盯着 Slack。
实现这个效果,需要 GitHub Webhook + SSE 中转 + Notification API 三层串联。GitHub 负责触发,SSE 负责实时推送,Notification 负责在操作系统层面展示。
第一步:让 GitHub 在工作流跑完时通知你的服务。GitHub Actions 原生支持 Webhook,工作流完成时会向你指定的 URL 发送一个 POST 请求。
// GitHub Webhook 接收端(Express 示例)
app.post('/webhook/github-actions', (req, res) => {
const payload = req.body;
const event = req.headers['x-github-event'];
// 只处理 workflow_run 事件
if (event !== 'workflow_run') return res.json({ ok: true });
const { action, workflow_run } = payload;
if (action !== 'completed') return res.json({ ok: true });
const status = workflow_run.conclusion; // success / failure / cancelled
const repo = workflow_run.repository.full_name;
const workflow = workflow_run.name;
const branch = workflow_run.head_branch;
// 立即通过 SSE 推送给所有在线的浏览器客户端
broadcast({
type: 'workflow_complete',
status,
repo,
workflow,
branch,
url: workflow_run.html_url,
time: new Date().toISOString()
});
res.json({ ok: true });
});
这一步的关键是 broadcast 函数——它把 GitHub 发来的事件转发给所有当前在线的 SSE 客户端。不需要数据库,不需要队列,GitHub 已经帮你做了重试,你只需要实时地把消息送出去。
第二步:SSE 中转层,把 GitHub Webhook 的消息实时推给浏览器。
// SSE 端点
app.get('/stream/ci', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 把这个响应对象存入连接池
const clientId = addClient(res);
req.on('close', () => removeClient(clientId));
// 每 30 秒发送一个心跳,防止连接被中间件关闭
const heartbeat = setInterval(() => {
res.write(': heartbeatnn');
}, 30000);
req.on('close', () => clearInterval(heartbeat));
});
这里有两个细节要注意。第一个是心跳:很多反向代理(Nginx、Cloudflare)会在 60 秒没有流量时主动关闭连接,所以每 30 秒发一个空的心搏帧 : heartbeatnn 保持连接活跃。第二个是 Cache-Control: no-cache:如果这个头没设置,有些 CDN 会缓存 SSE 响应,导致消息延迟。
第三步:浏览器接收推送并弹出系统通知。
// 先请求通知权限
async function requestNotifPermission() {
if (!('Notification' in window)) return false;
if (Notification.permission === 'granted') return true;
if (Notification.permission !== 'denied') {
const perm = await Notification.requestPermission();
return perm === 'granted';
}
return false;
}
// 建立 SSE 连接
const canNotify = await requestNotifPermission();
const eventSource = new EventSource('/stream/ci');
// 注册 Service Worker,让页面关闭后通知依然能弹出来
if ('serviceWorker' in navigator) {
const reg = await navigator.serviceWorker.register('/sw.js');
await navigator.serviceWorker.ready;
}
eventSource.addEventListener('workflow_complete', async (e) => {
const data = JSON.parse(e.data);
const { status, repo, workflow, branch, url } = data;
const icon = status === 'success' ? '/icons/success.png' : '/icons/failure.png';
const title = status === 'success' ? '✅ CI 通过' : '❌ CI 失败';
const body = `${workflow} · ${branch} · ${repo}`;
if (navigator.serviceWorker.controller) {
// 通过 Service Worker 弹出系统通知,页面关了也能弹
navigator.serviceWorker.controller.postMessage({
title, body, icon, url, tag: `ci-${workflow}-${branch}`
});
} else if (canNotify) {
// 降级:直接用 Notification API
new Notification(title, { body, icon, tag: `ci-${workflow}-${branch}` });
}
});
重点在这里:通过 Service Worker 发通知,和直接在页面里 new Notification() 是两个不同的东西。前者运行在浏览器后台,页面关闭后依然能触发通知。后者只在页面打开时有效。Service Worker 注册一次,之后就常驻后台,不需要页面一直开着。
最后,Service Worker 里的处理逻辑:
// sw.js
self.addEventListener('message', (event) => {
const { title, body, icon, url, tag } = event.data;
self.registration.showNotification(title, {
body,
icon,
tag,
requireInteraction: true, // 点击才关闭,不自动消失
data: { url }
});
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});
requireInteraction: true 是这个方案里最重要的一个参数。默认情况下,浏览器会在几秒后自动关闭通知。对于 CI 失败这种需要立即处理的事件,应该让通知保持直到用户主动点击。
整个方案的延迟是多少?GitHub Webhook 到你的服务通常是 1-2 秒,SSE 推送是即时的,Notification 弹出来通常在事件触发后 3 秒以内。对比轮询方案常见的 30-60 秒轮询周期,实时性好了不止一个量级。
这个方案有一个隐性成本你要知道:浏览器对每个域名的 Service Worker 通知数量有限制,Chrome 是大约 3 条 / 分钟,超出后通知会被静默丢弃。如果你的 CI 频率非常高(比如每次 PR commit 都触发),需要对通知做合并,相同 workflow + branch 的失败只保留最新一条,用 tag 机制控制合并。
// 高频场景下的通知合并逻辑
if (status === 'failure') {
// 用固定 tag,同一个 workflow + branch 的失败合并
tag = `ci-failure-${workflow}-${branch}`;
} else {
// 成功事件不需要合并,每次都弹
tag = `ci-success-${Date.now()}`;
}
GitHub 的通知已经够用了,为什么还要这个方案?因为 GitHub 通知是邮件,你不一定在看邮件。这个方案让你在开发时不用主动去看任何地方,CI 的结果自己找上门来。对于需要快速响应的团队,这个差异挺大的。
评论区
登录后可评论。