Appearance
VitePress 项目 GitHub Actions 自动化 CI/CD 实践
前言
在完成了 Nuxt.js 项目 GitHub Actions 自动化 CI/CD 实践 系列文章后,我收到了一个很自然的问题:VitePress 文档项目能否也用类似的方式实现自动化?
答案是肯定的。本文就是将这套 CI/CD 思想应用到 VitePress 文档项目的完整实践。
为什么值得单独写一篇?
虽然核心思想相通,但 VitePress 与 Nuxt.js 在以下方面存在差异:
- 构建输出目录不同
- 验证文件不同
- 服务管理方式不同
- 包管理器不同
这些差异点正是本文要重点说明的内容。
本文目标:
当你创建一个 Release 时,自动完成:
- 代码检出 → 依赖安装 → 项目构建
- 生成压缩包并上传到 Release 页面
- 自动部署到服务器
- Nginx 重载服务
- 邮件通知(成功/失败)
NOTICE
本文假设你已经阅读过 Nuxt.js 系列的 CI/CD 实践文章,重点讲解差异点。如果你对基础概念不熟悉,建议先阅读相关文档。
核心差异速览
| 对比项 | Nuxt.js 项目 | VitePress 项目 |
|---|---|---|
| 构建输出目录 | .output/ | docs/.vitepress/dist/ |
| 验证文件 | nitro.json | index.html |
| 服务管理 | PM2 重启 | Nginx 重载 |
| 包管理器 | pnpm | npm |
目录结构
.github/
└── workflows/
├── ci.yaml # PR 自动化检查
└── release-bump.yaml # 构建 + 部署 + 通知完整配置文件
1. ci.yaml(PR 自动化检查)
yaml
name: ci
on:
push:
branches: [ master, docs/* ]
pull_request:
branches: [ master ]
jobs:
check-links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build VitePress
env:
NODE_OPTIONS: "--max-old-space-size=4096"
run: npm run docs:build2. release-bump.yaml(构建 + 部署 + 通知)
yaml
name: Auto Bump Version on Release
on:
release:
types: [created] # 仅在创建新 Release 时触发
jobs:
# Job 1: 构建和发布
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.extract-version.outputs.version }}
project_name: ${{ steps.project-name.outputs.project_name }}
steps:
# 1. 检出代码
- uses: actions/checkout@v6
with:
ref: ${{ github.event.release.target_commitish }}
fetch-depth: 0
# 2. 设置 Node.js 环境
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 20
cache: npm
# 3. 安装依赖(推荐 ci 而非 install)
- name: Install dependencies
run: npm ci
# 4. 提取版本号
- name: Extract Version
id: extract-version
run: |
TAG=${GITHUB_REF#refs/tags/}
VERSION=$(echo "$TAG" | sed 's/^v//')
echo "version=$VERSION" >> $GITHUB_OUTPUT
# 5. 提取项目名称(从 package.json 动态获取)
- name: Extract Project Name
id: project-name
run: |
PROJECT_NAME=$(node -p "require('./package.json').name")
echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT
# 6. 更新 package.json 版本号
- name: Update Version
run: |
npm version ${{ steps.extract-version.outputs.version }} --no-git-tag-version
cat package.json | grep version
# 7. 构建 VitePress 项目
- name: Build VitePress
env:
NODE_OPTIONS: "--max-old-space-size=4096"
run: npm run docs:build
# 8. 创建压缩包(使用动态项目名称)
- name: Create Distribution Archive
run: |
if [ ! -d "docs/.vitepress/dist" ]; then
echo "Error: ./docs/.vitepress/dist/ directory not found!"
exit 1
fi
echo "Contents of ./docs/.vitepress/dist/ directory:"
ls -la ./docs/.vitepress/dist/
PROJECT_NAME="${{ steps.project-name.outputs.project_name }}"
VERSION="${{ steps.extract-version.outputs.version }}"
tar -czf ${PROJECT_NAME}-${VERSION}.tar.gz -C ./docs/.vitepress/dist/ .
cd ./docs/.vitepress/dist && zip -r ../../../${PROJECT_NAME}-${VERSION}.zip . && cd ../../../
echo "Archive created successfully!"
ls -la *.tar.gz *.zip
# 9. 上传到 GitHub Release
- name: Upload Release Assets
uses: ncipollo/release-action@v1
with:
artifacts: "./${{ steps.project-name.outputs.project_name }}-${{ steps.extract-version.outputs.version }}.tar.gz,./${{ steps.project-name.outputs.project_name }}-${{ steps.extract-version.outputs.version }}.zip"
allowUpdates: true
omitBody: true
omitName: true
token: ${{ secrets.ACCESS_TOKEN }}
makeLatest: "true"
# 10. 上传构建产物供后续 Job 使用
- name: Upload Build Artifact
uses: actions/upload-artifact@v7
with:
name: build-output
path: ${{ steps.project-name.outputs.project_name }}-${{ steps.extract-version.outputs.version }}.tar.gz
retention-days: 1
if-no-files-found: error
overwrite: true
# 11. 提交版本号变更
- name: Commit Changes
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add package.json package-lock.json
git commit -m "chore(release): bump to ${{ steps.extract-version.outputs.version }} [skip ci]"
git push origin HEAD:${{ github.event.release.target_commitish }}
# 12. 构建成功邮件
- name: Send build success email
if: success()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 587
username: ${{ vars.GMAIL_SMTP_USER }}
password: ${{ secrets.GMAIL_SMTP_PASSWORD }}
subject: "✅ VitePress 构建成功 - ${{ github.repository }}"
to: ${{ vars.NOTIFY_EMAILS }}
from: "GitHub Actions <${{ vars.GMAIL_SMTP_USER }}>"
body: |
🎉 VitePress 项目构建成功!
仓库:${{ github.repository }}
版本:${{ steps.extract-version.outputs.version }}
触发者:${{ github.actor }}
时间:${{ github.event.repository.updated_at }}
状态:✅ 成功
详情:${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# 13. 构建失败邮件
- name: Send build failure email
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 587
username: ${{ vars.GMAIL_SMTP_USER }}
password: ${{ secrets.GMAIL_SMTP_PASSWORD }}
subject: "❌ VitePress 构建失败 - ${{ github.repository }}"
to: ${{ vars.NOTIFY_EMAILS }}
from: "GitHub Actions <${{ vars.GMAIL_SMTP_USER }}>"
body: |
🚨 VitePress 项目构建失败,请立即检查!
仓库:${{ github.repository }}
版本:${{ steps.extract-version.outputs.version }}
触发者:${{ github.actor }}
时间:${{ github.event.repository.updated_at }}
状态:❌ 失败
详情:${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
请点击上方链接查看详细错误信息。
# Job 2: 远程部署
remote-deploy:
runs-on: ubuntu-latest
needs: build-and-release
if: github.event_name == 'release' && success()
permissions:
actions: read
steps:
# 1. 下载构建产物
- name: Download Artifact
uses: actions/download-artifact@v8
with:
name: build-output
path: ./
# 2. 解压 artifact
- name: Extract Artifact
run: |
echo "Contents before extract:"
ls -la
echo ""
echo "Extracting tar.gz..."
tar -xzf ${{ needs.build-and-release.outputs.project_name }}-${{ needs.build-and-release.outputs.version }}.tar.gz
echo "Extracted contents:"
ls -la
echo "Total files: $(find . -maxdepth 1 -type f | wc -l)"
# 3. 验证提取的内容(VitePress 验证 index.html)
- name: Verify Extracted Content
run: |
if [ ! -f "index.html" ]; then
echo "ERROR: index.html not found after extraction!"
exit 1
fi
echo "✅ Artifact extracted successfully"
# 4. SCP 传输到服务器
- name: Deploy to Server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: ${{ secrets.DEPLOY_PORT || 22 }}
source: "./"
target: "/tmp/${{ needs.build-and-release.outputs.project_name }}-build-${{ needs.build-and-release.outputs.version }}/"
# 5. SSH 执行远程部署
- name: Execute Remote Deployment
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: ${{ secrets.DEPLOY_PORT || 22 }}
script: |
set -e
PROJECT_NAME="${{ needs.build-and-release.outputs.project_name }}"
VERSION="${{ needs.build-and-release.outputs.version }}"
DEPLOY_DIR="/var/www/${PROJECT_NAME}"
BUILD_DIR="/tmp/${PROJECT_NAME}-build-${VERSION}"
echo "=========================================="
echo "Deploying ${PROJECT_NAME} version ${VERSION}"
echo "=========================================="
# 1. 备份当前版本
if [ -d "$DEPLOY_DIR" ]; then
BACKUP_DIR="/var/www/${PROJECT_NAME}-backup-$(date +%Y%m%d_%H%M%S)"
echo "📦 Creating backup: ${BACKUP_DIR}"
cp -r "$DEPLOY_DIR" "$BACKUP_DIR"
ls -t /var/www/${PROJECT_NAME}-backup-* 2>/dev/null | tail -n +6 | xargs rm -rf || true
else
echo "📁 Creating deployment directory: ${DEPLOY_DIR}"
mkdir -p "$DEPLOY_DIR"
fi
# 2. 部署新文件
echo "📤 Deploying files..."
rm -rf ${DEPLOY_DIR}/*
cp -r ${BUILD_DIR}/* ${DEPLOY_DIR}/
# 3. 设置权限
chown -R www-data:www-data ${DEPLOY_DIR}
chmod -R 755 ${DEPLOY_DIR}
# 4. 重载 Nginx 服务(静态站点)
echo "🔄 Reloading nginx service..."
nginx -s reload
# 5. 清理临时文件
echo "🧹 Cleaning up..."
rm -rf ${BUILD_DIR}
# 6. 显示部署结果
echo "=========================================="
echo "✅ Deployment completed successfully!"
echo "📍 Deployed to: ${DEPLOY_DIR}"
echo "📌 Version: ${VERSION}"
echo "=========================================="
systemctl status nginx
# 6. 输出部署概览
- name: Build Summary
run: |
echo "=========================================="
echo "🎉 DEPLOYMENT SUMMARY"
echo "=========================================="
echo "Project: ${{ needs.build-and-release.outputs.project_name }}"
echo "Version: ${{ needs.build-and-release.outputs.version }}"
echo "Status: ${{ job.status }}"
echo ""
echo "📊 Directory Statistics:"
echo " HTML files: $(find . -maxdepth 1 -type f -name "*.html" | wc -l) files"
echo " Total directories (root): $(find . -maxdepth 1 -type d | wc -l) directories"
echo ""
echo "🔍 Key Files Check:"
echo " favicon.svg: $([ -f "favicon.svg" ] && echo '✅ YES' || echo '❌ NO')"
echo " favicon.ico: $([ -f "favicon.ico" ] && echo '✅ YES' || echo '❌ NO')"
echo " index.html: $([ -f "index.html" ] && echo '✅ YES' || echo '❌ NO')"
echo " 404.html: $([ -f "404.html" ] && echo '✅ YES' || echo '❌ NO')"
echo " assets/: $([ -d "assets" ] && echo '✅ EXISTS' || echo '❌ NOT FOUND')"
echo ""
echo "⏱️ Time: $(date)"
echo "=========================================="
# 7. 部署成功邮件
- name: Send deploy success email
if: success()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 587
username: ${{ vars.GMAIL_SMTP_USER }}
password: ${{ secrets.GMAIL_SMTP_PASSWORD }}
subject: "✅ VitePress 部署成功 - ${{ github.repository }}"
to: ${{ vars.NOTIFY_EMAILS }}
from: "GitHub Actions <${{ vars.GMAIL_SMTP_USER }}>"
body: |
🎉 VitePress 项目部署成功!
仓库:${{ github.repository }}
版本:${{ needs.build-and-release.outputs.version }}
触发者:${{ github.actor }}
时间:${{ github.event.repository.updated_at }}
状态:✅ 成功
🌐 访问地址:${{ vars.DEPLOY_WEBSITE_DOMAIN }}
详情:${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# 8. 部署失败邮件
- name: Send deploy failure email
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 587
username: ${{ vars.GMAIL_SMTP_USER }}
password: ${{ secrets.GMAIL_SMTP_PASSWORD }}
subject: "❌ VitePress 部署失败 - ${{ github.repository }}"
to: ${{ vars.NOTIFY_EMAILS }}
from: "GitHub Actions <${{ vars.GMAIL_SMTP_USER }}>"
body: |
🚨 VitePress 项目部署失败,请立即检查!
仓库:${{ github.repository }}
版本:${{ needs.build-and-release.outputs.version }}
触发者:${{ github.actor }}
时间:${{ github.event.repository.updated_at }}
状态:❌ 失败
详情:${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
请点击上方链接查看详细错误信息。必需的 Secrets 和 Variables 配置
在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中添加:
Secrets(敏感信息)
| Secret 名称 | 说明 |
|---|---|
ACCESS_TOKEN | GitHub Personal Access Token |
GMAIL_SMTP_PASSWORD | Gmail SMTP 应用专用密码 |
DEPLOY_HOST | 服务器 IP 或域名 |
DEPLOY_USER | SSH 登录用户名 |
DEPLOY_SSH_KEY | SSH 私钥内容 |
Variables(非敏感配置)
| Variable 名称 | 说明 | 示例 |
|---|---|---|
GMAIL_SMTP_USER | Gmail 邮箱地址 | your-email@gmail.com |
NOTIFY_EMAILS | 通知邮箱列表 | admin@gmail.com,team@gmail.com |
DEPLOY_WEBSITE_DOMAIN | 网站访问地址 | https://docs.example.com |
DEPLOY_PORT | SSH 端口(可选) | 22 |
关键差异详解
1. 构建输出路径
| 项目 | 路径 |
|---|---|
| Nuxt.js | .output/ |
| VitePress | docs/.vitepress/dist/ |
bash
# VitePress 打包命令
npm run docs:build
# 打包后验证
if [ ! -d "docs/.vitepress/dist" ]; then
echo "Error: ./docs/.vitepress/dist/ directory not found!"
exit 1
fi2. 验证文件
| 项目 | 验证文件 | 说明 |
|---|---|---|
| Nuxt.js | nitro.json | Nuxt 构建产物标志 |
| VitePress | index.html | 静态站点入口文件 |
bash
# VitePress 验证逻辑
if [ ! -f "index.html" ]; then
echo "ERROR: index.html not found after extraction!"
exit 1
fi3. 服务管理方式
| 项目 | 服务管理 | 说明 |
|---|---|---|
| Nuxt.js | pm2 restart | Node.js 服务进程管理 |
| VitePress | nginx -s reload | 静态文件服务重载 |
bash
# VitePress 部署后
nginx -s reload
systemctl status nginx4. 包管理器
| 项目 | 包管理器 | 安装命令 |
|---|---|---|
| Nuxt.js | pnpm | pnpm install --frozen-lockfile |
| VitePress | npm | npm ci |
💡
npm ci比npm install更适合 CI 环境,它严格按照package-lock.json安装,速度更快且更稳定。
5. 压缩包创建路径
bash
# VitePress 打包(注意路径层级)
tar -czf ${PROJECT_NAME}-${VERSION}.tar.gz -C ./docs/.vitepress/dist/ .
cd ./docs/.vitepress/dist && zip -r ../../../${PROJECT_NAME}-${VERSION}.zip . && cd ../../../运行流程
PR 流程(ci.yaml)
- PR 创建/更新 → 触发 workflow
- 检出代码 → 安装依赖 → 构建验证
- 确保 PR 不会破坏文档构建
Release 流程(release-bump.yaml)
- 创建 Release → 触发 workflow
- 检出代码 → 安装依赖 → 构建 VitePress
- 打包
docs/.vitepress/dist/内容 - 上传到 GitHub Release
- 上传 artifact
- 发送构建通知邮件
- 下载 artifact → 解压 → 验证
index.html - SCP 传输到服务器
- 备份当前版本 → 部署新文件
- Nginx 重载
- 发送部署通知邮件(含访问地址)
常见问题
Q: 为什么验证文件从 nitro.json 变成了 index.html?
A: VitePress 构建输出是纯静态文件,入口文件是 index.html,而 Nuxt.js 输出的是 Nitro 服务器应用,标志文件是 nitro.json。
Q: 为什么服务管理从 PM2 变成了 Nginx?
A: VitePress 生成的是静态文件,不需要 Node.js 进程托管,只需 Nginx 提供静态文件服务即可。
Q: 为什么使用 npm ci 而不是 npm install?
A: npm ci 专为 CI 环境设计,严格遵循 package-lock.json,安装更快、更可靠。
Q: 如何配置 Nginx 来托管 VitePress 静态文件?
A: 参考以下配置(包含 HTTPS 支持和 SPA 路由处理):
nginx
# HTTPS 配置(443 端口)
server {
server_name fishfinal.com;
# 代理 VitePress 项目打包目录
location / {
root /var/www/fishfinal;
index index.html;
# 解决页面刷新后 404 问题(SPA 路由)
try_files $uri $uri/ @router;
}
# 页面刷新后路由重写
location @router {
rewrite ^.*$ /index.html last;
}
listen [::]:443 ssl;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/fishfinal.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/fishfinal.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
# HTTP 重定向到 HTTPS(80 端口)
server {
if ($host = fishfinal.com) {
return 301 https://$host$request_uri;
}
listen 80;
listen [::]:80;
server_name fishfinal.com;
return 404;
}关键配置说明:
| 配置项 | 说明 |
|---|---|
root /var/www/fishfinal | VitePress 构建产物存放目录 |
try_files $uri $uri/ @router | 先尝试匹配静态文件,失败则进入路由重写 |
location @router | 将未匹配的请求重写到 index.html |
rewrite ^.*$ /index.html last | 实现 SPA 前端路由,解决页面刷新 404 |
| SSL 证书配置 | Let's Encrypt 自动管理 |
💡 VitePress 是静态站点 + SPA 路由,必须配置
try_files+@router才能支持页面刷新后不 404。
总结
本文基于 Nuxt.js 系列的 CI/CD 思想,成功迁移到 VitePress 文档项目。主要调整包括:
- ✅ 构建输出路径:
docs/.vitepress/dist/ - ✅ 验证文件:
index.html - ✅ 服务管理:
nginx -s reload - ✅ 包管理器:
npm ci
核心思想不变,细节按需调整——这正是 CI/CD 自动化的精髓。
相关文档
本文思路源自以下系列文章,建议先行阅读以了解完整的 CI/CD 设计思想:
💡 确认点
运行此 workflow 后,Release 页面应有 tar.gz 和 zip 附件,服务器目录应更新为最新版本,Nginx 服务状态正常,且邮箱收到通知。