GitHub Actions定时触发失效排查实录:timezone的坑与纯UTC方案
一、引言
GitHub Actions 的 schedule 事件是很多项目实现定时任务的首选方案——无需额外服务器,配置简单,与代码仓库天然集成。但某天早上,我发现一个依赖定时触发的日报工作流没有按预期运行,Actions 页面中没有任何对应的自动运行记录。
本文还原了从"怀疑代码有 bug"到"定位 GitHub 平台行为"的完整排查过程,重点揭示 timezone 字段的一个隐蔽坑点。
二、问题现象
一个用于生成和推送日报的 workflow 配置了如下定时触发:
on:
schedule:
- cron: '55 9 * * *'
timezone: 'Asia/Shanghai' # 期望北京时间每天 09:55 运行
workflow_dispatch: # 保留手动触发用于调试
但到了预定时间,既没有收到推送,GitHub Actions 页面也没有出现任何 schedule 类型的运行记录。手动触发(workflow_dispatch)则完全正常。
三、排查过程
3.1 排除代码层面问题
首先检查了 workflow 的执行逻辑:
- 确认 YAML 配置语法正确,
schedule节点存在 - 确认工作流依赖的配置文件在 Actions 环境中被正确加载
- 手动触发能成功执行到推送环节
结论:问题不在项目代码,而在 GitHub 没有投递 schedule 事件。
3.2 新增最小化探针(Schedule Probe)
为了排除"原 workflow 太复杂导致静默失败"的可能性,新增了一个极简探针:
name: Schedule Probe
on:
schedule:
- cron: '*/10 * * * *' # 每10分钟,多点触发
workflow_dispatch:
jobs:
probe:
runs-on: ubuntu-latest
steps:
- run: |
echo "event_name=${{ github.event_name }}"
echo "schedule=${{ github.event.schedule }}"
echo "time=$(date -u)"
这个探针不依赖任何 secrets,不运行业务逻辑,只打印触发事件类型。结果:手动触发成功,但多个应触发的时间点过去后,依然没有任何自动 schedule 运行。
3.3 通过 GitHub API 交叉验证
使用 GitHub REST API 查询 workflow 状态:
# 查询 workflow 列表
GET /repos/{owner}/{repo}/actions/workflows
# 查询运行记录
GET /repos/{owner}/{repo}/actions/workflows/{id}/runs
查询结果显示:
- 所有 workflow 状态均为
active - 所有历史 run 记录的
event字段都是workflow_dispatch - 没有任何
schedule类型的 run 记录
3.4 对照上游仓库
由于该项目是一个 fork,怀疑 fork 仓库的 schedule 行为可能与普通仓库不同。查询上游仓库同名 workflow 的历史记录,发现其 run 记录也全部是 workflow_dispatch,没有 schedule 事件——这不能直接证明 fork 有问题,但也不能排除。
3.5 新建非 fork 仓库做对照实验
为了排除 fork 的因素,创建了一个全新的非 fork 测试仓库,只包含一个每 5 分钟触发一次的最简 workflow。推送后等待超过预设周期,通过 API 查询,结果依然是 只有手动触发记录,没有自动 schedule 运行。
到这一步,可以基本排除"fork 导致"的假设。
3.6 转折:schedule 终于触发,但时间不对
在多次排查得出"完全不触发"的结论之后,GitHub Actions 页面突然出现了一条:
Daily Horizon Summary #5: Scheduled
通过 API 确认:event = schedule,触发时间为 2026-06-17T06:55:19Z,换算北京时间是 14:55。
而 workflow 配置的目标时间是北京时间 09:55——相差整整 5 个小时,正好是 UTC+8 的时差。
四、根因定位:timezone 字段的行为不可靠
这是本次排查最大的发现:
cron: '55 9 * * *'
timezone: 'Asia/Shanghai'
实际行为:GitHub 在大多数时间点没有触发,而最终触发的那一次,表现更像是按 UTC 09:55 解释了 cron(即北京时间 17:55,但实际记录也并非完全吻合)。
核心结论:
timezone字段在 GitHub Actions 中的行为并不可靠。不能假设它一定会按指定时区正确解释 cron 表达式。
这不是 cron 语法错误,而是 GitHub 平台的 schedule 事件投递 + timezone 解释组合存在不稳定性。
五、解决方案:纯 UTC Cron
5.1 手动换算 + 纯 UTC
放弃 timezone 字段,将目标时间手动换算为 UTC,直接写入 cron:
# 错误做法(不可靠)
on:
schedule:
- cron: '55 9 * * *'
timezone: 'Asia/Shanghai'
# 正确做法(可靠)
on:
schedule:
# 北京时间每天 07:00 = UTC 23:00 (前一天)
- cron: '0 23 * * *'
workflow_dispatch:
5.2 换算参考
| 北京时间目标 | UTC 等价 | cron 表达式 |
|---|---|---|
| 08:00 | 00:00 | 0 0 * * * |
| 09:00 | 01:00 | 0 1 * * * |
| 12:00 | 04:00 | 0 4 * * * |
5.3 兜底方案
如果纯 UTC cron 仍然不可靠,可以考虑:
- 外部定时器 + workflow_dispatch API:用外部 cron 服务调用 GitHub API 手动触发 workflow
- 向 GitHub Support 提交问题:尤其是当非 fork 仓库的简单 workflow 长时间不触发时
六、排查经验总结
排查方法论
怀疑代码 → 排除代码(手动触发正常)
↓
怀疑配置 → 排除配置(YAML 语法正确、secrets 正确)
↓
怀疑复杂度 → 最小化探针(Schedule Probe 也不触发)
↓
怀疑 fork → 对照实验(非 fork 仓库也不触发)
↓
等待观察 → 最终触发但时间不对 → 定位 timezone
关键信号速查
| 现象 | 优先怀疑 |
|---|---|
| 手动触发正常,schedule 无记录 | 平台事件投递或 timezone 问题 |
| schedule 触发了但时间不符合预期 | timezone 解释不一致 |
| 长时间无 schedule 后突然出现 | 改用纯 UTC cron |
| 所有仓库 schedule 都不触发 | 账号级别问题,考虑联系 GitHub Support |
最佳实践
- 永远使用纯 UTC cron,手动换算时区,不依赖
timezone字段 - 保留
workflow_dispatch作为手动触发入口,用于调试和紧急执行 - 用注释标明北京时间,避免后续维护者误读 cron 表达式
- schedule 触发可能延迟数分钟到数十分钟,不要将定时任务设计为精确到秒
- 仓库长期无活动时,GitHub 可能自动禁用 scheduled workflow,需定期维护
本文基于真实排查经验编写,已去除所有敏感信息。核心价值在于揭示
timezone字段的实际行为与文档描述之间的偏差,以及一套可复用的 schedule 问题排查思路。