GitHub 自动提交刷绿墙:从一行 cron 到一个带面板的小软件
想做什么
GitHub 个人主页那张 contribution 图,空一两天没事,但要是天天都是白的,看着总觉得差点意思。我又不想为了填格子去硬造「有意义」的提交——那样更没劲。
所以就有了这个:一个靠 GitHub Actions 定时跑的小东西,自动往一个专用私有仓库里随机提交几次。提交内容没有任何实际意义,只往一个日志文件里追加一行时间戳,也不碰任何真实项目。
这个东西前后折腾了两天,从最初 20 行的 workflow 文件,一路长成了带桌面面板、邮件通知、还能打包成 exe 发给朋友的小软件。这篇文章把它整个记下来——包括每一个踩过的坑。
第一版:一个 yml 文件
最初的核心就一个文件:.github/workflows/daily.yml。
1 | on: |
为什么放云端跑而不是本机计划任务:电脑得开机、令牌得存本机,重装个系统就断了。GitHub Actions 跑在它自家服务器上,只要仓库在它就跑,跟我的电脑没有任何关系。
这个版本能用,当天确实绿了。但用了没两天就不满足了。
用着用着就开始不爽
不爽的点很具体:
- 想改提交时间,得改 yml 里的 cron、commit、push 一整趟。 cron 是 UTC 时间,每次都得心算加八小时;
- GitHub 的调度不靠谱,说好 20:30 跑,实际经常拖到九十点;
- 想让某天不提交(比如我在外面玩),没有开关。
于是改了设计:提交时间不再写死在 yml 里,而是放在仓库根目录的 config.json,workflow 改成每 30 分钟醒来检查一次「现在到没到配置的时间点」,到了才提交。
1 | on: |
config.json 长这样:
1 | { |
几个字段的含义:default_times 是每天的默认时间点;overrides 可以单独指定某天(设成 [] 就是当天不提交);count_range 是每个时间点随机提交几次;executed 是脚本自动维护的状态,记录当天哪些时间点已经执行过,防止同一时间点被反复执行。
还有个细节值得说:脚本只在一轮推送成功之后才把时间点记进 executed。这样哪怕某轮跑挂了,半小时后的下一轮会自动补上,不会出现「到点了但没提交、还以为提交过了」的洞。
从此改时间就是改一个 JSON 文件的事。但很快,连 JSON 都懒得改了——这是后话。
踩坑实录
坑一:提交不进贡献图,99% 是邮箱问题。 GitHub 只统计「账号关联邮箱」的提交。git 配置里的真实邮箱如果没在 GitHub 验证过,提交了也白提。解法是用 noreply 邮箱,格式形如 123456+username@users.noreply.github.com,在 Settings → Emails 页面顶部能看到。它永远有效,还不暴露真实邮箱。
坑二:push workflow 文件被拒。 手动生成的 Personal Access Token 推含 .github/workflows/ 的提交时,必须额外勾 workflow scope,否则被拒。当时卡了一会儿,重新授权才推上去。
坑三:私有仓库的绿点默认不显示。 得去 profile 设置里打开 “Include private contributions on my profile”。
坑四:GitHub 的 cron 是「尽力而为」。 文档写得客气,实际体验就是延迟半小时起步,高峰期一两个小时也正常。有天凌晨我收到一条 Actions 运行通知,一看时间 01:43——这就是白天某轮被挤掉的检查补跑的,跑完发现没到时间点,什么也没做就结束了。所以别指望分秒不差,这套设计里「每 30 分钟检查一次」本身就是为了吸收这种延迟。
坑五(最隐蔽):子进程污染了 stdout。 加邮件通知的时候,workflow 里用一行 shell 判断「这轮真的提交了吗」:
1 | OUT=$(python3 scripts/auto_commit.py) |
脚本自己只在真提交时才打印 made N commit(s),理论上没问题。但 Python 脚本内部用 subprocess 调 git,子进程默认继承 stdout 管道,于是 git 每次 commit 打的 [main 4ce3456] chore: ... 全混进了 OUT 开头,脚本自己那句 made ... 被挤到了中间。case 是按开头匹配的,结果就是:提交明明成功了,邮件永远静默。查了半天日志,才发现 OUT 开头全是方括号。
修法就一行——把 git 的输出甩去 stderr,stdout 只留协议内容:
1 | subprocess.run(["git", *args], check=True, stdout=sys.stderr) |
教训:当你拿一个进程的 stdout 当通信协议时,它调用的所有子进程都得闭嘴。
坑六:免费额度没想象中耐用。 私有仓库的 Actions 有免费分钟数(每月 2000 分钟),每次运行按分钟向上取整计费。每 30 分钟一跑,一次几十秒也按 1 分钟算:48 次/天 ≈ 1500 分钟/月,居然吃掉四分之三。仓库转 public 就不计时了,或者把间隔拉长。目前我留着 30 分钟,快到限额再说。
干脆做成一个桌面小软件
到了这一步,改个时间点还是得:开网页 → 找到仓库 → 编辑 JSON → commit。一个「改个闹钟」的操作要五步,忍不了。
于是写了 dashboard/server.py:一个只监听 127.0.0.1:8630 的本地小服务,纯 Python 标准库,零第三方依赖,配一个单文件网页界面,再用 Edge 的 --app 模式包成无地址栏的独立窗口——看起来就跟个原生桌面软件差不多。

登录支持三种方式:粘贴 Personal Access Token、导入本机 gh CLI 的登录、还有最舒服的设备配对码——面板显示一个码,去 github.com/login/device 输进去授权,完事。


Token 的存放走了 Windows DPAPI 加密。这里踩了个玄学坑:我一开始用 ctypes 直接调 crypt32 的 CryptProtectData,结果这台机器的 Anaconda Python 一调就整个进程静默消失——没有 traceback,没有报错,stdout 什么都不剩。排查半天换成 PowerShell 的 ConvertTo-SecureString(底层同样是 DPAPI、同样绑定当前用户),一次就稳了。有时候绕过舒适区不如站在舒适区边上。
另一个本地特色问题:直连 api.github.com 时,Python 的 TLS 握手总是被重置,curl 却能通——什么原因懂的都懂。面板里做了个自动回退:直连失败就依次试本机常见的代理端口(7897、7890 这些),哪条通就记住哪条,之后 REST 调用和 gh 子进程全走那条路。
面板能做的事,就是把前面所有的手动操作变成按钮:


改时间点、指定某天不提交、调整随机次数、开关机自启、手动触发一批、看每次云端运行的状态和耗时。顺便说一句,那个「立即提交一次」按钮走的是 workflow_dispatch,会绕过时间表直接强制提交,测试配置改动特别顺手。
邮件通知:只在真提交了才吵你
通知的设计原则只有一条:例行检查不发信,只有真的执行了提交(不管成功失败)才发。不然一天 48 次检查,邮箱先炸。
这就是坑五里那套 committed=true/false 协议的用途——workflow 里 commit job 把结果写进 job output,notify job 挂着 if: needs.commit.outputs.committed == 'true' 的条件,只有真提交了才运行,去读仓库 Secrets 里的 SMTP 配置发邮件。
绑定在面板上完成:填收件邮箱、选服务商、粘贴 SMTP 授权码,面板把它们加密写进仓库的 Actions Secrets,明文任何地方都不显示、不回显。

以 QQ 邮箱为例:网页版设置 → 账号 → 开启 SMTP 服务(要发短信验证)→ 拿到 16 位授权码。注意那不是 QQ 密码,是专门给第三方客户端用的。绑定后点一下「立即提交一次」,一两分钟内就能收到第一封结果邮件,没收到先翻垃圾箱。
打包成 exe,发给朋友
做到这里,这东西已经不太像「我的脚本」,而像个「软件」了。朋友看了说她也想要一个,于是用 PyInstaller 打成了单文件 exe,双击就跑,不用装 Python。
打包本身没什么难度,两个坑值得记:
一是 onefile 模式下资源路径会变——运行时所有文件被解压到一个临时目录(sys._MEIPASS),代码里所有「读当前目录下的文件」都得改成从那里读,否则一启动就找不到自己的网页文件。
二是朋友的机器上没有 gh CLI,写 Secrets 不能再偷懒调命令行,老老实实走 REST API。而 Secrets 写入接口要求用 libsodium 的 sealed-box 加密后提交:先从仓库拿公钥,本地加密,再上传密文。PyNaCl 一个依赖搞定,塞进 exe 里正好。
exe 是通用版:登录谁的账号就管谁的仓库。首次进入时如果没有 <用户名>/daily-commits 这个仓库,面板顶部会出现一条黄色横幅,点「一键创建并部署云端」,它会自动建好私有仓库,把 workflow、脚本、默认配置全部上传,十几秒后就能直接用,全程不碰命令行。

一点实话
这玩意儿填出来的格子,跟「你干了活」是两码事。它就是个装饰,图个自己看着顺眼。真要拿 profile 给谁看,还是得靠那些真实项目里的提交——那个谁都糊弄不了,也没必要糊弄。
不过回头看,为了这几块绿格子顺手学到的东西倒是实打实的:GitHub 的 device flow 登录、DPAPI、libsodium sealed box、PyInstaller 的冻结路径、还有「stdout 是协议通道就别让子进程往里吐」这种一辈子忘不掉的教训。格子是假的,这些是真的。
最初那版 20 行的简单方案我开源了,想抄的直接拿去改:github-auto-commits。带面板的完整版放在私有仓库里——毕竟这个仓库本身就是个替身,把它开源了总感觉有点套娃。文章里该讲的思路都在,想复刻一个不难。
