使用说明
GitHub Actions 构建失败,如何通知手机
复制完整 GitHub Actions YAML:构建失败仍保留失败状态,通过私有 HTTPS 发送运行链接,并在手机核对对应消息。
更新于
先确定需要什么报告
#GitHub Mobile 已提供工作流完成通知、仅失败提醒和运行查看,能满足需求时可直接使用。需要自定义摘要,或把 CI、备份与 n8n 结果归集到同一个手机收件箱时,可以使用 Ntfyx。本例发送简短状态及 GitHub 运行链接;私有日志仍由 GitHub 控制访问,通知不会授予仓库权限。
先确认一条 HTTPS 通知能收到
#截至 2026-10-29 已确认公开版为 iOS 1.8.3、Android 1.8.2。在商店更新后,按手机上的公开 Topic 或私密连接入口继续;Android 后台推送需要 Google Play 服务。 在 Ntfyx 打开 HTTPS 推送,发送 hello 测试,确认对应消息进入收件箱,再复制完整私有发送 URL。本文向固定 URL POST JSON,不需要 CLI 配对。先确认自己的手机实际收到,再配置自动化。首次设置受阻时,核对安装版本并按排障页继续。
1. 将发送 URL 存为仓库 Secret
#在仓库进入 Settings → Secrets and variables → Actions → New repository secret,名称填写 NTFYX_WEBHOOK_URL,值为手机复制的完整 URL。不要写入 YAML 或任务日志。本例通过环境变量读取密钥,消息字段由 JSON 编码器构造,不把事件文本拼入 Shell 代码。
2. 添加完整工作流
#在测试仓库默认分支保存为 .github/workflows/phone-notification.yml,无需安装依赖。在 Actions 中手动运行并勾选 fail_demo:无副作用的演示任务以 1 退出,随后运行仅失败时通知的步骤。首次测试完成后,再将演示步骤换成项目自己的检出、环境准备与构建步骤。
name: CI phone notification demo
on:
workflow_dispatch:
inputs:
fail_demo:
description: Fail the harmless demo task
type: boolean
default: true
permissions: {}
jobs:
task:
runs-on: ubuntu-latest
steps:
- name: Demo task (replace with your build or test steps)
env:
FAIL_DEMO: ${{ inputs.fail_demo }}
shell: bash
run: |
if [[ "$FAIL_DEMO" == "true" ]]; then
echo "Intentional demo failure"
exit 1
fi
echo "Demo succeeded"
- name: Notify phone on failure
if: ${{ failure() }}
continue-on-error: true
timeout-minutes: 3
env:
NTFYX_WEBHOOK_URL: ${{ secrets.NTFYX_WEBHOOK_URL }}
TASK_RESULT: ${{ job.status }}
shell: bash
run: |
python3 - <<'PY'
import datetime, email.utils, hashlib, json, os, sys, time
import urllib.error, urllib.parse, urllib.request
def stop(message):
print("::warning::Ntfyx: " + message)
sys.exit(1)
url = os.environ.get("NTFYX_WEBHOOK_URL", "")
parsed = urllib.parse.urlsplit(url)
if parsed.scheme != "https" or parsed.hostname != "api.ntfyx.me":
stop("Set NTFYX_WEBHOOK_URL to the complete production HTTPS URL copied from the app.")
run_id = os.environ["GITHUB_RUN_ID"]
attempt = os.environ["GITHUB_RUN_ATTEMPT"]
repository = os.environ["GITHUB_REPOSITORY"]
run_url = f"{os.environ['GITHUB_SERVER_URL']}/{repository}/actions/runs/{run_id}"
payload = json.dumps({
"title": "CI " + os.environ["TASK_RESULT"],
"text": f"{repository}\nRun {run_id}, attempt {attempt}\n{run_url}",
"session_id": hashlib.sha256(f"github-{repository}-{run_id}".encode()).hexdigest(),
"session_name": "GitHub Actions",
}).encode("utf-8")
key = hashlib.sha256(f"github-{repository}-{run_id}-{attempt}-task-result".encode()).hexdigest()
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
opener = urllib.request.build_opener(NoRedirect)
for index in range(3):
delay = 5 * (index + 1)
request = urllib.request.Request(url, data=payload, method="POST", headers={
"Content-Type": "application/json", "Idempotency-Key": key,
})
try:
with opener.open(request, timeout=15) as response:
if not 200 <= response.status < 300:
stop("Unexpected response; inspect the sender execution.")
print("Ntfyx accepted the message; verify receipt separately on your phone.")
break
except urllib.error.HTTPError as error:
if error.code not in (408, 429, 500, 502, 503, 504):
stop(f"HTTP {error.code}; check configuration before retrying.")
retry_after = error.headers.get("Retry-After")
if retry_after:
try:
delay = max(delay, float(retry_after))
except ValueError:
try:
retry_at = email.utils.parsedate_to_datetime(retry_after)
delay = max(delay, (retry_at - datetime.datetime.now(datetime.timezone.utc)).total_seconds())
except (TypeError, ValueError):
stop("Invalid Retry-After; inspect the response before retrying.")
except (urllib.error.URLError, TimeoutError, OSError):
pass # Outcome unknown: preserve this payload and key.
if index == 2 or delay > 60:
stop("Send unconfirmed. Inspect the phone and sender before a retry; retain the original key and body.")
time.sleep(delay)
PY
3. 分别核对任务失败、消息收到与系统提醒
#手机预期显示标题“CI failure”,正文包含仓库、运行 ID、尝试次数和运行 URL。打开链接,确认对应刚才故意失败的运行。发送成功后,工作流仍应保持失败。发送日志的“accepted”只证明服务受理;必须在收件箱找到匹配消息才算完成接入,再单独检查 App 在后台时是否有横幅或声音。
- 运行一次 fail_demo=true:预期任务失败,并收到一条匹配的收件箱消息。
- 再次运行 fail_demo=false:预期任务通过,跳过仅失败时发送的步骤。
- 对照运行 ID 与尝试次数,不只看标题;完成后移除演示任务。
区分通知失败与构建失败
#只有通知步骤设置 continue-on-error。构建失败仍是失败;发送失败会留下警告,不覆盖原始结果。构建步骤应保持严格失败。若也要通知成功,将通知步骤条件改为下方表达式,TASK_RESULT 会提供发送前的任务状态。它覆盖已执行步骤,不保证取消、执行器失联或任务超时后一定通知。
if: ${{ !cancelled() }}重试继续同一条逻辑消息
#发送器固定正文,并按仓库、运行 ID、尝试次数和任务生成请求标识。临时错误最多尝试三次,复用原标识,并在等待预算内遵守 Retry-After;要求等待过久时停止发送,不提前重试。鉴权或输入错误立即停止。重新运行 GitHub 任务会增加尝试次数、生成新报告,不是对未知发送结果的原样重试。多个任务各自发送时,应设置不同且稳定的任务后缀。复用标识时不能修改正文。
验证范围与下一步
#下载示例在本地检查 YAML/Python 语法、成功与失败分支、JSON 构造、重试输入稳定性,并以模拟 HTTP 响应检查发送失败处理。这不是 GitHub 托管执行或实体手机收件记录;使用前请在自己的仓库完成上述步骤。下一篇将同一个 HTTPS 接收入口用于 n8n 错误。