工程实践
从静态服务器到 CI/CD:GitHub Pages、Jekyll 与 GitHub Actions 完全指南
以「只服务静态文件」这一个约束为主线,串起 GitHub Pages 的托管原理、Jekyll 与前端框架的构建部署、GitHub Actions 自动化,直到 CI/CD 的完整图景。
「部署一个网页」听上去是件小事,但顺着这个问题往下追,会牵出比想象中多得多的东西。
本文用一条主线把这些串起来:GitHub Pages「只服务静态文件」这一个约束,如何推导出后面几乎所有的设计。 从静态服务器的本质,到单页应用为什么会在刷新时 404,再到 GitHub Actions 与 CI/CD 的完整概念——理解了这个约束,就理解了这套生态。
本文整理自一次围绕「GitHub 部署网页的原理是什么」的系统学习,重新组织为一篇完整指南。
一、GitHub Pages 的原理:一台自动售货机
1.1 网页部署的本质
先回到最原始的问题:网页到底是什么?
浏览器打开一个网页,本质上就是下载了一个文件(HTML),再照着上面的内容渲染出来。所以「部署网页」这件事,拆到最底层就是:
把文件放到一台永远开机的电脑上,别人访问网址时,那台电脑把文件发给他。
一个 HTTP 服务器最基础的职责,就是「URL 路径 → 返回文件字节」:请求 /index.html,它就把磁盘上那个文件读出来发回去。
GitHub Pages 做的正是这件事,只不过是给几千万个仓库批量托管的工业化版本。它的逻辑很朴素:反正代码本来就存在 GitHub 上,那就顺手把它们当网页发出去。
1.2 为什么必须是「静态」的
这是理解一切的钥匙。用一个比喻来说:
- 静态网站 = 自动售货机。所有商品提前放好,按 A3 就吐 A3,给谁都一样。
- 动态网站 = 现炒的餐馆。你点单,厨师现场做,每个人拿到的可能不同——比如电商首页,每个人看到的都不一样,因为服务器现场查了各自的数据。
GitHub Pages 只当售货机,不当厨师。它只把提前放好的文件原样吐出来,不跑后端代码、不连数据库、不记住用户是谁。
原因很直接:GitHub 上有几千万个仓库,如果每个请求都要现场执行仓库里的任意代码,那是安全与成本的双重灾难(想想恶意代码会发生什么)。所以规则被定死:只发文件。
这个约束直接推导出一个重要结论:任何「动态」的行为,要么发生在构建时(提前把文件生成好),要么发生在浏览器里(JS 在客户端跑)。服务器这一层永远是「静」的。
1.3 一次访问的完整链路
在浏览器地址栏输入 username.github.io/reponame 并回车之后,发生了这些事:
第一步:DNS 解析。 浏览器向 DNS 询问「这个域名对应哪个 IP」,返回的并不是 GitHub 机房的地址,而是 Fastly CDN 离你最近的边缘节点。
第二步:CDN 接手。 请求打到边缘节点,节点先查自己的缓存:
- 命中缓存 → 直接返回,GitHub 本体根本不知道你来过。这正是 Pages 又快、又能免费扛大流量的原因——绝大多数请求在边缘就消化掉了。
- 未命中 → 回源到 GitHub 的 Pages 服务层取一份,自己存一份,再发给你。下一个访客就走缓存了。
第三步:GitHub 如何定位你的站点。 同一个服务集群托管着几千万个站点,它靠两个信息路由:
- HTTP 请求头里的 Host(
username.github.io)→ 定位到账号; - URL 路径第一段(
/reponame)→ 定位到具体仓库的产物。
第四步:在静态产物里找文件。 定位到站点后,剩下的就是纯粹的目录查找:吐文件,或吐 404。
1.4 URL 与文件的映射规则
这是最容易被忽视、却最基础的规则:URL 的路径就是文件夹路径,一一对应。
静态产物/
├── index.html → /reponame/
├── about.html → /reponame/about.html
├── guide/
│ └── index.html → /reponame/guide/
├── blog/
│ ├── index.html → /reponame/blog/
│ └── first-post.html → /reponame/blog/first-post.html
└── assets/
└── logo.png → /reponame/assets/logo.png
文件夹叫什么名字完全随意,想要什么路由就建什么文件夹。真正带有「魔法」的只有两个文件名约定:
index.html:当 URL 指向一个文件夹(如/guide/)时,服务器约定俗成地找该文件夹下的index.html作为默认门面。这是互联网早期传下来的规矩。名字必须是index.html——放main.html也能访问,但 URL 得写全/guide/main.html。404.html:放在根目录,是找不到文件时的兜底页面。
两条规则可以归纳为:
- URL 精确到文件 → 精确找那个文件;
- URL 只到文件夹 → 自动找该文件夹下的
index.html,没有则 404。
顺带一提,VitePress、Hugo 这类工具能生成「好看的网址」(/guide/ 而非 /guide.html),手法就是把每个页面都生成为 某名字/index.html:纯粹利用上面第 1 条约定,没有任何额外的路由魔法。
1.5 部署流水线全景
push 代码
↓
构建(Jekyll 或 GitHub Actions)→ 静态文件快照
↓
打包成 artifact,上传到 Pages 存储层,通知 CDN 刷新缓存
用户访问网址
↓
DNS → 最近的 Fastly 节点
↓
命中缓存?直接返回:回源取文件并缓存
↓
浏览器拿到 HTML,渲染
一个关键细节:服务层与 Git 仓库是解耦的。真正被服务的是构建产物的快照,而不是仓库本身。这也解释了为什么 push 之后要等一两分钟才生效——构建、上传、缓存失效都需要时间。
1.6 实际操作:三步开站
GitHub 不会因为仓库里放了 index.html 就自动建站(否则所有人的仓库都会被公开成网页),需要一次性打开开关:
- 仓库根目录放好
index.html,push; - 进入仓库的 Settings → Pages,Source 选择分支(如
main)和文件夹(/ (root)),保存; - 等一两分钟,访问
username.github.io/reponame。
几点注意:
- 开关一个仓库只需开一次,之后随便 push 都会自动重新发布;
- 免费账号的仓库必须是 public 才能开启 Pages;
- 每个账号有一个特殊仓库
username.github.io,它部署在根域名;其他任意仓库都可以开 Pages,挂在子路径下——也就是说,有多少个仓库就能挂多少个站点; - 改了内容页面却没变,多半是浏览器缓存,强制刷新(Cmd/Ctrl + Shift + R)即可。
1.7 自定义域名的原理
想用自己的域名(如 example.com):
- 在域名的 DNS 里配好解析:
www子域用 CNAME 指向username.github.io;裸域(example.com)则用 A 记录指向 GitHub 提供的四个固定 IP(185.199.108.153等),或改用支持 ALIAS / ANAME 的 DNS 服务商——DNS 规范不允许在裸域上配 CNAME。本质都是告诉全世界「找这个名字请转向 GitHub」; - 在仓库 Settings → Pages 里填上域名,GitHub 记下「这个域名属于这个仓库」;
- 请求进来时,Host 头是
example.com,GitHub 查表就知道该吐哪个仓库的文件; - HTTPS 证书由 GitHub 自动向 Let's Encrypt 申请,免费且自动续期。
二、Jekyll:GitHub Pages 的「亲儿子」构建器
2.1 Jekyll 解决什么问题
纯手写 HTML 建站有一个明显痛点:重复。每个页面都要复制一遍导航栏、页脚、<head>;写一篇博客要手动包一层 HTML 骨架;文章列表页要手动维护每一条链接。
Jekyll 是一个静态站点生成器(Static Site Generator, SSG),思路是:
只写内容(Markdown)和模板(布局),构建时由程序把两者「压」成完整的 HTML 文件。
关键词是「构建时」:Jekyll 产出的依然是纯静态文件,完美契合售货机模型。动态的部分(模板渲染、文章列表生成)全部发生在构建那一刻,而不是用户访问时。
2.2 为什么说它是「亲儿子」
Jekyll 的特殊地位在于:GitHub Pages 内置了 Jekyll 构建。把 Jekyll 项目的源码(Markdown + 模板)直接 push 上去,不需要任何额外配置,GitHub 会自动在后台跑 Jekyll,把编译好的 HTML 发布出来。
这有历史渊源:Jekyll 的作者 Tom Preston-Werner 正是 GitHub 的联合创始人。GitHub Pages 最初就是围绕 Jekyll 设计的,这也是「经典 GitHub Pages 工作流」的由来。
三种托管模式对比如下:
| 模式 | 你 push 的内容 | GitHub 做的事 |
|---|---|---|
| 直接托管 | 现成的 HTML | 原样服务,零构建 |
| Jekyll | Markdown + 模板源码 | 自动跑 Jekyll 编译,再服务产物 |
| GitHub Actions | 任意框架源码 | 跑你自定义的构建流程 |
2.3 Jekyll 项目的结构
一个典型的 Jekyll 站点长这样:
my-blog/
├── _config.yml # 全站配置(站名、主题、插件等)
├── _layouts/ # 页面模板
│ ├── default.html # 基础骨架(导航、页脚)
│ └── post.html # 文章页模板
├── _includes/ # 可复用的片段(如 header.html)
├── _posts/ # 博客文章,文件名必须是 YYYY-MM-DD-标题.md
│ ├── 2026-07-01-hello-world.md
│ └── 2026-07-07-github-pages.md
├── index.html # 首页(通常包含文章列表)
└── assets/ # 图片、CSS 等静态资源
几个核心概念:
Front Matter(头信息):每个 Markdown 文件顶部用两行 --- 包起来的 YAML 块,声明这个页面的元数据。
---
layout: post
title: "GitHub Pages 部署原理"
date: 2026-07-07
tags: [github, devops]
---
正文从这里开始,直接写 Markdown……
Layout(布局):模板文件,用 {{ content }} 占位符表示「正文内容插在这里」。文章通过 Front Matter 里的 layout: post 声明自己用哪个模板。模板还可以继承——post.html 本身也能声明 layout: default,层层套用。
Liquid 模板语言:模板里可以写简单的逻辑,比如首页遍历所有文章生成列表:
<ul>
{% for post in site.posts %}
<li>
<a href="{{ post.url }}">{{ post.title }}</a>
<span>{{ post.date | date: "%Y-%m-%d" }}</span>
</li>
{% endfor %}
</ul>
构建时,Jekyll 把 _posts/ 里的每篇 Markdown 渲染成 HTML、套上模板、按日期生成 URL(如 /2026/07/07/github-pages.html),再把首页的文章列表循环展开成真实的链接,最终输出一个纯静态的 _site/ 文件夹。这就是要上货架的商品。
2.4 快速上手
本地开发(需要 Ruby 环境):
gem install bundler jekyll
jekyll new my-blog
cd my-blog
bundle exec jekyll serve # 本地预览,http://localhost:4000
部署到 GitHub Pages:直接把整个项目(源码,不是 _site/)push 到仓库,Settings → Pages 里选好分支即可。从分支部署时,Pages 默认就会用 Jekyll 处理内容(有没有 _config.yml 都会跑;放一个 .nojekyll 文件才会显式关掉)。
也可以更省事:GitHub 提供了一批官方主题,在 _config.yml 里写一行 theme: minima 就能换主题,连模板都不必自己写。
2.5 Jekyll 的局限与现状
经典 Jekyll 流程有几个限制:
- 插件白名单:GitHub 内置的 Jekyll 构建出于安全考虑只允许一小部分官方插件,第三方插件用不了;
- 版本较旧:内置的 Jekyll 版本更新缓慢;
- Ruby 生态:对前端开发者来说,本地装 Ruby 环境是额外负担;
- 构建黑盒:构建在 GitHub 后台发生,出错时调试不便。
破解这些限制的方式,恰好是下一章的主角:GitHub Actions。用 Actions 自定义构建流程后,你可以用任意版本的 Jekyll、任意插件,或者干脆不用 Jekyll,换成 Hugo、VitePress、Astro、Next.js——只要最终产出静态文件即可。
这也是为什么如今新项目大多直接走 Actions 路线:Jekyll 内置构建是「方便但受限的默认选项」,Actions 则是「完全自由的现代方案」。
三、现代前端框架(Vue / React)的部署
3.1 浏览器不认识 Vue / React
.vue 文件、JSX,浏览器根本看不懂。浏览器从始至终只认三样东西:HTML、CSS、JS。
所以框架项目必须经过一道编译工序(npm run build),把组件「压」成浏览器认识的文件:
你写的: build 之后(dist/):
src/App.vue index.html
src/components/Nav.vue → assets/index-a3f8b2.js
src/router/index.js assets/index-c9d1e4.css
main.js
关键点:上货架的是 dist,不是源码。 售货机需要的是压好的成品。
这里和 Jekyll 的逻辑是同构的:都是「构建时把动态的东西压成静态文件」。区别只在于,Jekyll 压的是 Markdown + 模板,前端框架压的是组件。
3.2 坑一:白屏(资源路径问题)
Vite / Webpack 默认认为站点部署在域名根路径 /,生成的 HTML 里引用资源写作 <script src="/assets/index-xxx.js">。
但站点实际部署在 username.github.io/仓库名/ 这个子路径下,浏览器按 /assets/... 去根路径找,找不到,JS 加载失败,白屏。
解法只需一行配置(以 Vite 为例,vite.config.js):
export default {
base: '/你的仓库名/',
}
3.3 坑二:刷新 404(前端路由问题)
这个坑值得理解原理。Vue Router / React Router 的「路由」其实是假的——从首页点到 /about,页面切换了,但浏览器根本没发请求,是 JS 在浏览器里偷偷换了内容、改了地址栏。服务器全程不知情。
问题来了:在 /about 页面按 F5 刷新,浏览器这次是真的向服务器请求 /about。服务器按映射规则去找 about/index.html——而产物里只有一个 index.html,没有这个文件,于是 404。
标准解法:把 index.html 复制一份,改名叫 404.html。这样任何找不到的路径,服务器吐出的「404 页面」其实就是应用本身;JS 加载后一看地址栏是 /about,自己把对应页面渲染出来。
这个方案第一眼看上去很别扭,但它别扭在观感,不别扭在效果:用户完全无感知,唯一的实际代价是返回状态码是 404 而非 200,对 SEO 不友好。而采用纯前端路由的项目(管理后台、工具应用、登录后的 dashboard)本来就不需要 SEO——这个代价在它的适用场景里恰好不痛,这正是它能成为标准做法的原因。
替代方案:路由改用 hash 模式,网址形如 /#/about。# 后面的部分服务器根本看不到,天然没有这个问题,代价是网址略丑。
3.4 该不该在 GitHub Pages 上用框架
被 404 补丁劝退之前,先分清三种情况——这个坑不是「框架」的锅,而是 SPA 这一种用法的锅:
- SPA 模式(history 路由):只生成一个
index.html,路由全靠 JS。踩坑的是它。 - 静态生成模式(SSG):VitePress、Astro、Next.js static export、Nuxt generate。同样用 Vue / React 写,但构建时每个路由都真的生成一个
文件夹/index.html。每个路径都有真实文件,刷新自然不会 404,零补丁。GitHub Pages 上的文档站、博客、官网大多这么做,与售货机模型完美契合。 - hash 模式:没有坑,只是网址略丑。
结论:GitHub Pages 天生适合内容型站点(SSG,体验完美),勉强适合 SPA(需要打一个补丁)。如果是重交互 SPA 且不愿接受补丁,更好的选择是 Vercel / Netlify / Cloudflare Pages——它们允许配置重写规则「所有路径都返回 index.html 且状态码 200」,同样免费。
GitHub Pages 是一台不提供任何配置项的极简售货机。这份固执让它能免费服务几千万个站点,也让大家只能利用 404 兜底这唯一的口子来打补丁。
四、GitHub Actions:GitHub 借你的一台云端电脑
4.1 一句话定位
GitHub Actions 的本质:GitHub 借你一台一次性的云端电脑,在特定事件发生时,自动跑你写好的脚本。
三个要素:
- 事件(when):什么时候跑?push、PR、定时、手动触发……
- 机器(where):GitHub 临时开一台干净的虚拟机(Ubuntu / macOS / Windows 任选);
- 脚本(what):在机器上按顺序执行命令,跑完机器销毁。
部署只是 what 的一种。跑测试、代码检查、定时任务、自动发版、给 issue 打标签——都是同一套机制。
4.2 一个真实文件,逐行拆解
Workflow 就是仓库里 .github/workflows/ 文件夹下的 YAML 文件,GitHub 看到这个位置的文件就自动生效。以最典型的「push 后自动测试」为例:
name: CI # 名字,显示在 Actions 页面
on: # ① 事件:什么时候触发
push:
branches: [main] # push 到 main 时
pull_request: # 有人提 PR 时也跑
jobs: # ② 任务清单
test: # 一个 job,名字叫 test
runs-on: ubuntu-latest # ③ 要一台 Ubuntu 虚拟机
steps: # ④ 按顺序执行:
- uses: actions/checkout@v4 # 把仓库代码下载到机器上
- uses: actions/setup-node@v4 # 装 Node.js
with:
node-version: 20
- run: npm ci # 装依赖
- run: npm test # 跑测试
概念层级是:workflow(整个文件)→ job(一台机器上的一组活)→ step(一条条命令)。
step 有两种写法,也是新手最容易懵的点:
run::直接执行 shell 命令,和在终端里敲一样;uses::调用别人封装好的「动作」(action)。actions/checkout@v4本身就是一个 GitHub 仓库,功能是「把当前 repo clone 下来」。它之于 workflow,相当于 npm 包之于 JS——GitHub Marketplace 上有几万个现成 action 可用。
4.3 核心心智模型:机器是一次性的
每次 workflow 触发,GitHub 都开一台全新的、空白的虚拟机。这解释了很多初看奇怪的事:
- 为什么每次都要
checkout?——新机器上什么都没有,连代码都没有; - 为什么每次都要装 Node、装依赖?——同理,裸机;
- 为什么跑完不用清理?——机器直接销毁。
换来的巨大好处是:环境绝对干净、绝对可复现。「在我电脑上能跑」这类玄学问题,在 Actions 上不存在——每次都从同一张白纸开始。
运行过程和结果在仓库的 Actions 标签页查看,每一步的终端输出完整记录,红了点进去就能看到哪一步报错。
4.4 常用进阶概念
Secrets(密钥):API key、密码不能明文写在 YAML 里。在 Settings → Secrets 里存好,workflow 里用 ${{ secrets.MY_API_KEY }} 引用,日志中会自动打码。
job 依赖:多个 job 默认并行(各开一台机器)。用 needs: test 声明依赖,即「test 成功了我再跑」。典型用法是:测试通过才允许部署。
更多触发器:
on:
schedule:
- cron: '0 0 * * *' # 每天定时(爬虫、日报)
workflow_dispatch: # 网页上手动点按钮触发
免费额度:public 仓库随便跑;private 仓库每月有 2000 分钟免费额度。注意这个额度按 Linux 分钟折算:跑一分钟 macOS 记 10 分钟、Windows 记 2 分钟,私库上用多平台 matrix 很容易烧掉配额。
4.5 用 Actions 部署 Pages(完整示例)
name: Deploy to Pages
on:
push:
branches: [main]
permissions: # 授权 workflow 写 Pages
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build # 产出 dist/
- uses: actions/upload-pages-artifact@v3
with:
path: dist # 把 dist 打包成 artifact
deploy:
needs: build # build 成功才跑
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@v4 # 把 artifact 发布到 Pages
id: deployment
翻译成人话:push 到 main → 开机器 → 下代码 → 装环境 → build → 把 dist 打包上传 → 另一个 job 把包发布到 Pages 服务层。全程一两分钟,你只管 push。
(使用这种方式时,Settings → Pages 的 Source 要选「GitHub Actions」。)
4.6 Matrix:一份配置,多台机器并行
runs-on 不止能要 Ubuntu,还能要 macOS 和 Windows 的真机。配合 matrix,可以让同一份 steps 在多个系统上并行跑:
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
matrix 本质是个「填空批量生成器」:给 os 三个取值,它就把 job 复制三份并行执行。还能多维叠加:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
3 × 3 = 9 台机器同时开跑,九个组合全覆盖。
想想没有它的时代:手头一台 Mac,要测 Windows 得找台 Windows 机器或装虚拟机,测 Linux 再来一套,改一行代码三边重来。以前的开源作者要么真攒三台机器,要么在 README 里写「Windows 未测试,欢迎反馈」。现在这只是四行 YAML——复杂度并没有消失,而是被 GitHub 用真金白银的机器池扛掉了,你付出的只是声明「我要哪几种机器」。
4.7 自动发版:发一个版本的成本 = 打一个 tag
成熟开源项目的 Releases 页面,每个版本下挂着一排各平台的下载包,基本没有一个是作者手动上传的。这全靠三块积木:
积木 1:tag 也是触发事件。
on:
push:
tags: ['v*'] # 凡是 v 开头的 tag 被 push,就跑
日常 push 代码不触发,只有郑重打了 v1.2.0 这样的 tag 才发版。tag 从一个单纯的标记,变成了发版的扳机。
积木 2:artifact 是 job 之间的传送带。 机器是一次性的,三台 matrix 机器构建完就销毁,产物先用 upload-artifact 存到 GitHub 临时仓库,后面的 job 再 download-artifact 取出来。
积木 3:现成的 release action。 softprops/action-gh-release,给它文件,它自动创建 Release 并挂上去。
拼起来:
name: Release
on:
push:
tags: ['v*']
permissions:
contents: write # 允许 workflow 创建 release
jobs:
build:
strategy:
matrix:
include:
- os: macos-latest
name: myapp-macos-arm64
- os: ubuntu-latest
name: myapp-linux-x64
- os: windows-latest
name: myapp-windows-x64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build:binary # 换成你实际的打包命令
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.name }}
path: dist/
release:
needs: build # 三台机器全成功才跑
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4 # 三份产物全部下载
- uses: softprops/action-gh-release@v2
with:
files: '**/*' # 全部挂到 release
generate_release_notes: true # 自动生成更新日志
一个彩蛋:generate_release_notes: true 会根据这个 tag 到上个 tag 之间的 PR 和 commit 自动生成更新日志——连 changelog 都不用手写。
于是发版体验变成了这样:
git tag v1.2.0
git push --tags
敲完这两行去倒杯水,回来时 Releases 页面上就多了个 v1.2.0,各平台的包挂好了,更新日志也写好了。
这套东西的意义超出了「省事」:它真正改变的是发版意愿。手动发版麻烦时,人会攒着改动「等攒够了再发」,用户等得久,反馈回路慢;发版变成零成本后,修个小 bug 都可以随手发一个 v1.2.1。可见的产出频率,往往比憋大招更重要。
五、CI / CD:给这一切一个学名
5.1 CI = Continuous Integration,持续集成
「集成」指的是:把多个人的代码合并到一起。
历史背景是这样的:早期的团队,每人各自开发几周甚至几个月,最后来一场「集成周」把代码合到一起——然后就是灾难现场,冲突满天飞,痛苦到有一个专门的黑话叫 integration hell(集成地狱)。
CI 的思想是:与其攒大招憋出内伤,不如高频小步合并——每人每天把代码合进主干,配合机器人每次合并都自动跑构建 + 测试,坏了立刻报警。问题在只有一天增量时暴露,五分钟就能修完;攒三个月再暴露,就是地狱。
「push 后自动跑测试」这条流水线,术语上就是 CI。Actions 是工具,CI 是这个工具在执行的实践。
5.2 CD 的两个全称
CI 保证了「主干代码随时是好的」,下一个问题是:代码是好的,多久发布一次给用户?CD 有两个含义,区别就在这里:
- Continuous Delivery(持续交付):每次合并后自动完成构建、测试、打包,产物随时处于可发布状态,但发不发、何时发,由人按一下按钮决定;
- Continuous Deployment(持续部署):连按钮都去掉。测试全绿?直接自动上线,全程无人。
一句话记忆:Delivery 是「随时能发」,Deployment 是「直接就发」。 差别在于发布前有没有一个人为闸门。
日常口语里的 CI/CD,CD 一般指前者,因为敢玩全自动 Deployment 需要极强的测试覆盖兜底,是少数派。
5.3 对号入座
本文讲过的所有流水线,恰好把这几个概念全占了:
| 流水线 | 概念 |
|---|---|
| push 后自动测试 / 多平台构建验证 | CI |
| 打 tag 才触发自动发 release | Continuous Delivery(「打 tag」就是那个人为闸门) |
| push 到 main 即自动上线的 Pages 部署 | Continuous Deployment(中间没有任何人为环节) |
所以「CI/CD = 流水线自动化部署」这个常见理解,方向对,但糊掉了层次:CI 管合并质量(合进来的代码是不是好的),CD 管发布节奏(好的代码多快到用户手上)。流水线(pipeline)只是承载这两件事的载具;Actions、Jenkins、GitLab CI,都是造这个载具的不同牌子。
工程文化里有一句关于 CD 的核心信条:发布应该是无聊的(release should be boring)。当发版从一场仪式变成打个 tag 的日常动作,频率自然就上去了。
六、结语:一条主线串起所有东西
回顾全文,所有知识点其实都挂在同一条因果链上:
- GitHub Pages 是纯静态文件服务器(售货机)——因为要免费、安全地服务几千万个站点,只能只发文件、不跑代码;
- 于是一切动态行为被挤到两端:构建时(Jekyll / 前端框架的 build / SSG)或浏览器里(SPA 的 JS 路由);
- SPA 把动态挤到浏览器里,就撞上了服务器「按目录找文件」的规则 → 刷新 404,只能用
404.html兜底这唯一的口子打补丁; - 构建这件事需要一台机器来跑 → GitHub Actions,一台事件驱动的一次性云端电脑;
- 这台电脑不止能构建部署,还能测试、多平台验证(matrix)、自动发版(tag 触发 + artifact + release action);
- 这些实践的学名,就是 CI(管合并质量)与 CD(管发布节奏)。
从「部署一个网页」这个朴素的问题出发,走到这里会发现:现代前端工程与 DevOps 的一大块版图,其实都是围绕「静态」这一个约束条件生长出来的。理解了约束,就理解了生态。