GitHub CI/CD 全流程
代码合进 main,GitHub Actions 亮了绿灯,打开网站却还是旧页面。这种情况很常见:代码已经到了 GitHub,测试也过了,但还没人把新版本放到服务器上。
这篇文章用一个 Go 后端、两个 H5 前端的例子,把从提交代码到网站更新的过程走一遍。项目放在一个仓库还是多个仓库,区别主要在工作流怎么分工;CI/CD 要解决的问题是一样的。
简版:先看完整流程
text
推送代码到发布分支
↓
GitHub Actions 跑测试、检查能否构建
↓
后端把镜像推到 GHCR(GitHub 的镜像仓库);前端打包出网页文件
↓
服务器拿到这次提交对应的版本,更新服务
↓
检查新版本是否能访问;失败则尝试恢复旧版本
这里有三个动作,很容易被混在一起:
| 动作 | 做完以后发生了什么 |
|---|---|
git push |
GitHub 保存了源码,服务器不会因此自动更新 |
| CI(持续集成) | 自动运行测试和构建,发现这次改动有没有明显问题 |
| CD(持续部署) | 把通过检查的版本部署到服务器,并检查部署结果 |
所以,Actions 显示绿色,不一定表示网站已经更新。要看工作流最后有没有执行部署,以及部署后的检查是否通过。
"推送后实时部署"也不是零秒上线。它的意思是推送触发流水线,不用等服务器定时拉代码;测试、打包和更新服务器仍然需要几分钟。
严格说,CD 既可能指"持续交付",也可能指"持续部署"。前者把版本准备好,生产上线可以保留人工确认;后者在满足条件后自动上线。下文说的是后者。
详版:一次提交是怎么走到线上的?
推了代码,服务器为什么不会自己更新?
GitHub 上保存的是源码。服务器实际运行的通常是 Go 程序、Docker 镜像,或者前端打包后的 HTML、JS、CSS。源码要先经过测试和构建,才能变成这些东西。
当然,也可以让服务器收到通知后执行 git pull、再在服务器上构建。但这样做仍要回答几个问题:它拉的是哪个提交?这个提交通过测试了吗?构建失败怎么处理?旧版本还能不能恢复?
因此,比较清楚的分工是:GitHub Actions 检查并准备好一个明确的版本,服务器只负责运行这个版本。
GitHub Actions 怎样决定什么时候运行?
GitHub Actions 的配置文件放在仓库的 .github/workflows/ 目录。最常见的安排是:有人提交 PR(请求合并代码)时先跑检查;代码进入发布分支后,再跑检查、上传镜像并部署。
以 Go 后端为例,下面的工作流展示了前半段。仓库里需要有 go.mod 和 Dockerfile,镜像地址要换成自己的:
yaml
name: API CI
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- run: go build ./...
- run: go vet ./...
- run: go test ./...
- run: docker build -t api-check:${{ github.sha }} .
publish:
needs: check
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/example/api:${{ github.sha }}
看这段配置只需要抓住两点。needs: check 表示检查通过后才继续;if: github.event_name == 'push' 表示 PR 只检查,不会推送正式镜像。仓库的发布分支若是 dev,就把触发条件里的 main 换成 dev。
配置里的 check 和 publish 都是 job(作业,也就是工作流中的一组执行步骤)。publish 只负责把镜像推到 GHCR。**镜像传上去了,服务器还没有更新。**要让用户用上新版本,还得接着部署。
测试的范围也别想得太满。比如这里运行了 go test ./...,但如果数据库相关测试需要额外的测试库,而工作流没准备它,绿灯并不能说明数据库操作已经测过。绿色只代表"配置过、且实际运行的检查通过了"。
为什么要给镜像和网页文件标记版本?
假设上午提交了 A,下午又提交了 B。A 的测试刚通过,如果服务器此时只拉 latest,拿到的可能已经是 B。出了问题,再问"线上究竟是哪次提交",就很难说清。
所以,镜像和前端文件都应该能对应到一次 Git 提交。常见做法是用提交 SHA(这次提交的唯一编号)给镜像打标签,再把前端文件放进以 SHA 命名的目录:
text
后端镜像:ghcr.io/example/api:<提交 SHA>
前端文件:releases/<提交 SHA>/
这里还有一层细节:**SHA 标签能帮助你找到源码,但镜像标签本身仍可以被覆盖。**需要精确锁定镜像内容时,部署前应记下镜像摘要(digest,用于标识镜像内容的哈希值,例如 sha256:...),服务器按摘要拉取。前端也一样,不要到了部署阶段又从不断变化的分支重新构建一次。
前端构建完成后,可以直接把 dist 传到服务器,也可以先上传为 GitHub Actions Artifact(这次工作流保存下来的一份文件),再由部署步骤下载。在这个例子里,Artifact 存网页文件,GHCR 存容器镜像;两者都只是"准备好版本",不是"已经上线"。
如果 Artifact 只保留 7 天,网站会在第 8 天消失吗?不会。到期后不能再从 GitHub 下载那份文件,但已经放在服务器上的网页仍在那里。真正要考虑的是:服务器还留着几个旧版本,能否用它们回退。
服务器需要先准备到什么程度?
自动部署之前,网站至少要能手动上线一次。服务器需要运行环境、域名和 DNS、HTTPS、入口代理,以及数据库等依赖。数据库密码、JWT 密钥这类配置放在服务器或密钥管理服务里,不放进 Git 仓库,也不能打进前端 JS。
以一台服务器同时放后端、用户端和管理端为例,请求大致这样走:
text
浏览器 ──HTTPS──> Nginx / Caddy
├── 用户端网页
├── 管理端网页
└── API 请求 ──> Go 服务 ──> MySQL / Redis
两个前端可以用不同子域名,入口代理按域名把请求送到各自的网页目录。后端 API 可以使用单独域名,也可以由前端站点的 /api/ 路径转发。选哪种方式都要核对前端实际请求的地址;跨域方案还要配置 CORS(浏览器的跨域访问规则)。MySQL 和 Redis 通常只开放给应用所在的内部网络,不直接暴露到公网。
如果服务器原本已经运行了博客,80/443 端口可能已被占用。这时应让几个站点共用一个入口代理,由域名区分请求,而不是为新项目再启动一个抢占相同端口的代理。
第一次可以手动拉取一个确定版本的后端镜像,把两份前端文件分别放到对应站点,确认页面、API 和 HTTPS 都正常。手动上线能跑通以后,再把其中重复的动作交给 Actions。否则工作流报错时,连故障在服务器还是在自动化配置都很难分清。
GitHub Actions 怎么让服务器更新?
最直观的方法是 SSH(远程登录服务器的一种方式)。后端镜像推到 GHCR 后,Actions 连接服务器,把这次要部署的镜像版本传过去;服务器拉取镜像、更新服务、检查 API。前端构建完成后,Actions 把 dist 上传到新目录,再让服务器切换对应站点。
这一步不是随手加一条 ssh 命令就结束了。服务器需要一个专用部署账号;GitHub 仓库要安全保存 SSH 私钥,并预先核对服务器的主机公钥。只让通过检查的发布流程使用这些凭据。若后端镜像是私有的,服务器还需要读取 GHCR 镜像的权限;Actions 用来上传镜像的 GITHUB_TOKEN 不会自动传给服务器。
也可以在服务器上做一个接收部署请求的服务,让 Actions 带上 GitHub OIDC 身份令牌(用于证明请求来自哪个工作流的短期凭据)通知它。服务器验证令牌确实来自指定仓库和分支,再自行拉取镜像或前端文件。这能避免把服务器 SSH 私钥放进仓库,但要自己处理身份验证、重复请求和失败重试,工作量比 SSH 大得多。
若仓库不是自己管理的,还要留意权限:能提交工作流文件,不代表能设置仓库 Secrets 或调整分支保护。多仓库实时部署不要求服务器定时轮询,但每个仓库都得能运行自己的工作流,并配好访问服务器所需的权限。
三个仓库如何各管各的?
仍以一个 Go 后端、两个 H5 站点为例:
| 仓库 | 推送发布分支后做什么 | 更新服务器上的什么 |
|---|---|---|
| 后端 | 检查代码,构建并推送镜像 | API 容器 |
| 用户端 | 安装依赖,测试并构建 H5 | 用户端网页目录 |
| 管理端 | 安装依赖,测试并构建 H5 | 管理端网页目录 |
三个仓库不一定要使用同名分支。后端从 main 发布、两个前端从 dev 发布也可以,前提是工作流写的分支和团队真正用于上线的分支一致。用户端推送后只更新用户端;管理端推送后只更新管理端,不应该顺手重启另外两个服务。
更新完以后,怎么知道真的好了?
后端通常先拉取新镜像,再更新容器,最后从实际访问入口请求健康接口。还应检查一两条核心业务路径:健康接口返回 200,只能说明服务基本活着,不代表登录、上传或查数据一定正常。
前端可以先把新文件放进独立目录,确认 index.html 和静态资源齐全,再让网站指向新目录。部署后实际请求页面和静态资源;失败时再切回旧目录。不要一边把文件逐个覆盖到正在服务的目录,一边让用户访问。
浏览器缓存也会影响上线效果。常见做法是让带内容哈希的 JS/CSS 长时间缓存,而 index.html 每次重新验证。旧页面可能引用旧资源,所以切到新目录之后,要保证过渡期内旧资源的 URL 仍能访问。保留旧目录本身不够:如果代理只提供新目录,旧资源还是可能返回 404。
自动回退也有边界。新版本若改了数据库结构,旧应用可能已经不认识新表。应用切回去,不代表数据也回到了旧状态。发布数据库变更前,应考虑新旧版本兼容,做好备份并演练恢复。连续推送时还要防止旧任务跑得慢、最后反而把新版本盖掉;服务器更新时需要加锁,或在切换前确认该提交仍是当前准备发布的版本。
怎样确认自己做到了全自动部署?
不用只盯着 Actions 的绿灯。可以实际做几次演练:故意让测试失败,看服务器是否维持原版本;分别推送后端和两个前端,看每次是否只更新对应服务;模拟新版本启动失败,看旧版本能否恢复;最后从线上版本反查到具体提交。
把这几件事都验证过,再检查证书续期、数据库备份、磁盘空间和告警,自动部署才算真的接上了。GitHub CI/CD 最有用的地方,不是少敲几次命令,而是上线出了问题时,你能说清楚哪次提交上线了、它通过了哪些检查、怎样退回上一个版本。