Skip to content

Nuxt.js 项目 GitHub Actions 自动化 CI/CD 实践(一):构建与发布

前言

在现代化的前端开发流程中,自动化 CI/CD 已成为标配。然而,很多开发者在手动完成以下操作时感到繁琐:

  • 每次发布都要手动执行构建、打包
  • 构建产物需要手动上传到 Release
  • 部署到服务器需要手动拷贝文件、重启服务
  • 无法感知 CI/CD 的执行状态

为了解决这些痛点,本系列文章将带你从零开始,使用 GitHub Actions 为 Nuxt.js 项目搭建一套完整的自动化 CI/CD 流水线。

本系列文章共分为三部分:

部分目标预期效果
第一部分(本文)实现创建 Release 时自动构建、打包并上传到 GitHub Release创建 Release 后,自动生成 tar.gz/zip 附件,并自动更新 package.json 版本号
第二部分实现自动部署到远程服务器构建产物自动传输到服务器,通过 PM2 重启服务,完成上线
第三部分实现 CI/CD 邮箱通知构建和部署成功后发送邮件通知,失败时及时告警

达成效果:

当你在 GitHub 上创建一个新的 Release 时,整个流程将全自动运行:

  1. 代码自动检出 → 依赖安装 → 代码检查 → 项目构建
  2. 生成压缩包并上传到 Release 页面
  3. 构建产物自动传输到服务器
  4. PM2 自动重启服务
  5. 最终你将收到一封包含部署结果的邮件

NOTICE

本文为系列文章的第一部分,聚焦于构建与发布环节。

第一部分目标

当你在 GitHub 上创建一个新的 Release 时,自动完成:

  • 代码检出与依赖安装
  • 代码检查(Lint + Typecheck)
  • Nuxt.js 项目构建
  • 生成压缩包(tar.gz + zip)
  • 上传到 GitHub Release
  • 保存构建产物供后续部署使用
  • 自动提交版本号更新

目录结构

.github/
└── workflows/
    └── 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  # 允许推送代码和创建 Release
    outputs:
      version: ${{ steps.extract-version.outputs.version }}
      project_name: ${{ steps.project-name.outputs.project_name }} # 传递给后续部署 Job

    steps:
      # 1. 检出代码
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.release.target_commitish }}
          fetch-depth: 0

      # 2. 安装 pnpm
      - name: Install pnpm
        uses: pnpm/action-setup@v4

      # 3. 设置 Node.js
      - name: Setup Node.js
        uses: actions/setup-node@v5
        with:
          node-version: 20
          cache: 'pnpm'

      # 4. 安装依赖
      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      # 5. 代码检查
      - name: Lint
        run: pnpm run lint

      # 6. 类型检查
      - name: Typecheck
        run: pnpm run typecheck

      # 7. 提取版本号
      - name: Extract Version
        id: extract-version
        run: |
          TAG=${GITHUB_REF#refs/tags/}
          VERSION=$(echo "$TAG" | sed 's/^v//')
          echo "version=$VERSION" >> $GITHUB_OUTPUT

      # 8. 提取项目名称(从 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

      # 9. 更新 package.json 版本号
      - name: Update Version
        run: |
          pnpm version ${{ steps.extract-version.outputs.version }} --no-git-tag-version
          cat package.json | grep version

      # 10. 构建 Nuxt.js 项目
      - name: Build Nuxt.js
        env:
          NODE_OPTIONS: "--max-old-space-size=4096"
        run: pnpm run build

      # 11. 创建压缩包(使用动态项目名称)
      - name: Create Distribution Archive
        run: |
          if [ ! -d ".output" ]; then
            echo "Error: .output directory not found!"
            exit 1
          fi

          echo "Contents of .output directory:"
          ls -la .output/

          PROJECT_NAME="${{ steps.project-name.outputs.project_name }}"
          VERSION="${{ steps.extract-version.outputs.version }}"

          tar -czf ${PROJECT_NAME}-${VERSION}.tar.gz -C .output .
          cd .output && zip -r ../${PROJECT_NAME}-${VERSION}.zip . && cd ..

          echo "Archive created successfully!"
          ls -la *.tar.gz *.zip

      # 12. 上传到 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 }}

      # 13. 上传构建产物供后续 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

      # 14. 提交版本号变更
      - name: Commit Changes
        run: |
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git add package.json pnpm-lock.yaml
          git commit -m "chore(release): bump to ${{ steps.extract-version.outputs.version }} [skip ci]"
          git push origin HEAD:${{ github.event.release.target_commitish }}

必需的 Secrets 配置

在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中添加:

Secret 名称说明
ACCESS_TOKEN具有写入权限的 Personal Access Token(用于上传 Release 资产)

💡 注意

默认的 GITHUB_TOKEN 在某些场景下权限不足,建议使用 Fine-grained Personal Access Token,赋予 contents: writeactions: write 权限。

关键步骤详解

1. 触发条件

yaml
on:
  release:
    types: [created]

只在用户手动创建 Release 时触发,避免每次 push 都触发构建。

2. 版本号提取

yaml
TAG=${GITHUB_REF#refs/tags/}
VERSION=$(echo "$TAG" | sed 's/^v//')

从 Release 标签(如 v1.0.0)中提取纯净版本号(1.0.0)。

3. 项目名称提取

yaml
PROJECT_NAME=$(node -p "require('./package.json').name")

package.json 中动态读取项目名称,避免在配置文件中硬编码,提高配置的通用性和可维护性。

💡 延伸说明:提取的 project_name 会通过 Job 的 outputs 字段传递给后续的部署 Job(第二部分内容)。这样在部署阶段可以动态获取项目名称,用于构建服务器上的部署路径和文件名,实现 Job 间的数据共享与解耦。

4. 版本号更新

yaml
pnpm version ${{ steps.extract-version.outputs.version }} --no-git-tag-version

更新 package.json 中的 version 字段,但不创建 git tag(tag 已由 Release 创建)。

5. 构建优化

yaml
env:
  NODE_OPTIONS: "--max-old-space-size=4096"

分配 4GB 内存给 Node.js,避免大型项目构建时内存溢出。

6. 压缩包创建

yaml
PROJECT_NAME="${{ steps.project-name.outputs.project_name }}"
VERSION="${{ steps.extract-version.outputs.version }}"
tar -czf ${PROJECT_NAME}-${VERSION}.tar.gz -C .output .

打包 .output 目录的内容,而不是目录本身。这样解压后直接得到文件,无需额外处理目录层级。

7. 构建产物保存

yaml
- 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

将 tar.gz 文件保存为 artifact,供后续部署 Job 使用。使用 overwrite: true 避免名称冲突。

💡 踩坑提醒

upload-artifact@v4 上传目录时存在 bug,建议上传文件(如 tar.gz)而不是目录

运行流程

  1. 在 GitHub 仓库中创建新的 Release,填写版本号(如 v0.4.0-alpha11
  2. GitHub Actions 自动触发 workflow
  3. 执行检出 → 安装依赖 → 检查 → 构建 → 打包 → 上传
  4. 版本号自动更新并推送到仓库
  5. Release 页面出现可下载的 tar.gzzip 附件

常见问题

Q: upload-artifact 报错 "No files found"?

A: 使用 v7 版本并上传文件而不是目录:

yaml
path: ${{ steps.project-name.outputs.project_name }}-${{ steps.extract-version.outputs.version }}.tar.gz

Q: 为什么需要 ACCESS_TOKEN

A: 默认的 GITHUB_TOKEN 在某些权限场景下无法上传 Release 资产,使用 Personal Access Token 更可靠。

Q: fetch-depth: 0 的作用?

A: 获取完整的 git 历史,确保版本号提交能正确推送。

总结

本文完成了系列文章的第一部分——构建与发布的自动化。通过配置 GitHub Actions,我们实现了:

  • 创建 Release 时自动触发构建
  • 自动生成 tar.gz 和 zip 压缩包
  • 自动上传到 Release 页面
  • 自动更新 package.json 版本号

下一篇预告:

第二部分将讲解如何将构建产物自动部署到远程服务器,涵盖:

  • 使用 scp-action 传输文件到服务器
  • 通过 ssh-action 执行远程命令
  • 使用 PM2 重启服务实现无缝上线
  • 完整的部署脚本解析

敬请期待!

💡 确认点

运行此 workflow 后,Release 页面应有 tar.gzzip 附件,且仓库中 package.json 的版本号已更新。