171 lines
6.9 KiB
Markdown
171 lines
6.9 KiB
Markdown
---
|
||
tags:
|
||
- CI/CD
|
||
- Gitea Actions
|
||
- Slidev
|
||
create time: 2026-05-27 19:30
|
||
---
|
||
|
||
# Workflow 配置详解
|
||
|
||
## 概述
|
||
|
||
本文档对 `CICD 在 Slidev 中的应用` 中的 Workflow YAML 进行逐块解析,涵盖从触发条件到构建部署的每一步配置逻辑。
|
||
|
||
> [!NOTE] 如需了解部署架构和容器通信模式,详见 [[deployment-strategy]]。
|
||
|
||
## 正文
|
||
|
||
### 1. 触发条件
|
||
|
||
```yaml
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
pull_request:
|
||
branches: [main]
|
||
```
|
||
|
||
向 `main` 分支推送代码或提交 PR 时都会触发。这意味着开发者通过特性分支提 PR,CI 会先验证构建是否通过——只有合并到主干后才执行部署。
|
||
|
||
> [!TIP] PR 事件只适合跑验证性步骤(build、lint),实际部署仍保留在 `push: main` 中,因为 PR 上下文通常没有写入权限。如果 Gitea Actions 的 PR 事件无法读取 secret,可改用 `pull_request_target`,但需避免直接使用 PR 代码中的敏感操作以防止 token 泄露。
|
||
|
||
> [!QUESTION] 思考:如果你的 Slidev 项目需要同时支持 staging 和 production 环境,你会如何设计触发规则?
|
||
|
||
### 2. 运行环境与容器
|
||
|
||
```yaml
|
||
jobs:
|
||
build-and-deploy:
|
||
runs-on: aliyun
|
||
container: node:22-alpine
|
||
```
|
||
|
||
- **`runs-on: aliyun`** — 使用了自定义的 Gitea runner,运行在一台阿里云机器上(而非官方 GitHub-hosted runner)。
|
||
- **`container: node:22-alpine`** — 在 Alpine Linux 上的 Node.js 22 容器中执行,体积小、启动快,适合纯前端构建场景。
|
||
|
||
### 3. 安装系统工具
|
||
|
||
```yaml
|
||
- name: Install tools
|
||
run: |
|
||
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||
apk add --no-cache docker-cli git
|
||
```
|
||
|
||
Alpine 默认的软件源在海外,先用 `sed` 替换为阿里云镜像源加速下载。然后安装三个必要的工具:
|
||
|
||
| 工具 | 用途 |
|
||
|------|------|
|
||
| `docker-cli` | 指挥宿主机 Docker daemon,管理其他容器的生命周期 |
|
||
| `git` | 版本控制操作所需 |
|
||
| `apk` | Alpine 的包管理器(已内置) |
|
||
|
||
> [!NOTE] 这里只安装了 `docker-cli`(客户端),没有安装完整的 `docker`。完整的 Docker 包包含 daemon 本身(约 80MB),而我们需要的是轻量级 CLI(约 50MB on Alpine)——因为 daemon 已经在宿主机上运行了。
|
||
|
||
### 4. 检出代码 (Checkout)
|
||
|
||
```yaml
|
||
- name: Checkout
|
||
uses: http://47.121.181.112:3000/wonder/checkout@v4
|
||
with:
|
||
github-server-url: http://47.121.181.112:3000
|
||
```
|
||
|
||
使用私有 Gitea 实例的 checkout action,指向地址 `http://47.121.181.112:3000`。通过 `github-server-url` 参数告知 action 正确的 Gitea 服务器地址。
|
||
|
||
#### Checkout 到底做了什么?
|
||
|
||
很多开发者误以为 workflow 启动后工作目录里已经有代码了——实际上 runner 初始环境是**空的**。Checkout action 会在幕后执行一系列 git 操作:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["Runner 启动<br/>空工作目录"] --> B["git clone 仓库"]
|
||
B --> C["git fetch 指定 commit 的引用"]
|
||
C --> D["git reset --hard 精确 SHA"]
|
||
D --> E[".git 隐藏目录<br/>pnpm install 可用"]
|
||
```
|
||
|
||
| 步骤 | Git 命令等价物 | 作用 |
|
||
|------|---------------|------|
|
||
| Clone | `git clone <repo-url>` | 拉取全量历史到 action 工作区 |
|
||
| Fetch | `git fetch origin <commit-sha>` | 获取触发 workflow 的具体 commit 引用 |
|
||
| Reset | `git reset --hard <commit-sha>` | 将工作树精确还原到目标提交(而非默认分支最新) |
|
||
|
||
> [!TIP] PR 场景下尤其要注意:如果 workflow 被 pull_request 事件触发,Checkout 会自动检出 **PR 合并后的结果**(即 `refs/pull/<number>/merge`),而不是 PR 分支本身。这确保了 CI 验证的是"合并后能否构建",而非"分支本身能否构建"。
|
||
|
||
#### 常用配置参数
|
||
|
||
实际项目中经常需要调整 checkout 的行为,以下是高频参数及适用场景:
|
||
|
||
| 参数 | 默认值 | 用途 |
|
||
|------|--------|------|
|
||
| `ref` | 触发 workflow 的 commit SHA | 指定检出的分支、标签或 commit |
|
||
| `clean` | `true` | 检出前是否清理工作目录残留文件 |
|
||
| `persist-credentials` | `true` | 是否在后续步骤中保持 git 认证信息 |
|
||
| `fetch-depth` | `1` | 只拉取最近一次提交(fast clone) |
|
||
|
||
> [!QUESTION] 什么时候应该设置 `fetch-depth: 0`?
|
||
>
|
||
> 答案:当你需要完整的 git 历史时——比如用 `git log` 生成 changelog、执行语义化版本计算 (`conventional-changelog`),或者需要 `git blame` 追溯代码来源。`fetch-depth: 1` 是默认值,因为它更快更省空间;只有确实需要历史记录时才改为 `0`。
|
||
|
||
> [!WARNING] 安全注意:CI 中的 checkout 会拿到完整的 repo token。如果 workflow 包含不可信的第三方 action(如开源社区提供的 step),建议设置 `persist-credentials: false`,防止 token 被泄露给非官方代码。
|
||
|
||
|
||
### 5. 安装依赖与构建
|
||
|
||
```yaml
|
||
- name: Install pnpm
|
||
run: npm install -g pnpm
|
||
|
||
- name: Install dependencies
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
- name: Build all decks
|
||
run: pnpm run build
|
||
```
|
||
|
||
三步完成从包管理到构建的过程:
|
||
|
||
1. **全局安装 pnpm** — 容器初始状态只有 Node.js,需要先装包管理器
|
||
2. **安装项目依赖** — `--frozen-lockfile` 确保依赖版本精确锁定(详见下方说明)
|
||
3. **执行构建** — `pnpm run build` 对应 `slidev build`,将所有 `.md` deck 编译为静态站点
|
||
|
||
> [!TIP] Slidev 的 `build` 命令输出目录默认为 `dist/`,每个 slide deck 一个子目录。这就是为什么最后一步可以直接 `cp dist/. nginx:/usr/share/nginx/html/`。
|
||
|
||
#### --frozen-lockfile 的 CI 哲学
|
||
|
||
```bash
|
||
# 不加参数:lockfile 被悄悄更新 → 下次构建可能用不同版本
|
||
pnpm install
|
||
|
||
# 加 --frozen-lockfile:严格一致,否则立即失败
|
||
pnpm install --frozen-lockfile
|
||
```
|
||
|
||
| 场景 | 不加 `--frozen-lockfile` | 加上 `--frozen-lockfile` |
|
||
|------|------------------------|-------------------------|
|
||
| lockfile 正常 | ✅ 安装 | ✅ 安装 |
|
||
| lockfile 缺失 | 🔧 自动生成一个新的 | ❌ **报错退出** |
|
||
| lockfile 与 package.json 不匹配 | 🔧 悄悄更新 lockfile | ❌ **报错退出** |
|
||
|
||
CI 环境追求的是 **构建可复现性 (Build Reproducibility)**。同一个 commit,不同的人、不同的时间、不同的机器上构建出来的结果必须完全一致——否则线上 bug 将无法定位是"代码问题"还是"环境差异"。
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["开发者修改 package.json"] --> B{"push"}
|
||
B --> C["CI: frozen-lockfile 检查"]
|
||
C -->|lockfile 已同步| D["✅ 构建通过"]
|
||
C -->|lockfile 未同步| E["❌ 立即失败<br/>提示手动运行 pnpm install"]
|
||
|
||
style E fill:#ffe8e8
|
||
style D fill:#e8ffe8
|
||
```
|
||
|
||
> [!TIP] 正确的工作流:开发者改完依赖 → `pnpm install` 自动更新 lockfile → 同时提交两个文件。CI 只负责验证一致性,不负责修锁。
|
||
|
||
## 关联笔记
|
||
|
||
- [[CICD 在 Slidev 中的应用]]
|
||
- [[deployment-strategy]]
|