把青龙上的定时脚本搬去 GitHub Actions:免费额度实测、五个必踩的坑,和什么不该搬

6 阅读6分钟

背景:为什么非要把脚本搬走

上一篇文章里那台 CM311-1A 还在跑,2G 内存 7 个容器,青龙面板上挂着一堆定时任务。

但它有三个绕不过去的短板:

问题具体表现
eMMC 寿命16G 存储写满过一次,日志和数据库反复写同一个块
内存天花板2G,多开一个容器就可能触发 OOM Killer
单点家里断电、宽带故障、路由器重启,任务全停,而且没人告诉你

前两个靠调优能压住,第三个不行。家宽没有固定公网 IP,机器一挂就是静默失败——你得自己去翻日志才知道昨天没跑。

所以真正想解决的不是"性能",是**"别把定时任务绑在一台会坏的机器上"**。

GitHub Actions 正好是干这个的:不用服务器,不用备案,私有仓库每月免费 2000 分钟。这篇讲清楚它到底能干什么、怎么用、以及那几个文档里不写的坑。

先说结论:免费额度到底够不够

GitHub Free 账号的额度:

项目额度
公开仓库标准 runner 完全免费,不计分钟
私有仓库2000 分钟/月
计费倍率Linux 1× / Windows 2× / macOS 10×
存储(Artifact 等)500 MB
单 job 最长6 小时
免费账号并发20 个 job
日志保留默认 90 天,可调

算一笔账。一个签到脚本单次跑 1 分钟左右,一天 3 次,一个月 90 次 ≈ 90 分钟。再加一个监控脚本一天 2 次,60 分钟。一个月总共 150 分钟,只用掉额度的 7.5%。

只要你的脚本不是拿 Actions 当视频转码机,额度根本用不完。

三个必须记住的硬上限:

  • 单 job 6 小时,超时直接杀,没有商量
  • 定时任务是"尽量准时",不是"准时",高峰期延迟 5–30 分钟是常态
  • Windows 和 macOS runner 按 2× / 10× 计费,能上 Linux 就别用别的

它到底是怎么跑起来的

四层结构,一层套一层:

workflow   ← 一个 .yml 文件 = 一个工作流
  └─ job   ← 可以有多个 job,默认并行
       └─ step  ← 顺序执行的步骤
            └─ action ← 可复用的封装,如 actions/checkout@v4

一次运行的完整链路:

GitHub 收到事件(定时到点 / 你手动点 / 有人 push)
  → 去仓库里找匹配的 .yml
  → 开一台全新的云端虚拟机(Runner)
  → clone 你的仓库
  → 按 steps 逐条执行
  → 跑完销毁,机器上的东西全没了

最后一句是重点:机器是一次性的。这一点决定了后面所有的设计取舍,第五节的坑 5 会展开。

Runner 有两种。GitHub-hosted 就是上面这种,用完销毁,算在额度里;Self-hosted 是你自己的机器,挂上去接任务,不占 GitHub 分钟数,但要自己维护、自己保证在线——那又回到"绑在一台机器上"的老问题了,除非你本来就有一台常年开着的 VPS。

五分钟跑通第一个工作流

建文件

路径必须是这个,一个字都不能错:

你的仓库/
└── .github/
    └── workflows/
        └── hello.yml

workflows 是复数,.github 前面那个点不能少。

写最小版本

name: Hello Actions

on:
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: 打招呼
        run: echo "你好,现在是 $(date)"
      - name: 看看环境
        run: |
          python3 --version
          node --version

workflow_dispatch: 这一行表示"允许手动触发",第一版先用它,方便调试。

提交到默认分支

这一步很多人栽。schedule 和 workflow_dispatch 只认默认分支上的 workflow 文件。你在别的分支建的 yml,Actions 页面上 Run workflow 按钮压根不出现,你会以为是权限问题,其实只是分支不对。

手动跑一次

仓库首页 → 顶部 Actions 标签 → 左侧选你的工作流 → 右上角 Run workflow → 选分支 → 再点一次绿色按钮。

第一次进 Actions 页可能会看到一条横幅 I understand my workflows, go ahead and enable them,点掉就行。

看日志

点进某次运行 → 左侧点 job 名 → 展开每个 step。echo 和 print 的输出全在这。

右上角 ... 菜单里有 Download log archive,可以把整次运行的日志打包下载。

cron 是 UTC,而且会延迟

这是新手最容易搞错的地方。

cron 表达式用的是 UTC 时间,北京时间 = UTC + 8。所以:

schedule:
  - cron: "0 23 * * *"   # UTC 23:00 = 北京 07:00
  - cron: "0 4 * * *"    # UTC 04:00 = 北京 12:00
  - cron: "30 15 * * *"  # UTC 15:30 = 北京 23:30

写 0 7 * * * 以为是早上 7 点,实际是北京时间下午 3 点。

还有一个更隐蔽的误解:工作流里写 TZ: Asia/Shanghai 这个环境变量,只影响脚本内部打印出来的日志时间,完全不影响 cron 的触发时刻。cron 那一层是 GitHub 的调度器在算,跟你 job 里的环境变量没有半点关系。

很多人看到日志里时间是"对的",就以为 cron 也对,结果触发时刻一直歪着。这两件事必须分开看。

延迟也要有心理预期。定时任务在整点前后是全局高峰,延迟 5–30 分钟是正常现象。签到、日报、监控这类场景完全无所谓;抢购、秒杀这类场景想都不要想。

新版 UI 把入口藏起来了

如果你按老教程走「Settings → Secrets and variables → Actions」,会在正确的页面上找不到那一项。

原因很简单:GitHub 把仓库设置的左侧栏改成了分组结构,Secrets and variables 被挪到另一个分组下面了。真实结构是这样:

分组菜单项
(最上方)General、Collaborators
AccessCollaborators
Code, planning, and automationRulesets、Branches、Tags、Actions(可展开)、Webhooks、Copilot、Pages
Security and qualityAdvanced Security、Deploy keys、Secrets and variables(可展开)
IntegrationsGitHub Apps

Secrets and variables 在 Security and quality 分组里。 这是最容易卡住的一步。

比记菜单更省事的办法是直接用直达链接,UI 会改,URL 基本不会:

要干什么路径
加/改密钥/settings/secrets/actions
加普通变量/settings/variables/actions
改 Workflow permissions/settings/actions
工作流运行列表/actions
某个工作流的记录/actions/workflows/你的.yml
用量统计/settings/billing/usage
管理 PAT/settings/tokens

两个高频操作的精确路径

加密钥:

仓库首页 → Settings
  → 左侧 Security and quality 分组
    → Secrets and variables
      → Actions
        → 右上 "New repository secret"

新版这里多了一个子项叫 Agents(给 Copilot Agent 用的),别点错,要的是 Actions。加完之后列表里只显示名字和更新时间,看不到值,忘了只能重新加。

把 GITHUB_TOKEN 改成可写:

Settings → Code, planning, and automation
  → Actions → General
    → 往下滚,找到 "Workflow permissions"
      → 选 "Read and write permissions"
        → 点这一块的 Save

注意这个页面上有 4 个独立的 Save 按钮,分别属于 Actions permissions / Retention / Fork pull request / Workflow permissions。改哪块就点哪块的 Save,点错了看着像保存了其实没生效。新版最常漏的就是这一步。

五个坑,每个都让人重装一次

坑 1:60 天不活动,定时任务自动停用

仓库 60 天内没有任何提交活动,GitHub 会自动禁用所有 schedule,同时发一封邮件——但邮件很容易被当成通知忽略掉。

症状特别迷惑:手动点 Run workflow 一切正常,定时就是不响。

排查的时候如果只测手动触发,会得出"工作流没问题"的结论,然后永远找不到原因。

补救很简单:随便提交一次代码,或者进 Actions 页面点 Enable workflow。

顺带一提,如果你的工作流本身会往仓库回写文件(比如保存登录态),那每天都有提交,不会触发这个限制。一旦你哪天把回写关掉了,这个问题就会在两个月后突然出现。

坑 2:GITHUB_TOKEN 默认只有读权限

新建或导入的仓库,Workflow permissions 默认是 read-only。工作流里只要出现 git push,必然失败:

remote: Permission to xxx.git denied

改法见上面那一节。这个坑的特点是报错信息很像凭据错误,容易往 PAT 方向查很久。

坑 3:on: 在 YAML 里会变成 True

YAML 1.1 规范把裸词 on、off、yes、no 都当布尔值。所以你在本地用 PyYAML 读工作流文件,会发现:

import yaml
d = yaml.safe_load(open('.github/workflows/x.yml'))
print(d['on'])    # KeyError
print(d[True])    # 这才是你要的

GitHub 服务端不受影响,它自己解析。但如果你写本地校验脚本,必须写成 d.get('on') or d.get(True),否则会误判成"这个 workflow 没有触发条件"。

坑 4:多个工作流同时推 main 会撞车

每个 workflow 的 checkout 都是各自的独立快照。A 先 push 成功,B 再 push 就会被拒:

! [rejected]  main -> main (fetch first)

如果两个工作流都往同一个仓库回写文件,而且是同一时刻触发,这个冲突几乎必然发生。

解决办法有两种:给每个工作流加不同的 concurrency group,或者在工作流里写"被拒 → fetch → reset --hard → 重试"的逻辑。后一种更稳但要多写几行:

for i in 1 2 3 4 5; do
  git add -A && git commit -m "update state" || exit 0
  git push && break
  git fetch origin main
  git reset --hard origin/main
  sleep $((i * 3))
done

纪律上还有一条:手动触发时一次只点一个,别同时点三个。

坑 5:Runner 上的改动默认全丢

这个最根本。每次运行都是全新虚拟机,跑完之后:

  • 生成的文件没了
  • 数据库没了
  • 去重标记没了
  • 下载的缓存没了

唯一能留存的方式是把文件 git push 回仓库。

所以你的工作流最后往往需要一个"Persist state"步骤——把登录态、进度文件、去重列表提交回去。这也是为什么 Actions 不适合跑那种状态一大堆的脚本。

有人会想到 Actions Cache。它确实能存文件,但有清理策略,不是可靠的持久存储,别把它当唯一存储用。小 json 就 push 回仓库,敏感凭据放 Secrets,大数据老老实实用外部服务。

什么脚本能搬,什么不能

判断标准只有一个:这个脚本有没有"必须留住"的东西?

依赖特征支持度说明
纯 HTTP + 无状态✅ 完美最理想的用例
需要保存登录态 / cookie⚠️ 需改造必须 push 回仓库或外部存储
依赖青龙专有 API(sendNotify 等)⚠️ 要替换直接 POST 推送服务即可
依赖 MySQL / Redis⚠️ 要外接云库Runner 上没有本地持久库
需要浏览器(Playwright)⚠️ 能跑但慢每次装 Chromium,几百 MB
单次跑超 6 小时❌ 不行硬上限
需要固定出口 IP❌ 很难Runner 在 Azure,IP 每次不同
需要 GUI / 扫码❌ 不行没有交互界面

迁移的时候有四件事一定要做:

  1. 先列出所有"需要留存"的东西。登录态、去重标记、累计进度 → 必须留存;临时缓存 → 直接不管
  2. 选留存方式。小 json → push 回仓库;敏感凭据 → Secrets;大数据 → 外部服务
  3. 砍掉平台专有依赖。sendNotify → 直接 POST 推送接口;面板定时 → workflow cron;读取面板配置 → 环境变量
  4. 配置搬进 Secrets。名字保持不变,脚本里 os.getenv("XXX") 一行都不用改

有一个判断特别反直觉:需要固定 IP 或者设备指纹的脚本,不适合搬到 Actions。 因为 Runner 位于 Azure,出口 IP 每次都不一样,绑定 IP 的登录态第二天就掉。这类脚本恰恰需要一台固定 IP 的常驻机器。

混合结构才是正解

所以最后的结论不是"全部搬过去",而是分工:

任务类型放哪理由
无状态签到、日报、监控GitHub Actions免费、不占机器、挂了有邮件
需要固定 IP / 登录态的自建机器(青龙)Runner 出口 IP 一直在变
需要长时运行(>6h)自建机器Actions 硬上限
需要扫码 / GUI 的自建机器Runner 没有界面

两边都往同一个推送通道发通知,手机上看到的结果是一样的。

这样做的收益很实际:盒子挂了,签到和监控还在跑;盒子恢复之后,需要固定身份的那部分任务照常继续。单点故障从"全部停摆"降级成"部分降级"。

如果非要给一个优先级:先把每天都要跑、且必须跑成功的那个任务搬出去,剩下的慢慢来。

机器还在跑,欢迎交流。


本文同步发布于 xiaoyiweb.xyz,一台 2G 电视盒子上跑起来的那点东西,都整理在站内手册里。