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

最佳实践

  1. 永远使用纯 UTC cron,手动换算时区,不依赖 timezone 字段
  2. 保留 workflow_dispatch 作为手动触发入口,用于调试和紧急执行
  3. 用注释标明北京时间,避免后续维护者误读 cron 表达式
  4. schedule 触发可能延迟数分钟到数十分钟,不要将定时任务设计为精确到秒
  5. 仓库长期无活动时,GitHub 可能自动禁用 scheduled workflow,需定期维护

本文基于真实排查经验编写,已去除所有敏感信息。核心价值在于揭示 timezone 字段的实际行为与文档描述之间的偏差,以及一套可复用的 schedule 问题排查思路。