GitHub Actions 时区 Cron 和 Environment deployment false 实战

前言

我以前写 GitHub Actions 定时任务时,最容易出错的地方不是 Cron 表达式本身,而是时区换算。

比如想让任务在北京时间每个工作日早上 9 点执行,过去一般要换算成 UTC,再写成凌晨 1 点。一个人维护还好,团队里其他人看到 0 1 * * 1-5,很难马上判断这是北京时间 9 点,还是写错了凌晨任务。

Environment 也有类似问题。很多 CI 任务只是想复用某个环境里的 secrets 和 variables,比如集成测试要连 staging API、Terraform plan 要读取 production 配置。但只要 job 引用了 environment,就容易在部署历史里留下记录。时间一长,真正的部署和普通测试混在一起,部署记录就不干净了。

现在这两个地方都可以写得更直接。on.schedule 可以通过 timezone 按本地时区解释 Cron;environment 可以通过 deployment: false 只使用环境密钥和变量,不创建 deployment object。定时任务仍然运行在默认分支最新提交上,最短调度间隔还是 5 分钟;deployment: false 也不会绕过 wait timer 和 required reviewers。

一、定时任务终于可以按本地时间写

过去写 GitHub Actions 定时任务,默认按 UTC 解释。

假设我要在北京时间每个工作日上午 9 点跑报表,旧写法通常是:

yaml 复制代码
on:
  schedule:
    - cron: '0 1 * * 1-5'

这段配置能跑,但可读性很差。你要在脑子里做一次 UTC+8 的换算,才能知道它其实对应北京时间 9 点。对于上海、纽约、伦敦这种跨时区团队,表达式会越来越难维护。

现在可以直接加 timezone

yaml 复制代码
name: Daily Report

on:
  schedule:
    - cron: '0 9 * * 1-5'
      timezone: 'Asia/Shanghai'

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: ./scripts/generate-report.sh

这个配置读起来就很直观:工作日早上 9 点,按 Asia/Shanghai 执行。Cron 表达式仍然是五段 POSIX Cron,分别表示分钟、小时、日期、月份、星期。timezone 使用 IANA 时区字符串,比如 Asia/ShanghaiAmerica/New_YorkEurope/London

这里有两个细节值得注意。

第一个是夏令时。像 America/New_York 这类会进入夏令时的时区,调度会按该时区规则处理。遇到春季跳过的时间段,比如 2:30 不存在,会顺延到下一个有效时间。国内常用的 Asia/Shanghai 没有这个问题,但海外团队要留意。

第二个是默认分支。定时任务跑的是默认分支上的最新提交,不是你某个 feature 分支上的 workflow 文件。你在分支里改了定时配置,如果还没合并到默认分支,不要期待它按新规则执行。

二、多时区任务要写清楚触发来源

一个 workflow 可以配置多个 schedule。这样很适合全球化团队,比如中国区、美国区、欧洲区各跑一份日报。

yaml 复制代码
name: Regional Reports

on:
  schedule:
    - cron: '0 8 * * 1-5'
      timezone: 'Asia/Shanghai'
    - cron: '0 8 * * 1-5'
      timezone: 'America/New_York'
    - cron: '0 8 * * 1-5'
      timezone: 'Europe/London'

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Generate regional report
        run: ./scripts/generate-report.sh

这段配置有一个问题:三个 schedule 都会触发同一个 job,但 job 内部不知道当前是哪一个地区触发的。简单项目里可以接受,真实项目最好把触发来源判断清楚。

github.event.schedule 可以拿到当前触发的 Cron 字符串。可问题是这里三个 Cron 都是 0 8 * * 1-5,只靠它区分不出地区。所以多时区任务最好把 Cron 时间写出差异,或者拆成多个 workflow。

如果想保留在一个文件里,可以这样处理:

yaml 复制代码
name: Regional Reports

on:
  schedule:
    - cron: '0 8 * * 1-5'
      timezone: 'Asia/Shanghai'
    - cron: '5 8 * * 1-5'
      timezone: 'America/New_York'
    - cron: '10 8 * * 1-5'
      timezone: 'Europe/London'

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      - name: Set region
        id: region
        shell: bash
        run: |
          case "${{ github.event.schedule }}" in
            "0 8 * * 1-5")
              echo "region=china" >> "$GITHUB_OUTPUT"
              ;;
            "5 8 * * 1-5")
              echo "region=us" >> "$GITHUB_OUTPUT"
              ;;
            "10 8 * * 1-5")
              echo "region=europe" >> "$GITHUB_OUTPUT"
              ;;
            *)
              echo "region=unknown" >> "$GITHUB_OUTPUT"
              ;;
          esac

      - name: Generate report
        run: ./scripts/generate-report.sh "${{ steps.region.outputs.region }}"

我会更偏向把不同地区的 Cron 分开一点,避免后面定位问题时看不出是哪一条规则触发。虽然多条 schedule 可以放在一个 workflow 里,但一旦地区逻辑差异变大,拆成多个 workflow 会更清楚。

还要留意触发频率。定时任务最短只能 5 分钟跑一次。想做高频轮询、秒级调度、严格 SLA 的任务,GitHub Actions 不太适合当主调度器。它更适合日常报表、依赖检查、数据同步、清理任务这类工程自动化。

三、只想拿环境密钥,不一定要创建部署记录

Environment 原本更偏部署场景。比如 production environment 里放生产密钥、配置 required reviewers、加 wait timer,job 引用它后会产生部署记录。

这个行为对真正部署很合理:

yaml 复制代码
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/checkout@v6
      - run: ./scripts/deploy.sh

问题出在非部署场景。

比如集成测试只是想拿 staging 的 API Key:

yaml 复制代码
jobs:
  integration-test:
    runs-on: ubuntu-latest
    environment:
      name: staging
    steps:
      - uses: actions/checkout@v6
      - run: npm run test:integration

这类 job 并没有部署 staging,却可能让环境历史里多出一条记录。时间久了,部署记录会被测试、扫描、检查任务污染。

现在可以这样写:

yaml 复制代码
jobs:
  integration-test:
    runs-on: ubuntu-latest
    environment:
      name: staging
      deployment: false
    steps:
      - uses: actions/checkout@v6

      - name: Run integration tests
        env:
          API_KEY: ${{ secrets.API_KEY }}
          API_BASE_URL: ${{ vars.API_BASE_URL }}
        run: npm run test:integration

deployment: false 的作用很明确:job 仍然能访问 environment secrets 和 variables,但不会创建 deployment object,也不会更新该环境的部署历史。这个配置适合 CI、集成测试、安全扫描、Terraform plan、配置校验这些只需要环境配置、不执行真实部署的任务。

它不会绕过环境保护规则。wait timer 仍然会生效,required reviewers 仍然需要审批。也就是说,如果你把 staging 配成了必须审批,那么测试 job 即使写了 deployment: false,也还是要等审批。

这里最容易踩的坑是 custom deployment protection rules。它依赖 deployment object 工作,如果 environment 上配置了这类 GitHub App 保护规则,再写 deployment: false,job 会直接失败。遇到这种情况,要么去掉 deployment: false,让它按部署流程跑;要么把这类 CI 密钥放到一个没有 custom deployment protection rules 的 environment 里。

四、发布前检查可以复用环境配置,但不要污染部署历史

deployment: false 最适合放在发布前检查里。

比如一个生产发布流程,真正部署前要先做几件事:

检查数据库迁移是否可执行。

检查第三方服务配置是否完整。

跑一轮 smoke test。

生成发布说明或变更摘要。

这些步骤需要读取 production 或 release environment 里的配置,但它们本身还没有部署。可以把检查阶段和部署阶段拆开:

yaml 复制代码
name: Release

on:
  workflow_dispatch:

jobs:
  preflight:
    runs-on: ubuntu-latest
    environment:
      name: production
      deployment: false
    steps:
      - uses: actions/checkout@v6

      - name: Check release readiness
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_BASE_URL: ${{ vars.API_BASE_URL }}
        run: ./scripts/check-release-readiness.sh

  deploy:
    runs-on: ubuntu-latest
    needs: preflight
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/checkout@v6

      - name: Deploy production
        run: ./scripts/deploy-production.sh

这个结构更清楚。

preflight 用 production 的 secrets 和 variables 做检查,但不写入部署历史。

deploy 才是真正的生产部署,会创建部署记录。

如果 production environment 配了 required reviewers,两个 job 都可能等待审批。想减少重复审批,可以单独建一个 production-checks environment,把发布前检查需要的只读密钥放进去,部署 job 继续使用 production environment。

yaml 复制代码
jobs:
  preflight:
    runs-on: ubuntu-latest
    environment:
      name: production-checks
      deployment: false
    steps:
      - uses: actions/checkout@v6
      - run: ./scripts/check-release-readiness.sh

  deploy:
    runs-on: ubuntu-latest
    needs: preflight
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/checkout@v6
      - run: ./scripts/deploy-production.sh

这种拆法在团队里更容易维护。检查环境放只读配置,部署环境放真正高权限密钥。权限边界清楚以后,deployment: false 的价值会更明显。

五、定时任务、Environment 和 concurrency 要一起考虑

定时任务一旦和环境配置结合,就要考虑并发。

比如每天早上 8 点按多个地区生成报表,如果前一次任务还没跑完,下一次又触发了,可能会同时写同一份报表、同一个对象存储路径,或者同时更新外部系统。

可以给同一地区加并发组:

yaml 复制代码
name: Regional Scheduled Report

on:
  schedule:
    - cron: '0 8 * * 1-5'
      timezone: 'Asia/Shanghai'

concurrency:
  group: report-china
  queue: max

jobs:
  report:
    runs-on: ubuntu-latest
    environment:
      name: reporting
      deployment: false
    steps:
      - uses: actions/checkout@v6

      - name: Generate report
        env:
          REPORT_BUCKET: ${{ vars.REPORT_BUCKET }}
          REPORT_TOKEN: ${{ secrets.REPORT_TOKEN }}
        run: ./scripts/generate-report.sh china

这里我会用 queue: max,让任务排队,而不是取消正在执行的任务。报表生成通常有副作用,直接取消可能留下半成品文件。如果只是普通 CI 检查,可以用 cancel-in-progress: true;如果会写外部系统,排队更稳。

如果一个 workflow 里有多个地区,可以把地区名放进并发组:

yaml 复制代码
concurrency:
  group: report-${{ inputs.region || 'scheduled' }}
  queue: max

不过 schedule 触发时没有 inputs.region。如果地区判断来自 github.event.schedule,并发组也要跟着设计,不要让不同地区全部挤到同一个 report 组里。

这个地方有一个实用原则:哪个资源会互相抢,就按哪个资源设计并发组。多个地区写同一个外部系统,就用同一个组排队;多个地区写不同 bucket,就按地区分组。

总结

时区 Cron 解决的是定时任务可读性问题。过去要在本地时间和 UTC 之间来回换算,现在可以直接写 timezone: 'Asia/Shanghai'timezone: 'America/New_York' 这类 IANA 时区字符串。对跨时区团队来说,这个改动能减少很多误解。

deployment: false 解决的是 environment 复用问题。CI、集成测试、发布前检查、Terraform plan 这些任务,经常需要环境 secrets 和 variables,但不应该出现在部署历史里。现在可以只拿环境配置,不创建 deployment object。

这两个能力都有边界。定时任务仍然跑默认分支最新提交,最短间隔还是 5 分钟。deployment: false 仍然会受 wait timer 和 required reviewers 影响,也不能和 custom deployment protection rules 一起用。

我的建议是先改两类 workflow。

第一类是定时任务。把原来需要手动换算 UTC 的 Cron 改成带 timezone 的本地时间表达,顺手检查是否需要 concurrency

第二类是 CI 里引用 environment 的 job。只做测试、扫描、plan、preflight 的 job,加上 deployment: false,让部署历史只保留真正的部署。

这样改完以后,Actions 配置会更接近人的理解方式。定时任务按业务时间写,测试任务只拿配置,部署任务才进入部署历史。

相关推荐
2601_955781986 小时前
整合Kimi 大模型 OpenClaw 自动化能力再度升级
开源·github·kimi·open claw安装·open claw部署
淘矿人6 小时前
【AI大模型】AI 大模型推理平台完整测评:8 家主流聚合服务对比分析
人工智能·sql·gpt·学习·github·php
逛逛GitHub6 小时前
有人花 3 天做了个开源工具,一句话生成各种场景的 HTML。
github
归故里8 小时前
harmony-next.skills 为 AI 而生!
前端·后端·github
右耳朵猫AI8 小时前
Github趋势榜 2026年第15周
github
米高梅狮子9 小时前
01.mysql的备份与恢复
运维·数据库·mysql·docker·容器·kubernetes·github
本地化文档9 小时前
rust-style-guide-l10n
rust·github·gitcode
Revio Lab9 小时前
如何在国内API方式接入 Claude Opus 4.7、GPT-5.5、GPT Image 2
gpt·github
猫猫与橙子9 小时前
ubuntu22.04(google浏览器)使用开发者边车(DEV-SIDECAR)的使用
github