很多刚入行的同学一听到 CI/CD 就头大,觉得这玩意儿是不是得买个 Jenkins 服务器,或者搞一堆复杂的配置。简单来说,GitHub Actions 就是 GitHub 官方给你提供的一个免费的、跟仓库深度绑定的自动化工具。你不用自己维护服务器,不用操心环境,只要你的代码在 GitHub 上,就能用。
CI/CD 其实是两件事:CI (Continuous Integration,持续集成) 和 CD (Continuous Deployment,持续部署)。
git pull 然后 npm install 的操作,现在全交给机器去做。要在 GitHub 里玩转这个,得先搞懂几个关键词,这就像玩积木一样,得知道每块积木叫啥:
push 代码的时候?还是提 pull_request 的时候?或者是每天凌晨 3 点定时跑?这个触发条件就是 Event。checkout 代码,第二步是 setup node,第三步是 npm install。Step 是按顺序执行的。值得留意的是,啊,GitHub Actions 的工作原理其实特别简单。
当你仓库里发生了你定义的 Event(比如你 git push 了代码),GitHub 就会去你的仓库里找一个特定的文件夹:.github/workflows/。
在这个文件夹里,通常会有 .yml 或者 .yaml 结尾的文件。GitHub 会读取这些文件,解析里面的内容,然后去调度 Runner 来执行。
其实,你只要在这个文件夹里写个配置文件,就等于告诉 GitHub:“嘿,有人动我代码的时候,去那个 Ubuntu 机器上帮我跑一下 npm test 呗。”
新手最容易踩的坑就是权限问题。从 2022 年 10 月开始,GitHub 默认给 Actions 的 GITHUB_TOKEN 权限是 read-only(只读)。如果你在 Workflow 里需要往 Release 里上传文件,或者往 Packages 里推镜像,记得在 YAML 文件里手动开启写权限,或者去仓库 Settings -> Actions -> General 里把 Workflow permissions 改成 Read and write permissions。不然你跑半天,最后因为没权限报错,真的会心态爆炸。
光说不练假把式。咱们现在就动手写一个最基础的 Workflow 文件。别怕,YAML 语法看着怪,其实跟写配置文件差不多,层级关系用空格缩进,别用 Tab 键就行。
在你的项目根目录下,新建路径 .github/workflows/first-test.yml。
注意,文件名随便起,但必须放在 .github/workflows/ 目录下,且后缀是 .yml。
咱们先写一个最简单的:只要有人往 main 分支 push 代码,就在 Ubuntu 系统上打印一句 "Hello World"。
咱们来拆一下这个文件,保证你看懂每一行:
name: First Test Workflow:这是工作流的名称,会显示在 GitHub 仓库的 Actions 标签页里。on: push: branches: [ "main" ]:这是事件。意思是,监听 push 事件,并且只有目标分支是 main 才触发。jobs::下面定义具体的任务。greet::这是 Job 的 ID,你可以随便起名,比如 build, test。runs-on: ubuntu-latest:指定 Runner。这里用的是 GitHub 托管的 ubuntu-latest(写这篇文章时是 Ubuntu 22.04)。你也可以选 windows-latest 或者 macos-latest。steps::具体的执行步骤。- name: ...:给这一步起个名字,方便你看日志。run: ...:在 Runner 的终端里直接执行的命令。把这段代码 commit 并 push 到你的 GitHub 仓库。
然后打开你的 GitHub 仓库页面,点击顶部的 Actions 标签页。你会看到刚才那个 Workflow 正在运行(黄色圆圈)或者已经跑完(绿色对勾)。点进去,点 greet 这个 Job,你就能看到 Say Hello 那一步打印出了 "Hello World from GitHub Actions!"。
千万注意缩进! YAML 对缩进极其敏感。如果你发现 Workflow 不触发,或者报错 "We couldn't find a workflow with this name",90% 是因为你的缩进或者语法有问题。建议在 VS Code 里装一个 YAML 插件(比如 Red Hat 的那个),它能实时校验语法。另外,如果你是在 Windows 上写代码,记得把换行符设置成 LF (\n) 而不是 CRLF (\r\n),虽然 GitHub 通常会处理,但有时候 Windows 的换行符会导致脚本执行奇怪的错误。
这一章咱们来点硬核的。假设你有个 Node.js 项目,以前你部署可能是本地 docker build,然后 docker push。现在咱们让 GitHub Actions 帮你全自动搞定。
场景:当你打了一个 v* 开头的 Tag(比如 v1.0.0)时,自动构建 Docker 镜像并推送到 GitHub Container Registry (GHCR)。
Dockerfile。先看看咱们假设的 Dockerfile 长啥样(这是一个标准的多阶段构建,体积很小):
咱们新建一个文件 .github/workflows/docker-publish.yml。
这个流程稍微复杂点,包含了登录、构建、推送。
这里有几个新面孔,我来帮你解释一下:
on: push: tags::这次咱们不用 branches 了,改用 tags。只有当你执行 git tag v1.0.0 并 push 这个 tag 时,它才会跑。env:定义了环境变量。REGISTRY 是 ghcr.io(GitHub 的镜像仓库地址),IMAGE_NAME 直接用仓库名(比如 username/repo)。permissions:, 关键点这里必须手动声明 packages: write。因为推送镜像属于写操作,默认是没权限的。uses: ...:这就不是 run 了。uses 表示使用别人的 Action(也就是一段封装好的脚本)。- actions/checkout@v4:必选,把代码下载下来。
- docker/setup-buildx-action@v3:设置 Docker 环境。
- docker/login-action@v3:登录镜像仓库。这里用到了 secrets.GITHUB_TOKEN。这个 Token 是 GitHub 自动生成的,你不用自己填,直接用就行。
- docker/metadata-action@v5:这个很智能,它会自动根据 Tag 生成 Docker 镜像的标签。比如你打了 v1.0.0,它会自动生成 ghcr.io/you/repo:1.0.0 这样的标签。
- docker/build-push-action@v5:核心步骤,构建并推送。注意 cache-from 和 cache-to 用了 type=gha,这是 GitHub Actions 的缓存机制,能让你下次构建快得飞起,不用每次都重新下载 npm 包。
在本地执行:
去 GitHub Actions 看,跑完之后,去你仓库主页的 Packages 部分,就能看到推送上去的镜像了。
关于 Dockerfile 的路径问题。很多新手在 docker/build-push-action 里避雷经验,是因为 context 设置不对。如果你的 Dockerfile 就在项目根目录,那 context: . 就行。如果你把 Dockerfile 放在了 docker/Dockerfile,那你得把 file 参数加上:file: ./docker/Dockerfile。还有一个坑,如果你的 Node.js 项目里有 .dockerignore 文件,一定要确保 node_modules 被忽略(虽然多阶段构建里其实不需要忽略,但忽略掉能减小构建上下文的大小,加快构建速度),同时千万不要把 .dockerignore 把 package.json 或者你的源代码给忽略掉了,不然构建出来的镜像里啥都没有。
玩转了基础的 CI/CD 流程,咱们得往深了挖一挖。可以这么理解,真实项目里哪有只往一个地方扔代码的?你肯定得区分开发环境(Dev)、测试环境(Staging)和生产环境(Prod)。这一节咱们聊聊怎么优雅地搞定这些事儿,还有怎么让你的流水线跑得飞快。
很多新手容易犯的一个错误,就是写三个几乎一模一样的 Job 去分别部署三个环境。这太蠢了,维护起来想死。咱们要用 GitHub Actions 的 strategy.matrix 或者利用 environment 功能来做。
我比较推荐的做法是结合 GitHub Environments。你在仓库的 Settings -> Environments 里建两个环境,比如 production 和 staging。你可以给环境设置保护规则,比如“生产环境部署必须经过我审批”。
看这个例子,我用了矩阵策略来跑测试,然后用环境隔离来做部署:
📖 学习建议:千万别把生产环境的 IP、密码直接写在 YAML 文件里,那是找死。一定要用 secrets,而且配合 GitHub Environments 使用,这样你可以限制只有 main 分支能访问 production 环境的 Secret,安全性直接拉满。
刚才提到了 Secrets,这里多啰嗦两句。GitHub 的 Secrets 其实挺好用,但有个坑,它不支持直接存文件(比如你的私钥 id_rsa)。
如果你需要部署时用到 SSH 私钥,你得把私钥的内容复制粘贴进去,然后在 workflow 里把它还原成文件。
如果你发现你的 CI 跑一次要 10 分钟,其中 8 分钟都在 npm install 或者 pip install,那你绝对该优化缓存了。简单来说,依赖包又不会每次都变,咱们得学会“偷懒”。
GitHub Actions 提供了 @actions/cache 这个 Action,现在 setup-node 和 setup-python 这些官方 Action 都内置了缓存选项,用起来巨方便。
如果你用的是比较复杂的场景,比如缓存 node_modules 文件夹,可以这么写:
⚡ 效率提示:缓存虽好,可不要贪杯。有时候你会发现缓存的依赖有问题,导致部署失败。这时候别慌,去 GitHub Actions 的界面,有个 "Delete caches" 的按钮,清掉重来就好。另外,npm ci 比 npm install 更适合 CI 环境,因为它严格按照 package-lock.json 安装,速度更快且更严谨。
---
搞 CI/CD 最让人头大的不是写配置,而是看那个红色的 ❌ 报错。有时候本地跑得好好的,一上 Actions 就崩。这一节咱们聊聊几个我踩过无数次的坑,以及怎么填。
这是最常见的坑之一。特别是当你用 GitHub 自带的 GITHUB_TOKEN 去操作仓库时。
比如,你想在 CI 里自动打个 Tag,或者自动创建一个 Release,然后 push 回仓库。你可能会发现报错 403 Permission denied。
原因是默认的 GITHUB_TOKEN 权限是受限的。虽然 GitHub 在 2023 年左右更新了默认权限策略,但有些操作还是需要你显式声明。
看个实战例子,我在 CI 里自动生成版本号并打 Tag:
🔧 实战技巧:如果你发现 push 失败,第一反应先看 permissions。另外,如果你用的是 Personal Access Token (PAT),记得检查那个 Token 有没有 repo 权限。
有时候你会看到 Error: The operation was canceled. 或者 Timeout。
这通常是两种情况:
npm install 卡住了。这时候用上面说的缓存。对于第二种情况,尽量让你的部署脚本变成非交互式的。如果是 SSH 连接超时,可以调整 SSH 的配置,或者在 workflow 里加个超时限制,避免无限等待:
依赖安装失败简直是家常便饭。特别是前端项目,经常遇到 node-gyp 报错,或者 node-sass 这种需要编译的包。
实际案例实录:有一次我遇到 npm ci 失败,报错说 package-lock.json 和 node_modules 不匹配。后来发现是本地 Node 版本是 18,CI 里默认是 16,导致 lock 文件生成的版本不一样。
解决办法很简单,强制统一 Node 版本:
如果是 Python 项目,经常遇到 pip install 失败,可能是因为缺少系统依赖。比如 psycopg2 需要 libpq-dev。这时候你得在 Actions 里先装系统包:
⚡ 效率提示:遇到依赖报错,别光看最后一行。点开 Actions 的日志,往上翻,看 npm 或者 pip 的具体报错。很多时候是网络问题(比如连不上 GitHub 或者 PyPI),这时候可以考虑用国内的镜像源,或者给 Actions 配置代理(如果你有的话)。另外,定期更新你的 actions/checkout@v4 这种 Action 的版本号,老版本可能不兼容新的 Runner 环境。
---
写了这么多,咱们来收个尾。其实 CI/CD 没那么玄乎,“把重复的手工活交给机器”。但要把这事儿做漂亮,还是得讲究点章法。这一节我总结了一些我这几年摸爬滚打出来的经验,还有我觉得值得收藏的资源。
deploy.yml 放在一个公共仓库,然后其他仓库调用它。比如公共仓库里的 deploy-template.yml:
`yaml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
SSH_KEY:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- run: echo "部署到 ${{ inputs.environment }}"
`
然后在你的项目里直接调用:
`yaml
jobs:
call-deploy:
uses: your-org/shared-actions/.github/workflows/deploy-template.yml@main
with:
environment: 'production'
secrets:
SSH_KEY: ${{ secrets.MY_SSH_KEY }}
`
lint 一个 Job,test 一个 Job,build 一个 Job。这样哪个环节挂了一目了然,而且还能并行跑,节省时间。actions/checkout@v4 的参数:默认它只拉取最新的一次提交。如果你需要打 Tag 或者做版本比对,一定要加 fetch-depth: 0,不然你会发现自己拿不到完整的 Git 历史。GitHub Actions 的生态非常丰富,很多轮子不用你自己造。
docker、ssh、slack 等关键词,能找到大把现成的 Action。比如 appleboy/ssh-action 就是专门用来做 SSH 部署的神器,比你自己写 Shell 脚本稳得多。用 appleboy/ssh-action 的例子(这玩意儿真的巨好用,强烈推荐):
`yaml
name: SSH Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: executing remote ssh commands
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/my-app
git pull origin main
npm install
pm2 restart app
`
awesome-actions,有一个社区维护的列表,收录了各种牛逼的 Action,从发送邮件到操作 AWS,应有尽有。actions/checkout@v3 这种依赖有没有安全更新,并自动提 PR 帮你升级到 v4。这能省去你很多维护的心智负担。🔧 实战技巧:刚开始写 CI/CD,别追求大而全。先搞定“代码推上去能自动跑测试”,再搞定“能自动部署到测试环境”,最后再搞“多环境、审批流、回滚机制”。一步步来,别一上来就想搞个完美的流水线,那样容易因为太复杂而放弃。记住,能跑起来的自动化,比写在文档里的最佳实践更有用。