GitHub Actions 事务邮件 + Rovela(2026):Secrets 与基于 run_id 的幂等键
从 GitHub Actions 经 Rovela POST /emails 发送 CI 失败、Release 发布与部署成功通知。把 ROVELA_API_KEY 放进 Actions secrets;用 curl 或 Node fetch,Idempotency-Key = gha-{kind}-{github.run_id}。与 Vercel cron 摘要、AI agent Cursor 发信区分。
GitHub Actions 负责跑流水线 — Rovela 负责投递通知邮件
GitHub Actions 已经在你关心的每次 push、PR、release 与部署任务上运行。当 CI 失败、Release 发布或生产部署完成时,对值班/创始人更有用的往往是一封短事务邮件——而不是又一个被静音的 Slack bot。把 ROVELA_API_KEY(以及 MAIL_FROM / NOTIFY_TO)存成 GitHub Actions secrets,再在 workflow 步骤里用 curl 或 fetch 调用 POST https://api.rovela.dev/emails,并用 github.run_id 构造稳定的 Idempotency-Key。
当步骤调用发送接口时,Rovela 负责入队、DKIM 签名与投递(发送邮件 API、快速开始、鉴权)。GitHub 拥有触发器、runner 与密钥注入;你的 workflow YAML 决定何时发信以及正文有哪些字段;Rovela 只负责投递。
本指南会接好这类 workflow:
- 把
ROVELA_API_KEY、MAIL_FROM、NOTIFY_TO放在仓库 / Environment secrets——不要写进 YAML,不要echo - 在 release published、CI / 部署失败(
if: failure())或部署成功(if: success())时发信 - 用 Ubuntu runner 自带的
curl,或一小段 Nodefetch调用 Rovela - 设置
Idempotency-Key: gha-{kind}-{github.run_id},避免 24 小时内重试/重投导致重复邮件(发送邮件 API)
Rovela 不会创建 GitHub workflow、不会代管 Actions secrets,也不会读取你的仓库。 GitHub 拥有这次 run;你的步骤负责把 context 映射成 HTML/text;Rovela 负责 POST /emails。
与这些文章区分:
- Vercel Cron 摘要邮件 — 按计划对生产路径发
GET并聚合应用状态;不是 GitHub Actions runner 步骤 - AI Agent 用 HTTP 发邮件 — 临时 agent / Cursor 的
curl脚手架,不是 CI/CD 在release/failure()上的通知 - Cloudflare Workers 事务邮件 — 边缘 Worker + Wrangler secrets,不是 Actions
GitHub Actions 做什么,Rovela 发什么
| 关注点 | 负责方 |
|---|---|
Workflow 触发(push、release、workflow_run 等) | GitHub Actions |
Runner + secrets.* 注入 | GitHub |
| 选择失败 / 成功 / release | 你的 workflow if: |
用 github.* context 拼主题与 HTML/text | 你的步骤 |
POST /emails、DKIM、队列、退信 | Rovela |
Idempotency-Key ≈ gha-{kind}-{run_id} | 你的步骤 |
Rovela 返回 200 表示已接受并入队,并不代表「已经进收件箱」(快速开始)。
前置条件
- 在 Rovela 控制台完成发信域名验证。未验证的
from域名会以400拒绝(快速开始)。建议使用ci.example.com或mail.example.com。 - 形如
re_…的 API Key。CI 通知优先使用sending_access(鉴权)。 - 在 Settings → Secrets and variables → Actions 配置仓库(或 Environment)密钥:
ROVELA_API_KEY、MAIL_FROM、NOTIFY_TO。 - 在
.github/workflows/下添加 workflow。GitHub 托管的ubuntu-latestrunner 自带curl与jq。
按 GitHub 的方式存放密钥
在 GitHub UI 创建 secret(或用 gh secret set)。只通过 env: 里的 ${{ secrets.NAME }} 引用——不要把密钥插进日志或 commit message。
# 在已有本地值的机器上执行(切勿提交这些值)
gh secret set ROVELA_API_KEY --body "$ROVELA_API_KEY"
gh secret set MAIL_FROM --body 'Acme CI <ci@mail.example.com>'
gh secret set NOTIFY_TO --body 'oncall@example.com'
若通知只应来自受保护的部署 job,优先使用 Environment secret(如 production)。轮换 Rovela 密钥:先新建 → 更新 secret → 再吊销旧密钥(鉴权)。
Idempotency-Key = github.run_id(以及何时加 attempt)
根据 github context:
github.run_id— 仓库内每次 workflow run 唯一;重新运行同一 run 时不变github.run_attempt— 从1开始,每次 re-run 递增github.server_url/github.repository/github.run_id— 拼出邮件里可点的 Actions run URL
Rovela 的 Idempotency-Key(最长 256 字符)会让 24 小时内相同请求的重试返回原响应,而不是再发一封(发送邮件 API)。
推荐键:
| 键 | 适用场景 |
|---|---|
gha-ci-failure-{run_id} | 每个 workflow run 一封失败告警 |
gha-release-{run_id} | 每次发布 release 的 run 一封公告 |
gha-deploy-ok-{run_id} | 每次成功部署 run 一封「已上线」通知 |
gha-ci-failure-{run_id}-a{run_attempt} | 有意在 re-run 时再通知一次 |
默认只用 run_id,这样 GitHub 重试通知步骤,或有人在 24 小时内 re-run,都不会轰炸值班。只有在 re-run 必须再发一封新邮件时,才加上 run_attempt。
模式 A — CI / 部署失败通知(if: failure())
把通知步骤放在最后。if: failure() 会在前面步骤失败后执行;你不需要给失败步骤加 continue-on-error: true(那样会让 job 变绿)。
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install
run: npm ci
- name: Test
run: npm test
- name: Email on-call via Rovela
if: failure()
env:
ROVELA_API_KEY: ${{ secrets.ROVELA_API_KEY }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
NOTIFY_TO: ${{ secrets.NOTIFY_TO }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REPO: ${{ github.repository }}
WORKFLOW: ${{ github.workflow }}
REF: ${{ github.ref_name }}
ACTOR: ${{ github.actor }}
SHA: ${{ github.sha }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
SHORT_SHA="${SHA:0:7}"
SUBJECT="CI failed: ${REPO} @ ${REF} (${SHORT_SHA})"
TEXT=$(printf '%s\n' \
"Workflow: ${WORKFLOW}" \
"Repo: ${REPO}" \
"Ref: ${REF}" \
"Actor: ${ACTOR}" \
"SHA: ${SHORT_SHA}" \
"Run: ${RUN_URL}")
HTML="<p><b>CI failed</b></p><ul>
<li>Workflow: ${WORKFLOW}</li>
<li>Repo: ${REPO}</li>
<li>Ref: ${REF}</li>
<li>Actor: ${ACTOR}</li>
<li>SHA: <code>${SHORT_SHA}</code></li>
<li><a href=\"${RUN_URL}\">Open Actions run</a></li>
</ul>"
# 用 jq 安全拼 JSON — 分支名/actor 可能含引号
payload=$(jq -n \
--arg from "$MAIL_FROM" \
--arg to "$NOTIFY_TO" \
--arg subject "$SUBJECT" \
--arg html "$HTML" \
--arg text "$TEXT" \
--arg run_id "$RUN_ID" \
'{from:$from, to:$to, subject:$subject, html:$html, text:$text,
tags:{kind:"gha-ci-failure", run_id:$run_id}}')
curl -fsS -X POST "https://api.rovela.dev/emails" \
-H "Authorization: Bearer ${ROVELA_API_KEY}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: gha-ci-failure-${RUN_ID}" \
-d "$payload"
注意:
- 通过
env:传入密钥,让它们成为 runner 上的环境变量;避免把${{ secrets.* }}直接拼进可能进入 debug 日志的 shell 字符串。 - 用
jq -n构建 JSON(GitHub 托管 Ubuntu 预装),避免 ref 中的特殊字符弄坏正文。 - 务必带上 run URL——值班凌晨点的就是它。
模式 B — Release 发布公告
# .github/workflows/release-email.yml
name: Release email
on:
release:
types: [published]
jobs:
announce:
runs-on: ubuntu-latest
steps:
- name: Email release note via Rovela
env:
ROVELA_API_KEY: ${{ secrets.ROVELA_API_KEY }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
NOTIFY_TO: ${{ secrets.NOTIFY_TO }}
TAG: ${{ github.event.release.tag_name }}
NAME: ${{ github.event.release.name }}
HTML_URL: ${{ github.event.release.html_url }}
BODY: ${{ github.event.release.body }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
SUBJECT="Released ${TAG} — ${REPO}"
# 正文保持简短;完整说明放在 GitHub Release 页面
TEXT=$(printf '%s\n' "Release: ${NAME:-$TAG}" "Tag: ${TAG}" "URL: ${HTML_URL}")
HTML="<p>Release <b>${TAG}</b> is published.</p>
<p><a href=\"${HTML_URL}\">Open release</a></p>"
payload=$(jq -n \
--arg from "$MAIL_FROM" \
--arg to "$NOTIFY_TO" \
--arg subject "$SUBJECT" \
--arg html "$HTML" \
--arg text "$TEXT" \
--arg tag "$TAG" \
'{from:$from, to:$to, subject:$subject, html:$html, text:$text,
tags:{kind:"gha-release", tag:$tag}}')
curl -fsS -X POST "https://api.rovela.dev/emails" \
-H "Authorization: Bearer ${ROVELA_API_KEY}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: gha-release-${RUN_ID}" \
-d "$payload"
release 且 types: [published] 会在 GitHub Release 上线时触发。changelog 可能很长——邮件里链到 html_url,保持事务性即可。
模式 C — 部署成功通知
- name: Email deploy success via Rovela
if: success()
env:
ROVELA_API_KEY: ${{ secrets.ROVELA_API_KEY }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
NOTIFY_TO: ${{ secrets.NOTIFY_TO }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REPO: ${{ github.repository }}
REF: ${{ github.ref_name }}
SHA: ${{ github.sha }}
RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
SHORT_SHA="${SHA:0:7}"
SUBJECT="Deployed ${REPO} @ ${REF} (${SHORT_SHA})"
TEXT="Deploy succeeded. ${RUN_URL}"
HTML="<p>Deploy succeeded for <b>${REPO}</b> @ <code>${REF}</code> (<code>${SHORT_SHA}</code>).</p>
<p><a href=\"${RUN_URL}\">Open Actions run</a></p>"
payload=$(jq -n \
--arg from "$MAIL_FROM" \
--arg to "$NOTIFY_TO" \
--arg subject "$SUBJECT" \
--arg html "$HTML" \
--arg text "$TEXT" \
'{from:$from, to:$to, subject:$subject, html:$html, text:$text,
tags:{kind:"gha-deploy-ok"}}')
curl -fsS -X POST "https://api.rovela.dev/emails" \
-H "Authorization: Bearer ${ROVELA_API_KEY}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: gha-deploy-ok-${RUN_ID}" \
-d "$payload"
成功邮件请留给生产部署 workflow——不要给每个 PR CI 都发,否则收件箱会训练人忽略通知。若 job 也会在其他分支跑,用 if: success() && github.ref == 'refs/heads/main' 收紧。
可选:用 Node fetch 代替 curl
若 job 里已有 Node:
- name: Email via Rovela (Node)
if: failure()
env:
ROVELA_API_KEY: ${{ secrets.ROVELA_API_KEY }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
NOTIFY_TO: ${{ secrets.NOTIFY_TO }}
RUN_ID: ${{ github.run_id }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REPO: ${{ github.repository }}
run: |
node <<'NODE'
const res = await fetch('https://api.rovela.dev/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ROVELA_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `gha-ci-failure-${process.env.RUN_ID}`.slice(0, 256),
},
body: JSON.stringify({
from: process.env.MAIL_FROM,
to: process.env.NOTIFY_TO,
subject: `CI failed: ${process.env.REPO}`,
html: `<p>CI failed.</p><p><a href="${process.env.RUN_URL}">Open run</a></p>`,
text: `CI failed. ${process.env.RUN_URL}`,
tags: { kind: 'gha-ci-failure', run_id: process.env.RUN_ID },
}),
});
const body = await res.json();
if (!res.ok) {
console.error(body);
process.exit(1);
}
console.log('queued', body.id);
NODE
同一套 API 与幂等规则。Actions 里优先使用 sending_access 密钥(鉴权)。
多收件人与 tags
to 可以是字符串或数组(to / cc / bcc 合计最多 50;按收件人计费)(发送邮件 API)。小值班列表可以:
# NOTIFY_TO secret 可以是单个地址;或在 jq 里传 JSON 数组:
--argjson to '["oncall@example.com","founders@example.com"]'
可选的消息 tags 便于之后在 Rovela 控制台 / webhook 里过滤(kind、run_id、repo)。
值得处理的失败模式
| 现象 | 常见原因 |
|---|---|
| 步骤绿了但没信 | secret 名写错,或 if: 从未为真 |
Rovela 401 | ROVELA_API_KEY 错误或已吊销 |
Rovela 400 点名域名 | MAIL_FROM 不在已验证域名上 |
Rovela 429 | 触达限流 — 退避重试,同一 Idempotency-Key |
| 重复告警 | 幂等键每次随机生成,或不小心加了 run_attempt |
日志里密钥显示 *** 但仍 401 | 该 Environment / fork PR 拿不到 secret |
| Fork PR 发不出 | 默认不会把仓库 secrets 传给来自 fork 的 workflow — 符合预期 |
Fork 的 pull request 不应拿到生产发信密钥。把通知步骤放在对 main 的 push、release,或 Environment 保护的部署 job 上。
不要 echo API key。GitHub 会在日志里打码已知密钥,但那只是兜底,不是打印密钥的许可。
上线检查清单
- 在 Rovela 验证发信域名(按控制台完成 SPF / DKIM / DMARC / return-path)。
- 创建
sending_access密钥;把ROVELA_API_KEY、MAIL_FROM、NOTIFY_TO设为 Actions secrets。 - 添加失败 / release / 部署步骤,使用
Idempotency-Key: gha-…-${{ github.run_id }}。 - 故意触发一次失败,或 draft→publish 一次 release;确认一个 message id 与一封收件箱投递。
- 24 小时内 re-run 同一 workflow——在
run_id作用域下应返回原 id(不会第二封)。 - 若运维需要退信可见性,订阅 Rovela 投递 webhook(读消息需要
full_access或 session;Actions 应继续用sending_access)。
本文未覆盖
- 以 Slack / Discord webhook 为主通道 — 同样可用
if: failure(),只是 URL 不同;本文是经 Rovela 发邮件 - 按计划从生产数据库汇总摘要 — 见 Vercel Cron 摘要邮件
- 临时 agent 工具链 — 见 AI Agent 用 HTTP 发邮件
- 从 Actions 发营销群发 — 活动邮件留给营销工具;本路径只做事务性运维 / release 通知
小结
GitHub Actions 已经知道 CI 何时失败、Release 何时发布、部署何时成功。把 Rovela 凭据放进 Actions secrets,用 curl 或 fetch 调用 POST https://api.rovela.dev/emails,并把 Idempotency-Key 钉在 github.run_id 上(仅当 re-run 必须再发时才加 run_attempt)。GitHub 拥有 workflow;Rovela 拥有投递——你的应用里不必再挂一套邮件 worker。