- 后端
- 即时通讯
【免费下载链接】mercure
🪽 An open, easy, fast, reliable and battery-efficient solution for real-time communications
本文面向需要在 CI 流水线中验证 Mercure 实时通信逻辑的开发者,讲解如何把 Mercure Hub 以 GitHub Actions 的service container(服务容器)形式拉起,配合内置健康检查完成集成测试,并演示如何在 workflow 内部直接向 hub 发布更新、通知依赖 Mercure 驱动的状态页面。读完本文,你将掌握一套开箱即用、可复制的 GitHub Actions 集成测试配置,并能理解其背后健康检查端点与 JWT 鉴权的实现原理。
场景:为什么用 service container 跑 Mercure Hub
Mercure 是一个基于 Server-Sent Events(SSE)的实时通信协议,Hub 负责接收发布请求并把更新推送给订阅者。在持续集成中,如果被测代码依赖真实的 hub(例如测试"发布→订阅→收到推送"的完整链路),最干净的做法不是 mock 协议,而是在同一个 job 里拉起一个真实的 hub 进程。
GitHub Actions 的 service container 机制正好满足这个需求:它允许你在services段声明一个容器,job 的所有 steps 都能通过localhost访问它,容器由 GitHub 托管运行器负责生命周期管理。Mercure Hub 官方镜像就是一个现成的服务容器,无需额外配置即可在测试前启动、在 job 结束时销毁。
完整工作流示例
将下面这份 YAML 保存为.github/workflows/ci.yml(或你自己的 workflow 文件),即可获得一个带 Mercure Hub 服务容器的集成测试 job:
name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest permissions: contents: read services: mercure: image: ghcr.io/dunglas/mercure env: SERVER_NAME: ":1337" MERCURE_PUBLISHER_JWT_KEY: "ci-only-publisher-key-never-reuse-it" MERCURE_SUBSCRIBER_JWT_KEY: "ci-only-subscriber-key-never-reuse-it" MERCURE_EXTRA_DIRECTIVES: | anonymous cors_origins * options: >- --health-cmd "wget -q -O /dev/null http://localhost:2019/mercure/health/ready" --health-interval 5s --health-timeout 3s --health-retries 10 ports: - 1337:1337 steps: - uses: actions/checkout@v7 - name: Run tests env: MERCURE_URL: http://localhost:1337/.well-known/mercure run: ./run-tests.sh工作流触发后,job 的 steps 就能通过http://localhost:1337/.well-known/mercure访问到 hub。这套纯 HTTP 的配置只适用于隔离的 CI 环境(无需 TLS、无需真实域名),这正是它简洁可靠的原因。
逐项拆解服务容器配置
镜像选择:优先 GHCR,而非 Docker Hub
示例使用ghcr.io/dunglas/mercure而非 Docker Hub 上的dunglas/mercure。原因是:GitHub 托管的 runner 拉取 GHCR 镜像无需认证,从而规避 Docker Hub 匿名拉取的速率限制(rate limit)和偶发的认证超时问题。两个仓库里的镜像及 tag 完全相同,如果你偏好 Docker Hub,把image换成dunglas/mercure同样可用——在 Docker 部署文档中可以看到镜像变体的完整说明,其中明确列出ghcr.io/dunglas/mercure与dunglas/mercure是同一镜像同一 tag。
环境变量:把 Caddyfile 的默认值搬进 CI
服务容器内运行的 hub 读取一组环境变量,这些变量在仓库根目录的 Caddyfile 中被声明为 Caddy 占位符:
| 变量 | 本示例取值 | 作用 |
|---|---|---|
SERVER_NAME | :1337 | Caddy 站点地址。:1337表示不绑定主机名、监听 1337 端口(默认是localhost,即 443/80 端口) |
MERCURE_PUBLISHER_JWT_KEY | 任意一次性密钥 | 发布者 token 的签名验证密钥,对应 Caddyfile 中publisher { jwt ... } |
MERCURE_SUBSCRIBER_JWT_KEY | 任意一次性密钥 | 订阅者 token 的签名验证密钥,对应subscriber { jwt ... } |
MERCURE_EXTRA_DIRECTIVES | anonymous+cors_origins * | 追加到mercure块内的额外指令,一行一条 |
anonymous指令允许不带 token的订阅者接收公开更新,这让测试可以省去先签发订阅者 token 的步骤;cors_origins *允许任意跨域来源(不带 cookie 的跨域请求),保证测试页与 hub 不同源时也能建立 SSE 连接。两者的完整语义可参见 Mercure 配置文档 中的指令表与 CORS 一节。
注意:CI 里的 JWT 密钥是"一次性"的——它只存在于这个 job 的生命周期内,绝不与生产密钥混用。示例中的密钥值ci-only-...-never-reuse-it只是占位,你可以直接用openssl rand -base64 32生成真正的随机密钥。
healthcheck:GitHub 等待服务就绪
options段向容器传递 Docker 的 healthcheck 参数:
options: >- --health-cmd "wget -q -O /dev/null http://localhost:2019/mercure/health/ready" --health-interval 5s --health-timeout 3s --health-retries 10健康检查命令向transport 就绪端点localhost:2019/mercure/health/ready发送 GET 请求。这个端口是 Caddy 的 admin API 默认端口,它绑定在容器回环地址上,因此健康检查在容器内部执行,并不会把 admin API 暴露给 runner。GitHub Actions 会等待服务容器进入 healthy 状态后才开始执行 job steps,从而保证测试开始时 hub 一定可以对外服务。
这条健康检查命令与仓库 Dockerfile 中镜像自带HEALTHCHECK的探测目标完全一致(同样使用wget访问http://127.0.0.1:2019/mercure/health/ready),只是 CI 里把间隔缩短为 5 秒、把重试次数提高到 10,以便在 runner 环境中更快就绪。
不要把订阅端点(
/.well-known/mercure的 SSE 订阅)当作就绪探针:订阅请求是长连接,用一个错误响应去判断"未就绪"既不准确,还会污染测试环境。
端口映射
ports: - 1337:1337把容器内 Caddy 监听的 1337 端口映射到 runner 的 localhost 上。hub 的公开 URL 由SERVER_NAME与站点路径共同决定,即http://localhost:1337/.well-known/mercure。
连接细节:issuer 与 audience 的坑
在 CI 中连接 hub 时有一个关键点容易踩坑:token 的签发与校验参数。
由于SERVER_NAME被设为:1337(且未设置MERCURE_TRUSTED_ISSUERS),hub 的默认可信 issuer 是https://localhost(见 Caddyfile 中{$MERCURE_TRUSTED_ISSUERS:https://localhost}的默认值)。同时,hub 会从每个请求推导自己的资源标识符(即 OAuth 2.0 的 audience):客户端访问的是http://localhost:1337/.well-known/mercure,那么测试 token 的aud声明就必须携带http://localhost:1337/.well-known/mercure,而不是 HTTPS 形式,也不是别的端口。
这意味着如果你在测试中签发 token(而不是依赖anonymous指令跳过订阅鉴权),必须:
caddy mercure-token \ --iss https://localhost \ --aud http://localhost:1337/.well-known/mercure \ --key "$MERCURE_PUBLISHER_JWT_KEY" \ --publish 'https://example.com/books/1'caddy mercure-token是 hub 内置的令牌铸造命令,定义在 caddy/cmdtoken.go,它会生成符合 RFC 9068 的at+jwt访问令牌,自动填充sub、client_id、iat、jti等声明。由于发布与订阅各有一把独立密钥,签发发布者 token 用MERCURE_PUBLISHER_JWT_KEY,订阅者 token 用MERCURE_SUBSCRIBER_JWT_KEY。
hub 校验 token 时会强制检查iss必须匹配可信 issuer、aud必须包含资源标识符、必须有exp,并以配置的密钥验签(算法来自配置而非 token 本身)。完整校验规则见 Mercure 授权文档。
健康检查背后的实现:transport 感知的就绪/存活端点
CI 健康检查使用的端点并非简单的 HTTP 探活,而是transport 感知的:GET /mercure/health/ready在所有 transport 都能提供流量时返回200,否则返回503。它由 Caddy admin API 上的一个模块实现,源码位于 caddy/health.go:
- 模块注册为
admin.api.mercure_health,路由模式为/mercure/health/(第 41-48 行); handleHealth只接受GET(其余方法返回 405),并对每个注册的 hub 遍历其 transport,调用mercure.TransportHealthChecker接口的Ready()/Live()方法(第 130-140 行);- transport 不健康时返回
503,且响应体只含通用的"status":"error",不会泄露内部连接细节——详细错误只记录在服务端日志(第 82-95 行)。
同族端点还有:
| 端点 | 语义 |
|---|---|
GET /mercure/health/ready | 所有 transport 可服务则200,否则503。用作就绪判断 |
GET /mercure/health/live | 所有 transport 基本可用则200。用作存活判断 |
GET /mercure/health/{name}/ready | 运行多个 hub 时按名称定向就绪检查 |
GET /mercure/health/{name}/live | 按名称定向存活检查 |
仓库中的 caddy/health_test.go 用真实 Caddy 测试实例验证了这些行为:就绪/存活端点返回"status":"ok"、未知路径返回 404、非 GET 方法返回 405、transport 故障时返回 503 且响应体不含内部错误字符串。
之所以区分 ready 和 live,是因为就绪可能因 transport 临时故障而失败,而存活判断用于区分"可自愈的临时故障"与"需要重启的硬故障"。对于 CI 场景,用ready端点即可:它保证测试开始时 hub 已能接收流量。值得注意的是,捆绑的 BoltDB 与 local transport 没有外部依赖检查,探针恒返回 200(这不验证磁盘容量或端到端投递),但对于本地集成测试来说,能确认 Caddy 与 hub 模块完成加载并正常运行,已经足够。更深入的探针讨论见 健康监控文档。
另外,如果你的测试 job 需要在容器启动后立即发布、而 BoltDB 首次打开需要片刻,官方建议把start_period视为"尚不算不健康"的宽限期(镜像默认 60 秒);本例中通过 10 次重试 × 5 秒间隔兜底。
从 workflow 发布更新:通知与状态同步
除了让测试程序自己调用 hub,你也可以直接在 workflow 里发布更新。典型场景包括:部署完成时通知一个由 Mercure 驱动的状态页面,或者通过自己的 hub 推送一条类 Slack 的通知:
- name: Notify run: | curl --fail-with-body -X POST "$MERCURE_URL" \ -H "Authorization: Bearer $MERCURE_JWT" \ --data-urlencode "topic=https://example.com/deploys/${{ github.repository }}" \ --data-urlencode "data={\"status\":\"deployed\",\"sha\":\"${{ github.sha }}\"}" env: MERCURE_URL: https://hub.example.com/.well-known/mercure MERCURE_JWT: ${{ secrets.MERCURE_PUBLISHER_JWT }}这个POST请求的协议细节与 发布文档 一致:向/.well-known/mercure发送application/x-www-form-urlencoded表单,字段包括必填的topic和可选的data(hub 把data当作不透明字节,JSON、HTML、纯文本均可),Authorization 头携带Bearertoken。--fail-with-body保证发布失败时 step 会以非零码退出,避免"发布失败但 job 绿了"的假阳性。
这里的两个关键实践:
- 发布者 token 必须作用域化:不要用
*全量发布权限,而是签发一个只覆盖通知 topic 的 token(例如只允许publish到https://example.com/deploys/*)。授权模型基于authorization_details声明,见 授权文档。 - 以 repository secret 存储并定期轮换:把
MERCURE_PUBLISHER_JWT存入仓库的 Secrets(${{ secrets.MERCURE_PUBLISHER_JWT }}),到期前轮换。如需自动化轮换,可通过你的授权服务签发短生命周期 token——这正好契合 hub 强制exp声明的设计:短 token 限制了泄露后的影响面。
已有的社区 GitHub Action
社区已有封装好的可复用 Action,把上面的发布调用包装成一个 step:
Ilshidur/action-mercure:将向 Mercure hub 发布更新封装为可复用的 Action,适合不想手写curl的场景。
如果你只需要"发布一条更新",用它能让 workflow 更简洁;需要细粒度控制(多 topic、自定义type、private标记等)时,直接使用上面的curl示例更灵活。
实战 Tips 汇总
原文档总结了三条在 GitHub Actions 中使用 Mercure 的实操经验,这里连同补充说明一并列出:
- 在服务容器内部执行健康检查:示例通过容器的回环地址查询 admin API,既验证了 hub 就绪,又没有把
localhost:2019暴露给 runner。注意不要改绑0.0.0.0:2019——那会把整个 admin API(含/stop、/load)暴露到网络。 - 使用固定端口:
1337是约定俗成的选择,请确保它不会与 job 中其他服务容器(如数据库、缓存)的端口冲突。多个服务并存时,给每个服务分配互不重叠的端口并显式映射。 - 不要在 fork 的 PR 之间共享 JWT:仓库 Secrets 不会暴露给来自 fork 的 PR。如果你的 workflow 会在外部 PR 上执行并向外发布更新,务必把这一点纳入安全设计——要么只在非 fork 分支触发发布步骤,要么使用只读权限的 token。
此外,还可以参考本仓库自身的 .github/workflows/ci.yml:它展示了 GitHub Actions 与 Mercure 结合的另一种形态——在 runner 上直接用go run main.go start --config Caddyfile启动本地 hub,配合playground指令跑 Playwright 一致性测试。如果你在 CI 中需要 hub 的完整开发模式(含调试 UI 与万能 token),可以用MERCURE_EXTRA_DIRECTIVES=playground替代上面的anonymous/cors_origins组合(详见 Docker 部署文档 的开发模式一节;该模式仅限开发环境)。
延伸阅读
- Mercure 配置文档:全部指令、环境变量与健康检查端点参考
- Docker 部署文档:镜像变体、Compose 与内置 HEALTHCHECK
- 健康监控文档:ready/live 端点语义、探针写法与告警建议
- Mercure 授权文档:token 校验规则与
caddy mercure-token用法 - 发布文档:POST 发布协议与表单字段
- 后端
- 即时通讯
【免费下载链接】mercure
🪽 An open, easy, fast, reliable and battery-efficient solution for real-time communications
相关推荐
在 GitHub Actions 中运行 Artillery 负载测试:完整 CI/CD 集成指南
在 GitHub Actions 中运行 Artillery 负载测试:完整 CI/CD 集成指南 导读 本文基于 Artillery 仓库中的 example
性能测试接口测试CLINLWeb 实战:如何用真实服务器运行集成测试(RUN_TESTS_WITH_SERVER 完整指南)
NLWeb 实战:如何用真实服务器运行集成测试(RUN_TESTS_WITH_SERVER 完整指南) 导读 本文是 NLWeb 开源项目(Main refer
AI 应用MCP 服务AI Agent后端前端在 GitHub Actions 中运行 RedwoodJS 测试:CI/CD 与 Postgres 测试数据库完整指南
在 GitHub Actions 中运行 RedwoodJS 测试:CI/CD 与 Postgres 测试数据库完整指南 RedwoodJS 提供了一套开箱即用
后端前端Web框架开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考