我给你用最直白、最接地气的方式讲清楚,你马上就能懂👇 # 1. npm run selftest:pre-commit 通过 是什么意思? 这句话翻译成人话: ### 你本地电脑上,「提交前自检脚本」跑完全部通过了 ✅ 它具体做了这些事: - 运行代码格式检查(lint) - 运行代码规范检查 - 运行单元测试/基础自测 - 检查有没有语法错误、格式错误、明显bug 通过 = 你现在的代码可以安全 commit(提交) 不通过 = 不让你提交,必须先修复错误 --- # 2. CI 可以在本地跑吗? ### 完全可以!而且你现在就在本地跑 CI! 你看到的: - local checks = 本地版 CI 检查 - CI checks = 服务器版 CI 检查 它们99% 是同一套脚本,只是运行环境不一样: - 本地:你电脑 - 服务器:GitHub / GitLab 云端机器 ### 所以: 你本地跑 npm run selftest:pre-commit = 你在本地提前模拟跑了一遍 CI 好处: - 不用推到云端就知道能不能过 - 提前修复,避免云端 CI 变红 - 速度快很多 --- # 3. 你现在这套结构是什么?(超级重要) 你现在是双层保护: ### 第一层(本地) git 提交前 → 自动跑 selftest → 不通过不让提交 ### 第二层(云端) git push 后 → CI 自动再跑一遍 same checks → 不通过不让合并代码 --- # 4. 超简总结(你只需要记这 3 句) 1. npm run selftest:pre-commit 通过 = 本地提交前自检全过,可以提交代码。 2. CI 可以在本地跑 = 你现在跑的就是本地版 CI,和云端逻辑一样。 3. 你现在是双层防护 = 本地拦一次 + CI 拦一次,保证代码不会乱提交。 --- 如果你愿意,我可以用一张图给你画出你现在的完整提交流程,让你一眼看懂整个门禁机制!
CI/CD 自动部署:从 GitHub-hosted 账单门禁切到 170 自托管 runner 的一套正确做法
先说结论
这次最关键的认知不是“把 workflow 写出来了”,而是:
自动部署是否真的可用
!=
仓库里有一个 GitHub Actions YAML真正可用的自动部署,至少要同时满足四件事:
push后确实会触发 job- job 确实能在真实执行机上跑起来
- 部署脚本走的是 canonical 路径,不是临时拼装
- 部署后有公网与内部双验证,而不是只看 workflow 绿了
这次 Token Pool 的落地,最终收敛成了下面这条真实链路:
开发机改代码
↓
push 到 GitHub main
↓
GitHub Actions workflow: .github/workflows/deploy-runtime.yml
↓
170 自托管 runner: token-pool-prod-170
↓
actions/checkout
↓
deploy/deploy-runtime-local.sh
↓
git archive HEAD -> tar -xf -> npm ci --omit=dev
↓
systemctl restart token-pool-gateway-prod.service
↓
内部健康检查 + 公网入口验证一、这次真正踩到的坑,不是代码,是执行地点
一开始表面上看,是“已经有自动部署 workflow 了,但怎么就是不自动上线”。
后来真正查出来,问题根本不在业务代码,而在执行地点:
workflow 跑在 GitHub-hosted runner
↓
GitHub 账号被账单 / spending limit 门禁卡住
↓
job 连启动都启动不了
↓
看起来像“自动部署坏了”这个阶段最容易犯的错是继续改 YAML 细节,或者继续怀疑 SSH。
但只要 job 都没真的跑起来,后面所有 SSH、scp、docker、systemctl 都是伪命题。
所以第一条工程原则是:
先确认 job 到底跑在哪台机器上如果执行器本身起不来,再漂亮的 workflow 也是假的。
二、为什么 GitHub-hosted + SSH 容易变成“看似自动,实则脆弱”
很多项目的第一版自动部署都会写成这样:
GitHub-hosted runner
↓
SSH 到服务器
↓
scp 文件
↓
远程执行 restart这条链不是不能用,但它天然有几个问题:
1. 执行器和运行机分离
真正跑服务的是服务器,但真正执行部署的是 GitHub 的云主机。
这意味着:
- 中间多一层网络和 SSH 依赖
- 需要 repo secret 里长期保留部署私钥
- 任何 GitHub-hosted 额度、账单、并发限制,都会直接影响 deploy
2. YAML 容易堆成脚本坟场
如果把大量 SSH 逻辑直接塞进 workflow:
- 可读性会越来越差
- 别的项目很难复用
- 出问题时很难判断到底是 workflow、SSH、还是部署逻辑坏了
3. 容易误把“push 成功”当成“deploy 成功”
这类项目里最常见的错觉就是:
我已经 push 了
=
线上应该已经更新实际上必须拆开看:
push
↓
是否触发 workflow
↓
workflow 是否真的开始执行
↓
部署脚本是否成功
↓
服务是否成功重启
↓
公网是否验证通过任何一步没过,都不能叫“已部署”。
三、为什么这次要改成 170 自托管 runner
这次的正确解法,不是继续打补丁,而是把 deploy job 的执行地点直接迁到真实运行机:
GitHub 只负责下发任务
170 自己执行 checkout / deploy / restart / verify这样做的直接收益:
1. 避开 GitHub-hosted 账单门禁
job 不再依赖 GitHub 的云主机额度,而是跑在你自己的服务器上。
2. 自动部署链更短
从:
GitHub runner -> SSH -> 170变成:
170 本机 runner -> 170 本机 runtime中间少了一整层“远程跳板”复杂度。
3. 默认不再需要 repo 级 SSH 私钥
因为 workflow 已经在 170 本机上跑,所以不再需要把 TOKEN_POOL_DEPLOY_SSH_KEY 这种部署私钥长期塞在 GitHub secrets 里。
这件事很重要,因为:
不需要的高权限 secret
=
应该删除不是“留着也没事”。
四、正确结构:runner 负责执行,deploy 脚本负责部署
这次不是只改了一个 workflow,而是把职责拆开了。
1. workflow 只负责调度
Deploy runtime to 170 现在做的事很简单:
- 命中路径过滤后触发
- 绑定到
self-hosted, token-pool, production, cn-170 actions/checkout- 调
deploy/deploy-runtime-local.sh
也就是说:
workflow = 调度层2. deploy 脚本负责 canonical 发布动作
真正的发布动作收口在仓内脚本:
deploy/deploy-runtime-local.sh
它负责:
git archive HEAD- 解包到
/srv/token-pool-gateway-prod npm ci --omit=devsystemctl restart token-pool-gateway-prod.service- 健康检查和公网验证
也就是说:
deploy 脚本 = 发布层3. runner 安装模板负责基础设施复用
为了让别的项目也能照搬,这次又额外把 runner 安装动作抽成模板:
deploy/setup-self-hosted-runner.shdeploy/setup-self-hosted-runner.env.example
也就是说:
runner 安装模板 = 基础设施复用层这三层分开之后,整个系统会稳定很多。
五、旧 SSH 发布链应该怎么处理
一个很容易犯的错是:
新链路上线了
↓
旧链路还留着
↓
大家继续混着用这会制造长期认知污染。
正确做法不是“假装旧路径不存在”,而是明确降级:
当前 Token Pool 的做法
deploy/deploy-runtime-local.sh:当前 production 默认发布路径deploy/deploy-runtime-170.ps1:历史 / 应急 SSH 发布路径
而且不只是 README 写一句“历史”就算了,还要在脚本自身里直接提示:
emergency SSH deploy path in use
prefer deploy-runtime-local.sh for normal production deploys这样就算有人直接打开旧脚本,也会立刻看到它不是默认路径。
六、secret 管理的正确原则
这次有一个很值钱的收尾动作:
把 TOKEN_POOL_DEPLOY_SSH_KEY 从 GitHub repo secrets 删除为什么要删?
因为它已经不是自动部署必需项了。
如果继续留着,会带来两个问题:
1. 误导认知
后来的维护者会以为:
自动部署还依赖 SSH 私钥实际上已经不依赖。
2. 扩大不必要的敏感面
一个 secret 只要存在,就意味着:
- 它可能被误用
- 它需要被轮换
- 它增加了配置复杂度
所以工程上更好的规则是:
不再需要的 secret,应该删除,而不是保留当前这套结构下,保留的只应该是:
TOKEN_POOL_DEPLOY_VERIFY_TOKEN
它用于部署后的受保护接口验证,仍然有实际价值。
七、怎么判断“自动部署真的正常了”
不要只看:
- 本地没报错
- YAML 看起来很漂亮
- GitHub 页面有个绿色勾
真正应该看的是这几层证据:
1. 最新 workflow run 成功
而且要确认是跑在目标 self-hosted runner 上。
2. 服务真重启成功
例如:
token-pool-gateway-prod.service = active3. 内部健康检查通过
例如:
http://127.0.0.1:3301/health
4. 公网入口验证通过
例如:
https://pool-console.tengokukk.com/api/healthhttps://pool-console.tengokukk.com/console-next/https://key.tengokukk.com/openai/v1/responses
只有这些证据一起成立,才能说:
自动部署正常而不是“看起来好像正常”。
八、给以后项目复用的最小模板
如果别的项目也有一台长期在线的生产机,推荐直接复用这套思路:
项目 repo
├── .github/workflows/deploy-runtime.yml
├── deploy/deploy-runtime-local.sh
├── deploy/setup-self-hosted-runner.sh
└── deploy/setup-self-hosted-runner.env.example最小原则只有五条:
- 有长期在线主机时,优先自托管 runner,不优先 GitHub-hosted + SSH
- workflow 只做调度,不在 YAML 里塞满部署细节
- 部署动作收口到 repo 内 canonical 脚本
- deploy 后必须做内部 + 公网双验证
- 不再需要的 deploy secret 必须删除
九、一句话总结
这次最值钱的不是“把自动部署打通了”,而是把下面这套工程认知真正落地了:
执行器在哪里
比 YAML 写得多漂亮更重要以及:
push != deployed
workflow success != runtime success
不再需要的 secret != 留着备用如果把这三句话记住,后面很多 CI/CD 假成功、部署误判、权限污染的问题,都会少很多。