GitHub Actions 实战:从构建、测试到自动部署的完整流水线
GitHub Actions 实战:从构建、测试到自动部署的完整流水线
很多人对 GitHub Actions 的印象还停留在「加个 CI 跑跑测试」,但真正让它值钱的是从 push 代码到部署上线一条龙自动跑完。这篇不讲概念,直接带你搭一条能用于生产的流水线:代码推上去 → 装依赖 → 跑测试 → 构建镜像 → 推到镜像仓库 → SSH 到服务器滚动更新。中间会踩的坑(缓存不生效、secret 泄露、部署一半挂了没人知道)一个个填掉。
先看一个「能跑但很烂」的 workflow
新手常写出这样的东西,放在 .github/workflows/deploy.yml:
name: deploy
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test
- run: npm run build
- run: scp -r dist/ user@server:/var/www/ # 密码硬编码在哪?
问题一堆:
on: push会在任何分支、任何提交都触发,改个 README 也跑一遍部署。- 每次都
npm install全量装,没缓存,一次三五分钟。 scp部署没有任何鉴权配置,真要写密码只能塞明文,等于把服务器密码公开。- 测试挂了会不会继续部署?这里靠 step 顺序碰运气,没有明确的 job 依赖。
下面一步步改成正确的样子。
一、精确控制触发条件
只在 main 分支、且改动了代码时才触发,并且手动也能点一下跑:
on:
push:
branches: [main]
paths-ignore:
- '**.md' # 只改文档不触发部署
- 'docs/**'
workflow_dispatch: # 允许在 Actions 页面手动触发,方便回滚重跑
workflow_dispatch 强烈建议加上——部署流水线经常需要「不改代码但重新跑一次」,比如上次部署中途网络抖了。
二、把 CI 和 CD 拆成两个 job,用依赖串起来
关键点:测试不过,绝不部署。用 needs 建立依赖,而不是把所有步骤堆在一个 job 里靠顺序。
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # 关键:自动缓存 ~/.npm,下次装依赖秒级
- run: npm ci # ci 比 install 快且严格,锁定 lockfile
- run: npm test
deploy:
needs: test # test 这个 job 成功后才会跑 deploy
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # 双保险,只有 main 才部署
steps:
- uses: actions/checkout@v4
# ... 见下文
这里有两个容易忽略的点:
cache: 'npm'是setup-node内置的缓存能力,不用再手写actions/cache。它按package-lock.json的 hash 做 key,lockfile 没变就直接命中。npm ci而不是npm install:CI 环境要的是可复现,ci会严格按 lockfile 安装,lockfile 和package.json对不上直接报错,能提前发现依赖漂移。
三、构建 Docker 镜像并推送
假设我们用容器部署。这里用官方的 docker/build-push-action,并且给镜像打上 commit sha 作为 tag(方便精确回滚):
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: 登录镜像仓库
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} # 内置 token,推到 ghcr 够用
- name: 构建并推送
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha # 复用上次构建的 layer 缓存
cache-to: type=gha,mode=max
cache-from/cache-to: type=gha 用的是 GitHub Actions 自带的 layer 缓存,Docker 构建慢的项目开了它能省一大半时间。${{ github.sha }} 这个 tag 很重要:出了问题你可以直接用某个 sha 的镜像回滚,而不是只有一个飘忽不定的 latest。
四、SSH 到服务器滚动更新(secret 的正确姿势)
绝对不要把私钥、服务器 IP 写进 yaml。全部走 Repository Secrets(仓库 Settings → Secrets and variables → Actions 里配),在 workflow 里用 ${{ secrets.XXX }} 引用:
- name: 部署到服务器
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }} # 私钥内容,不是路径
script: |
set -e
docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
docker stop app || true
docker rm app || true
docker run -d --name app --restart=unless-stopped \
-p 8080:8080 \
ghcr.io/${{ github.repository }}:${{ github.sha }}
docker image prune -f # 清掉旧镜像,别把磁盘撑爆
几个救命细节:
set -e:脚本里任何一条命令失败就立刻退出并让这一步标红。不写它的话,docker pull失败了后面还会继续跑,最后「绿了」但服务其实没起来,这是最坑的假成功。key传的是私钥内容本身,把~/.ssh/id_ed25519的完整内容(含-----BEGIN...那几行)粘到 secret 里。对应的公钥要提前加到服务器的~/.ssh/authorized_keys。- secret 会在日志里被自动打码,即使你不小心 echo 出来,GitHub 也会显示成
***。但别依赖这个,不要主动打印 secret。
五、部署失败要有人知道
流水线最忌讳「悄悄挂了」。加一步失败通知,if: failure() 只在前面步骤失败时才执行:
- name: 部署失败通知
if: failure()
run: |
curl -X POST "${{ secrets.WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"🚨 部署失败: ${{ github.repository }} @ ${{ github.sha }}\"}"
if: failure() 是关键——默认情况下前面步骤挂了,后续步骤会被跳过,加上这个条件它反而只在失败时触发,正好用来报警。
六、并发控制:别让两次部署互相踩
连续 push 两次,可能触发两条部署同时跑,后推的镜像先部署完、先推的后覆盖,状态就乱了。用 concurrency 让新的取消旧的:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true # 同一分支有新部署时,取消正在跑的旧部署
放在 workflow 顶层(和 on、jobs 平级)。这样永远只有最新那次部署在跑,避免竞态。
小结
一条靠谱的部署流水线,核心就这几条:
- 触发要精确:限定分支 +
paths-ignore过滤无关改动 + 保留workflow_dispatch手动入口。 - CI/CD 分 job 并用
needs串联:测试不过绝不部署,依赖关系显式声明,不靠 step 顺序碰运气。 - 善用缓存:
setup-node的cache: npm和 Docker 的type=gha缓存,能把几分钟的流水线压到几十秒。 - secret 只进 Secrets,私钥传内容不传路径,脚本首行
set -e防假成功。 - 失败要报警(
if: failure())、并发要控制(concurrency)。
一句话记忆点:流水线的价值不在「能跑」,而在「挂了拦得住、失败叫得醒、回滚有得选」。
更多推荐



所有评论(0)