Skip to content

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.jsonindex.html
服务管理PM2 重启Nginx 重载
包管理器pnpmnpm

目录结构

.github/
└── workflows/
    ├── ci.yaml              # PR 自动化检查
    └── release-bump.yaml    # 构建 + 部署 + 通知

完整配置文件

1. ci.yaml(PR 自动化检查)

.github/workflows/ci.yaml
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:build

2. release-bump.yaml(构建 + 部署 + 通知)

.github/workflows/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_TOKENGitHub Personal Access Token
GMAIL_SMTP_PASSWORDGmail SMTP 应用专用密码
DEPLOY_HOST服务器 IP 或域名
DEPLOY_USERSSH 登录用户名
DEPLOY_SSH_KEYSSH 私钥内容

Variables(非敏感配置)

Variable 名称说明示例
GMAIL_SMTP_USERGmail 邮箱地址your-email@gmail.com
NOTIFY_EMAILS通知邮箱列表admin@gmail.com,team@gmail.com
DEPLOY_WEBSITE_DOMAIN网站访问地址https://docs.example.com
DEPLOY_PORTSSH 端口(可选)22

关键差异详解

1. 构建输出路径

项目路径
Nuxt.js.output/
VitePressdocs/.vitepress/dist/
bash
# VitePress 打包命令
npm run docs:build

# 打包后验证
if [ ! -d "docs/.vitepress/dist" ]; then
  echo "Error: ./docs/.vitepress/dist/ directory not found!"
  exit 1
fi

2. 验证文件

项目验证文件说明
Nuxt.jsnitro.jsonNuxt 构建产物标志
VitePressindex.html静态站点入口文件
bash
# VitePress 验证逻辑
if [ ! -f "index.html" ]; then
  echo "ERROR: index.html not found after extraction!"
  exit 1
fi

3. 服务管理方式

项目服务管理说明
Nuxt.jspm2 restartNode.js 服务进程管理
VitePressnginx -s reload静态文件服务重载
bash
# VitePress 部署后
nginx -s reload
systemctl status nginx

4. 包管理器

项目包管理器安装命令
Nuxt.jspnpmpnpm install --frozen-lockfile
VitePressnpmnpm ci

💡 npm cinpm 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)

  1. PR 创建/更新 → 触发 workflow
  2. 检出代码 → 安装依赖 → 构建验证
  3. 确保 PR 不会破坏文档构建

Release 流程(release-bump.yaml)

  1. 创建 Release → 触发 workflow
  2. 检出代码 → 安装依赖 → 构建 VitePress
  3. 打包 docs/.vitepress/dist/ 内容
  4. 上传到 GitHub Release
  5. 上传 artifact
  6. 发送构建通知邮件
  7. 下载 artifact → 解压 → 验证 index.html
  8. SCP 传输到服务器
  9. 备份当前版本 → 部署新文件
  10. Nginx 重载
  11. 发送部署通知邮件(含访问地址)

常见问题

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/fishfinalVitePress 构建产物存放目录
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.gzzip 附件,服务器目录应更新为最新版本,Nginx 服务状态正常,且邮箱收到通知。