.github/workflows/deploy-cdn.yml
持续集成工具踩坑实录:从运营脚本崩溃到 JS 构建翻车
在网易做游戏服务端开发这三年,我写过登录服、战斗逻辑、排行榜系统,也熬过无数次版本上线前的通宵。但最让我血压飙升的,不是策划临时改需求,也不是线上玩家炸服投诉——而是 CI/CD 流水线又双叒叕崩了。
尤其是最近半年,随着团队开始搞“自动化一切”,CI 工具成了我们每天打交道最多的东西。而作为一个重度依赖 ChatGPT 写代码、靠 Claude 解 Bug 的懒人开发者,我原本以为配个 pipeline 就是 copy-paste 的事儿。结果现实狠狠扇了我一耳光——你以为只是跑个测试?其实是在玩火。
事情得从去年 Q4 说起。当时我们一个新项目上线前夕,运营同学突然提了个“小需求”:能不能在每次发版后,自动把最新客户端包上传到内部 CDN,并通知运营后台更新版本信息?听起来很简单对吧?不就是加个 post-deploy 脚本嘛!
但问题来了:这个“小脚本”是用 JavaScript 写的(因为运营后台 API 只有 Node.js SDK),而我们的主服务是 C++ + Python 混合栈。CI 环境里压根没装 Node,更别说 npm 依赖了。
于是我在 Jenkinsfile 里加了这么一段:
stage('Deploy to CDN') {
steps {
sh 'npm install'
sh 'node upload.js --env=prod'
}
}
结果第一次跑就报错:
/bin/sh: npm: command not found
哦对,Jenkins slave 是裸的 Ubuntu 镜像,连 Node 都没装。那行,我改用 nvm 安装:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc
nvm install 18
npm install
看起来没问题?但 Jenkins 的每个 sh 指令都是独立 shell 进程!.bashrc 根本不会生效,nvm 装了等于白装。我当时盯着日志看了半小时,差点以为自己 nvm 用错了。
后来还是 ChatGPT 提醒我:要么用 Docker 容器,要么把所有命令塞进同一个 sh 块里,用 && 连起来。改成这样才跑通:
sh '''
curl -o- https https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 18
npm install
node upload.js --env=prod
'''
但这也太丑了!而且每次都要下载 nvm、安装 Node,构建时间直接多了 2 分钟。运营同学还催:“能不能快点?我们等着发公告呢!”
这时候我才意识到:用 JavaScript 写运维脚本本身没错,但把它硬塞进非 JS 项目的 CI 流程里,就是给自己挖坑。
于是我们做了两件事:
- 把上传脚本拆出来,做成一个独立的 GitHub Actions workflow,只在 tag 推送时触发;
- 用 Docker 打包整个 Node 环境,CI 里直接
docker run,避免环境污染。
on:
push:
tags:
- 'v*.*.*'
jobs:
deploy:
runs-on: ubuntu-latest
container: node:18-alpine
steps:
- uses: actions/checkout@v3
- run: npm ci --omit=dev
- run: node scripts/upload.js --env=prod
不仅速度快了(Alpine 镜像轻量),还彻底和主服务的 CI 解耦。运营同学再也不用追着我问“怎么还没更新”。
但坑还没完。
今年年初,我们另一个项目用 TypeScript 重写了管理后台,CI 里加了 ESLint 和单元测试。某天 PR 合并后,CI 显示“✅ All checks passed”,结果第二天 QA 说页面白屏。
查了一圈,发现是构建产物里混进了 dev-only 的代码。原因?process.env.NODE_ENV 在 CI 里没设置,导致 Webpack 默认按 development 模式打包,没做代码压缩和 tree-shaking。
但本地跑 npm run build 没问题啊!后来才发现:Jenkins 的环境变量和本地不一样,而我们的构建脚本依赖 .env 文件,但 .env 被 gitignore 了,CI 里根本不存在。
解决方案很简单:在 CI 配置里显式声明环境变量。
environment {
NODE_ENV = 'production'
}
但为什么没人提前发现?因为我们的 CI 只跑了 lint 和 test,根本没验证构建产物是否能正常运行!这属于典型的“假阳性”——流程走完了,但产出物是废的。
痛定思痛,我们在 CI 最后加了一个“冒烟测试”阶段:用 Puppeteer 启动一个本地服务器,访问首页,检查是否有 JS 报错或 404 资源。
// smoke-test.js
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
page.on('pageerror', err => {
console.error('JS Error:', err);
process.exit(1);
});
await page.goto('http://localhost:3000');
await browser.close();
})();
虽然只多花 15 秒,但从此再也没出现过“CI 过了但页面挂了”的尴尬。
说到这,不得不吐槽下公司某些“DevOps 化”运动的副作用。有些团队为了 KPI,硬要把所有东西塞进 CI,结果 pipeline 又长又脆弱。比如有个项目,CI 里居然跑起了性能压测——每次 PR 都要等 10 分钟,开发者怨声载道。
CI 不是垃圾桶,不是什么东西都能往里扔。我的经验是:核心原则就一条——快速反馈。如果一个步骤超过 2 分钟,或者失败率高于 5%,就该考虑移出去,或者做成异步通知。
最后聊聊心态。
作为一个干了三年、正琢磨跳槽去搞 AI 的老咸鱼,我越来越觉得:工具链的稳定,比写多少炫酷代码更重要。你写的功能再牛,CI 跑不通,上线不了,等于零。而运营、测试、策划这些角色,其实都依赖我们搭的这套“高速公路”。
所以现在每次改 CI 配置,我都会问自己三个问题:
- 这个步骤失败了,开发者能快速定位吗?
- 它会不会拖慢整个流程?
- 如果半夜报警,我能 5 分钟内修好?
如果你也在被 CI 折磨,不妨试试这些建议。当然,最好的办法可能是——劝运营同学别用 JavaScript 写脚本(开玩笑的,他们用 Python 我更头疼)。
附:常见 CI 坑点速查表
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
command not found (如 npm) |
CI 环境缺少运行时 | 使用 Docker 容器或统一基础镜像 |
| 构建通过但线上异常 | 缺少环境变量或未验证产物 | 显式设置 NODE_ENV,增加冒烟测试 |
| 流水线时快时慢 | 依赖网络下载(如 npm install) | 使用缓存(如 npm cache, Docker layer cache) |
| 脚本行为本地 vs CI 不一致 | Shell 环境差异(如 PATH, 权限) | 所有命令合并到单个 sh 块,或使用容器 |
| 运营脚本报错没人看 | 错误日志淹没在大量输出中 | 单独拆分为子流水线,失败时 @ 相关人 |
写完这篇,我准备去投简历了。下家要是还在用 Jenkins 而不用 GitLab CI 或 GitHub Actions,我可能得再想想——毕竟,谁不想下班前 CI 能一次过呢?

评论 0