前端多环境自动化部署实战:GitHub Actions + Azure Blob + Cloudflare

前言

在团队协作开发中,经常会遇到这样的场景:

  • 多个功能分支并行开发,每个分支都需要独立的测试环境
  • 测试通过后需要部署到预发布环境验证
  • 最终手动控制发布到生产环境

如果每次都手动打包、上传、配置,不仅效率低下,还容易出错。本文将介绍一套完整的自动化部署方案,实现:

  • feature/* 分支 push 后自动部署到 test.example.com/feature/xxx
  • PR 合并到 master 后自动部署到预发布环境 example.com/preview
  • 生产环境 example.com 需要手动触发部署
  • 部署完成后自动发送飞书/钉钉通知

架构概览

#mermaid-svg-UIlwuxmq0DJ29DHp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UIlwuxmq0DJ29DHp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UIlwuxmq0DJ29DHp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UIlwuxmq0DJ29DHp .error-icon{fill:#552222;}#mermaid-svg-UIlwuxmq0DJ29DHp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UIlwuxmq0DJ29DHp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UIlwuxmq0DJ29DHp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UIlwuxmq0DJ29DHp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UIlwuxmq0DJ29DHp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UIlwuxmq0DJ29DHp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UIlwuxmq0DJ29DHp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UIlwuxmq0DJ29DHp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UIlwuxmq0DJ29DHp .marker.cross{stroke:#333333;}#mermaid-svg-UIlwuxmq0DJ29DHp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UIlwuxmq0DJ29DHp p{margin:0;}#mermaid-svg-UIlwuxmq0DJ29DHp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UIlwuxmq0DJ29DHp .cluster-label text{fill:#333;}#mermaid-svg-UIlwuxmq0DJ29DHp .cluster-label span{color:#333;}#mermaid-svg-UIlwuxmq0DJ29DHp .cluster-label span p{background-color:transparent;}#mermaid-svg-UIlwuxmq0DJ29DHp .label text,#mermaid-svg-UIlwuxmq0DJ29DHp span{fill:#333;color:#333;}#mermaid-svg-UIlwuxmq0DJ29DHp .node rect,#mermaid-svg-UIlwuxmq0DJ29DHp .node circle,#mermaid-svg-UIlwuxmq0DJ29DHp .node ellipse,#mermaid-svg-UIlwuxmq0DJ29DHp .node polygon,#mermaid-svg-UIlwuxmq0DJ29DHp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UIlwuxmq0DJ29DHp .rough-node .label text,#mermaid-svg-UIlwuxmq0DJ29DHp .node .label text,#mermaid-svg-UIlwuxmq0DJ29DHp .image-shape .label,#mermaid-svg-UIlwuxmq0DJ29DHp .icon-shape .label{text-anchor:middle;}#mermaid-svg-UIlwuxmq0DJ29DHp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UIlwuxmq0DJ29DHp .rough-node .label,#mermaid-svg-UIlwuxmq0DJ29DHp .node .label,#mermaid-svg-UIlwuxmq0DJ29DHp .image-shape .label,#mermaid-svg-UIlwuxmq0DJ29DHp .icon-shape .label{text-align:center;}#mermaid-svg-UIlwuxmq0DJ29DHp .node.clickable{cursor:pointer;}#mermaid-svg-UIlwuxmq0DJ29DHp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UIlwuxmq0DJ29DHp .arrowheadPath{fill:#333333;}#mermaid-svg-UIlwuxmq0DJ29DHp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UIlwuxmq0DJ29DHp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UIlwuxmq0DJ29DHp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UIlwuxmq0DJ29DHp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UIlwuxmq0DJ29DHp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UIlwuxmq0DJ29DHp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UIlwuxmq0DJ29DHp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UIlwuxmq0DJ29DHp .cluster text{fill:#333;}#mermaid-svg-UIlwuxmq0DJ29DHp .cluster span{color:#333;}#mermaid-svg-UIlwuxmq0DJ29DHp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UIlwuxmq0DJ29DHp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UIlwuxmq0DJ29DHp rect.text{fill:none;stroke-width:0;}#mermaid-svg-UIlwuxmq0DJ29DHp .icon-shape,#mermaid-svg-UIlwuxmq0DJ29DHp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UIlwuxmq0DJ29DHp .icon-shape p,#mermaid-svg-UIlwuxmq0DJ29DHp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UIlwuxmq0DJ29DHp .icon-shape .label rect,#mermaid-svg-UIlwuxmq0DJ29DHp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UIlwuxmq0DJ29DHp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UIlwuxmq0DJ29DHp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UIlwuxmq0DJ29DHp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户访问
Cloudflare CDN
Azure Blob Storage
GitHub Actions 工作流
GitHub 仓库
push
trigger
trigger
feature/xxx 分支
GitHub Actions
PR 合并到 master
手动触发 / release tag
安装依赖
构建打包
上传到 Azure Blob
清理 CDN 缓存
发送通知
test/project-name/
test/project-name/feature/xxx/
prod/project-name/
prod/project-name/preview/
test.example.com
example.com
test.example.com/feature/xxx
example.com/preview
example.com

核心原理

为什么能实现分支隔离部署?

关键在于 动态 base 路径 的设计。

当构建 feature/test1 分支时:

  1. GitHub Actions 将分支名 feature/test1 作为环境变量 BASE_URL 传入
  2. Vite 读取 BASE_URL,设置 base: '/feature/test1/'
  3. 构建产物输出到 dist/feature/test1/ 目录
  4. 上传到 Azure Blob 的 test/project-name/ 路径下
  5. 最终文件位置:test/project-name/feature/test1/index.html

这样,不同分支的文件互不干扰,通过 URL 路径区分。

Vite base 配置的作用

base 配置决定了打包后资源的引用路径:

html 复制代码
<!-- base: '/' 时 -->
<script src="/assets/main.js"></script>

<!-- base: '/feature/test1/' 时 -->
<script src="/feature/test1/assets/main.js"></script>

如果不设置正确的 base,部署到子路径后所有资源都会 404。

目录结构

复制代码
project/
├── .github/
│   └── workflows/
│       ├── ci.yml                        # 代码检查
│       ├── deploy-test.yml               # 测试环境部署
│       ├── deploy-preview.yml            # 预发布环境部署
│       └── deploy-production.yml         # 生产环境部署
├── src/
├── vite.config.ts
├── package.json
└── ...

完整代码实现

1. Vite 配置

typescript 复制代码
// vite.config.ts
import { fileURLToPath, URL } from 'node:url';
import { defineConfig, loadEnv, type ConfigEnv } from 'vite';
import vue from '@vitejs/plugin-vue';

// 从环境变量获取 BASE_URL,默认为根路径
// 这个值由 GitHub Actions 在构建时注入
const BASE_URL = process.env.BASE_URL || '/';

// 判断是否为预发布环境,用于代码中的条件判断
const IS_PREVIEW_ENV = BASE_URL.startsWith('/preview');

export default defineConfig((env: ConfigEnv) => {
  // 加载 .env 文件中的环境变量
  const viteEnv = loadEnv(env.mode, process.cwd());

  return {
    // 设置资源基础路径,这是实现分支隔离部署的关键
    // 例如:feature/test1 分支会设置为 '/feature/test1/'
    base: BASE_URL,

    build: {
      // 输出目录也要包含 BASE_URL,确保目录结构正确
      // 例如:dist/feature/test1/
      outDir: `dist${BASE_URL}`,

      rollupOptions: {
        output: {
          // 入口文件命名,添加 hash 用于缓存控制
          entryFileNames: 'assets/main-[hash].js',
          // 代码分割后的 chunk 命名
          chunkFileNames: 'assets/chunks/[name]-[hash].js',
          // 静态资源分类存放
          assetFileNames: (assetInfo) => {
            const name = assetInfo?.name || '';
            if (name.endsWith('.css')) {
              return 'assets/styles/[name]-[hash].css';
            }
            if (/\.(png|jpe?g|gif|svg|webp)$/.test(name)) {
              return 'assets/images/[name]-[hash].[ext]';
            }
            if (/\.(woff2?|ttf|eot)$/.test(name)) {
              return 'assets/fonts/[name]-[hash].[ext]';
            }
            return 'assets/[name]-[hash].[ext]';
          }
        }
      }
    },

    // 在代码中可以通过 import.meta.env.IS_PREVIEW_ENV 判断环境
    define: {
      'import.meta.env.IS_PREVIEW_ENV': IS_PREVIEW_ENV
    },

    plugins: [vue()],

    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url))
      }
    }
  };
});

2. Vue Router 配置

typescript 复制代码
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue')
  }
  // ... 其他路由
];

// 从 Vite 注入的环境变量获取 base 路径
// import.meta.env.BASE_URL 会自动读取 vite.config.ts 中的 base 配置
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
});

export default router;

3. package.json 脚本配置

json 复制代码
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build-testing": "vite build --mode testing",
    "build-preview": "vite build --mode preview",
    "build-production": "vite build --mode production",
    "clean": "rimraf dist",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --fix"
  }
}

4. 环境变量文件

bash 复制代码
# .env.testing
VITE_API_BASE_URL=https://test-api.example.com
VITE_ENV=testing

# .env.preview  
VITE_API_BASE_URL=https://api.example.com
VITE_ENV=preview

# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_ENV=production

5. GitHub Actions 工作流

5.1 代码检查 (ci.yml)
yaml 复制代码
# .github/workflows/ci.yml
# 代码质量检查工作流
# 触发时机:向 master 分支提交代码或发起 PR 时执行
name: CI

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      # 检出代码
      - uses: actions/checkout@v4

      # 设置 Node.js 环境
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x
          # 如果使用私有 npm 包,需要配置 registry
          # registry-url: https://npm.pkg.github.com
          # scope: '@your-org'

      # 缓存依赖,加速后续构建
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}
          restore-keys: npm-

      # 安装依赖
      - name: Install dependencies
        run: npm ci
        # 如果使用私有包,需要设置 token
        # env:
        #   NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      # 执行 lint 检查
      - name: Run lint
        run: npm run lint
5.2 测试环境部署 (deploy-test.yml)
yaml 复制代码
# .github/workflows/deploy-test.yml
# 测试环境自动部署工作流
# 触发时机:feature/* 分支 push 时自动触发,或手动触发
# 部署结果:https://test.example.com/feature/xxx/
name: 测试环境部署

on:
  # 手动触发入口
  workflow_dispatch:
  # feature 分支 push 自动触发
  push:
    branches:
      - feature/**

env:
  # 动态计算 BASE_URL
  # 如果是 feature 分支 push,则 BASE_URL 为 /feature/xxx
  # 否则为空(手动触发时部署到根路径)
  BASE_URL: ${{ github.event_name == 'push' && startsWith(github.ref_name, 'feature') && format('/{0}', github.ref_name) || '' }}
  
  # Azure Blob 配置
  AZURE_BLOB_CONNECTION_STRING: ${{ secrets.AZURE_BLOB_CONNECTION_STRING }}
  AZURE_BLOB_CONTAINER_NAME: ${{ vars.AZURE_BLOB_CONTAINER_NAME }}
  # 测试环境部署路径
  AZURE_BLOB_DESTINATION_PATH: "test/my-project"
  BUILD_OUT_DIR: "dist"

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    # 使用 GitHub Environment 管理敏感配置
    environment: testing
    outputs:
      # 输出 commit message,用于通知
      firstCommitMessage: ${{ steps.getCommitMessage.outputs.firstCommitMessage }}

    steps:
      - uses: actions/checkout@v4

      # 获取最新的 commit message
      - id: getCommitMessage
        name: Get commit message
        run: |
          firstCommitMessage=$(git log -1 --format=%s)
          echo "firstCommitMessage=$firstCommitMessage" >> "$GITHUB_OUTPUT"

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}
          restore-keys: npm-

      - name: Install dependencies
        run: npm ci

      # 构建时注入 BASE_URL 环境变量
      # Vite 会读取这个变量设置 base 路径
      - name: Build
        run: npm run clean && BASE_URL=$BASE_URL npm run build-testing

      # 上传到 Azure Blob Storage
      - name: Deploy to Azure Blob
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}'

      # 单独设置 HTML 文件的缓存策略
      # HTML 文件不缓存,确保用户总是获取最新版本
      - name: Set HTML cache control
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}' \
              --content-cache-control 'public, max-age=0' \
              --pattern '*.html'

  # 部署完成后发送通知
  notify:
    needs: deploy
    runs-on: ubuntu-latest
    name: Send notification
    # 无论部署成功还是失败都发送通知
    if: ${{ always() }}
    steps:
      - name: Send Feishu notification
        env:
          WEBHOOK_URL: ${{ vars.FEISHU_WEBHOOK }}
          DEPLOY_RESULT: ${{ needs.deploy.result == 'success' && '成功' || '失败' }}
          DEPLOY_ENV: '测试环境'
          # 动态生成访问地址
          DEPLOY_URL: ${{ format('https://test.example.com{0}/', env.BASE_URL) }}
          BRANCH_NAME: ${{ github.ref_name }}
          ACTOR: ${{ github.triggering_actor }}
          ACTION_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
          COMMIT_MESSAGE: ${{ needs.deploy.outputs.firstCommitMessage }}
        run: |
          # 获取中国时区时间
          eventTime=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M:%S")
          
          # 构造飞书消息体
          reqData="{
            \"msg_type\": \"text\",
            \"content\": {
              \"text\": \"项目部署通知\n状态:$DEPLOY_RESULT\n环境:$DEPLOY_ENV\n分支:$BRANCH_NAME\n提交:$COMMIT_MESSAGE\n操作人:$ACTOR\n时间:$eventTime\n访问地址:$DEPLOY_URL\n构建详情:$ACTION_URL\"
            }
          }"
          
          curl -X POST -H "Content-Type: application/json" -d "$reqData" $WEBHOOK_URL
5.3 预发布环境部署 (deploy-preview.yml)
yaml 复制代码
# .github/workflows/deploy-preview.yml
# 预发布环境部署工作流
# 触发时机:PR 合并到 master 时自动触发,或手动触发
# 部署结果:https://example.com/preview/
name: 预发布环境部署

on:
  workflow_dispatch:
  pull_request:
    types:
      - closed
    branches:
      - master

env:
  AZURE_BLOB_CONNECTION_STRING: ${{ secrets.AZURE_BLOB_CONNECTION_STRING }}
  AZURE_BLOB_CONTAINER_NAME: ${{ vars.AZURE_BLOB_CONTAINER_NAME }}
  # 预发布环境部署到生产容器的 preview 子路径
  AZURE_BLOB_DESTINATION_PATH: "prod/my-project"
  BUILD_OUT_DIR: "dist"

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    # 只在手动触发或 PR 被合并时执行(不是关闭未合并的 PR)
    if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
    runs-on: ubuntu-latest
    environment: preview

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}
          restore-keys: npm-

      - name: Install dependencies
        run: npm ci

      # 预发布环境使用 /preview 作为 base 路径
      - name: Build
        run: npm run clean && BASE_URL=/preview npm run build-preview

      - name: Deploy to Azure Blob
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}'

      - name: Set HTML cache control
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}' \
              --content-cache-control 'public, max-age=0' \
              --pattern '*.html'

  notify:
    needs: deploy
    runs-on: ubuntu-latest
    if: ${{ always() }}
    steps:
      - name: Send notification
        env:
          WEBHOOK_URL: ${{ vars.FEISHU_WEBHOOK }}
          DEPLOY_RESULT: ${{ needs.deploy.result == 'success' && '成功' || '失败' }}
          DEPLOY_ENV: '预发布环境'
          DEPLOY_URL: 'https://example.com/preview/'
          PR_BRANCH: ${{ github.event.pull_request.head.ref }}
          PR_URL: ${{ github.event.pull_request.html_url }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          ACTOR: ${{ github.triggering_actor }}
          ACTION_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
        run: |
          eventTime=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M:%S")
          reqData="{
            \"msg_type\": \"text\",
            \"content\": {
              \"text\": \"项目部署通知\n状态:$DEPLOY_RESULT\n环境:$DEPLOY_ENV\nPR标题:$PR_TITLE\nPR分支:$PR_BRANCH\n操作人:$ACTOR\n时间:$eventTime\n访问地址:$DEPLOY_URL\nPR地址:$PR_URL\n构建详情:$ACTION_URL\n\n提示:预发布环境验证完成后,请手动触发正式发布\"
            }
          }"
          curl -X POST -H "Content-Type: application/json" -d "$reqData" $WEBHOOK_URL
5.4 生产环境部署 (deploy-production.yml)
yaml 复制代码
# .github/workflows/deploy-production.yml
# 生产环境部署工作流
# 触发时机:手动触发 或 推送 release/* tag
# 部署结果:https://example.com/
# 注意:生产环境部署需要谨慎,建议只允许手动触发
name: 生产环境部署

on:
  # 手动触发,这是推荐的生产部署方式
  workflow_dispatch:
  # 也可以通过 release tag 触发
  push:
    tags:
      - release/**

env:
  AZURE_BLOB_CONNECTION_STRING: ${{ secrets.AZURE_BLOB_CONNECTION_STRING }}
  AZURE_BLOB_CONTAINER_NAME: ${{ vars.AZURE_BLOB_CONTAINER_NAME }}
  AZURE_BLOB_DESTINATION_PATH: "prod/my-project"
  BUILD_OUT_DIR: "dist"

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    # 生产环境需要单独的 environment 配置
    # 可以在 GitHub 仓库设置中配置审批流程
    environment: production
    outputs:
      firstCommitMessage: ${{ steps.getCommitMessage.outputs.firstCommitMessage }}

    steps:
      - uses: actions/checkout@v4

      - id: getCommitMessage
        name: Get commit message
        run: |
          firstCommitMessage=$(git log -1 --format=%s)
          echo "firstCommitMessage=$firstCommitMessage" >> "$GITHUB_OUTPUT"

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20.x

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}
          restore-keys: npm-

      - name: Install dependencies
        run: npm ci

      # 生产环境 base 为根路径
      - name: Build
        run: npm run build-production

      - name: Deploy to Azure Blob
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}'

      - name: Set HTML cache control
        uses: Azure/cli@v2.1.0
        with:
          inlineScript: |
            az storage blob upload-batch \
              --destination '${{ env.AZURE_BLOB_CONTAINER_NAME }}' \
              --source ${{ env.BUILD_OUT_DIR }} \
              --destination-path ${{ env.AZURE_BLOB_DESTINATION_PATH }} \
              --overwrite \
              --connection-string '${{ env.AZURE_BLOB_CONNECTION_STRING }}' \
              --content-cache-control 'public, max-age=0' \
              --pattern '*.html'

      # 生产部署后清理 CDN 缓存
      # 确保用户能立即访问到最新版本
      - name: Clear Cloudflare cache
        id: clearCache
        uses: actions/github-script@v6
        env:
          # Cloudflare Zone ID,在 Cloudflare 控制台获取
          CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
          CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
        with:
          result-encoding: string
          script: |
            const { CF_ZONE_ID, CF_API_TOKEN } = process.env;
            const apiUrl = `https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache`;
            
            // 清理指定页面的缓存
            // 也可以使用 purge_everything: true 清理所有缓存
            const response = await fetch(apiUrl, {
              method: 'POST',
              headers: {
                'Authorization': `Bearer ${CF_API_TOKEN}`,
                'Content-Type': 'application/json'
              },
              body: JSON.stringify({
                files: [
                  'https://example.com/',
                  'https://example.com/about',
                  'https://example.com/products'
                  // 添加其他需要清理缓存的页面
                ]
              })
            });
            
            const result = await response.json();
            console.log('Cloudflare cache purge result:', result);
            return result?.success ? 'success' : 'failure';

      # 检查缓存清理结果
      - name: Check cache clear result
        run: |
          if [ "${{ steps.clearCache.outputs.result }}" != "success" ]; then
            echo "Warning: Failed to clear Cloudflare cache"
            # 缓存清理失败不阻断部署,只是警告
          fi

  notify:
    needs: deploy
    runs-on: ubuntu-latest
    if: ${{ always() }}
    steps:
      - name: Send notification
        env:
          WEBHOOK_URL: ${{ vars.FEISHU_WEBHOOK }}
          DEPLOY_RESULT: ${{ needs.deploy.result == 'success' && '成功' || '失败' }}
          DEPLOY_ENV: '生产环境'
          DEPLOY_URL: 'https://example.com'
          BRANCH_NAME: ${{ github.ref_name }}
          ACTOR: ${{ github.triggering_actor }}
          ACTION_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
          COMMIT_MESSAGE: ${{ needs.deploy.outputs.firstCommitMessage }}
        run: |
          eventTime=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M:%S")
          reqData="{
            \"msg_type\": \"text\",
            \"content\": {
              \"text\": \"生产环境部署通知\n状态:$DEPLOY_RESULT\n提交信息:$COMMIT_MESSAGE\n分支/标签:$BRANCH_NAME\n操作人:$ACTOR\n时间:$eventTime\n访问地址:$DEPLOY_URL\n构建详情:$ACTION_URL\"
            }
          }"
          curl -X POST -H "Content-Type: application/json" -d "$reqData" $WEBHOOK_URL

配置说明

GitHub 仓库配置

需要在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中配置以下内容:

Secrets(敏感信息)
名称 说明 获取方式
AZURE_BLOB_CONNECTION_STRING Azure Storage 连接字符串 Azure Portal -> Storage Account -> Access keys
CF_ZONE_ID Cloudflare Zone ID Cloudflare Dashboard -> 域名概览页右侧
CF_API_TOKEN Cloudflare API Token Cloudflare Dashboard -> My Profile -> API Tokens
Variables(非敏感配置)
名称 说明 示例值
AZURE_BLOB_CONTAINER_NAME Azure Blob 容器名 $web
FEISHU_WEBHOOK 飞书机器人 Webhook 地址 https://open.feishu.cn/open-apis/bot/v2/hook/xxx
Environments(环境配置)

建议创建三个 Environment:

  1. testing - 测试环境,无需审批
  2. preview - 预发布环境,可选审批
  3. production - 生产环境,建议配置审批流程

在 Settings -> Environments 中可以为每个环境配置:

  • 审批人员(Required reviewers)
  • 等待时间(Wait timer)
  • 部署分支限制(Deployment branches)

Azure Blob Storage 配置

1. 创建 Storage Account
bash 复制代码
# 使用 Azure CLI 创建
az storage account create \
  --name mystorageaccount \
  --resource-group myResourceGroup \
  --location eastasia \
  --sku Standard_LRS
2. 启用静态网站托管
bash 复制代码
az storage blob service-properties update \
  --account-name mystorageaccount \
  --static-website \
  --index-document index.html \
  --404-document index.html

启用后会自动创建 $web 容器,静态网站端点格式为:

https://mystorageaccount.z6.web.core.windows.net

3. 配置 CORS(如需要)
bash 复制代码
az storage cors add \
  --account-name mystorageaccount \
  --services b \
  --methods GET HEAD \
  --origins '*' \
  --allowed-headers '*'

Cloudflare 配置

1. 添加域名

在 Cloudflare Dashboard 添加域名,并将域名的 NS 记录指向 Cloudflare。

2. 配置 DNS 记录
类型 名称 内容 代理状态
CNAME @ mystorageaccount.z6.web.core.windows.net 已代理
CNAME test mystorageaccount.z6.web.core.windows.net 已代理
3. 配置 Page Rules(可选)

为了优化缓存策略,可以添加 Page Rules:

复制代码
URL: example.com/*.html
设置: Cache Level = Bypass

URL: example.com/assets/*
设置: Cache Level = Cache Everything, Edge Cache TTL = 1 month
4. 创建 API Token

在 My Profile -> API Tokens -> Create Token:

  • 权限:Zone -> Cache Purge -> Purge
  • 区域资源:选择对应的域名

工作流程图

完整的开发部署流程

#mermaid-svg-rwvA1Ebnf89OsV8L{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rwvA1Ebnf89OsV8L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rwvA1Ebnf89OsV8L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rwvA1Ebnf89OsV8L .error-icon{fill:#552222;}#mermaid-svg-rwvA1Ebnf89OsV8L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rwvA1Ebnf89OsV8L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rwvA1Ebnf89OsV8L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rwvA1Ebnf89OsV8L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rwvA1Ebnf89OsV8L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rwvA1Ebnf89OsV8L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rwvA1Ebnf89OsV8L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rwvA1Ebnf89OsV8L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rwvA1Ebnf89OsV8L .marker.cross{stroke:#333333;}#mermaid-svg-rwvA1Ebnf89OsV8L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rwvA1Ebnf89OsV8L p{margin:0;}#mermaid-svg-rwvA1Ebnf89OsV8L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rwvA1Ebnf89OsV8L .cluster-label text{fill:#333;}#mermaid-svg-rwvA1Ebnf89OsV8L .cluster-label span{color:#333;}#mermaid-svg-rwvA1Ebnf89OsV8L .cluster-label span p{background-color:transparent;}#mermaid-svg-rwvA1Ebnf89OsV8L .label text,#mermaid-svg-rwvA1Ebnf89OsV8L span{fill:#333;color:#333;}#mermaid-svg-rwvA1Ebnf89OsV8L .node rect,#mermaid-svg-rwvA1Ebnf89OsV8L .node circle,#mermaid-svg-rwvA1Ebnf89OsV8L .node ellipse,#mermaid-svg-rwvA1Ebnf89OsV8L .node polygon,#mermaid-svg-rwvA1Ebnf89OsV8L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rwvA1Ebnf89OsV8L .rough-node .label text,#mermaid-svg-rwvA1Ebnf89OsV8L .node .label text,#mermaid-svg-rwvA1Ebnf89OsV8L .image-shape .label,#mermaid-svg-rwvA1Ebnf89OsV8L .icon-shape .label{text-anchor:middle;}#mermaid-svg-rwvA1Ebnf89OsV8L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rwvA1Ebnf89OsV8L .rough-node .label,#mermaid-svg-rwvA1Ebnf89OsV8L .node .label,#mermaid-svg-rwvA1Ebnf89OsV8L .image-shape .label,#mermaid-svg-rwvA1Ebnf89OsV8L .icon-shape .label{text-align:center;}#mermaid-svg-rwvA1Ebnf89OsV8L .node.clickable{cursor:pointer;}#mermaid-svg-rwvA1Ebnf89OsV8L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rwvA1Ebnf89OsV8L .arrowheadPath{fill:#333333;}#mermaid-svg-rwvA1Ebnf89OsV8L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rwvA1Ebnf89OsV8L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rwvA1Ebnf89OsV8L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rwvA1Ebnf89OsV8L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rwvA1Ebnf89OsV8L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rwvA1Ebnf89OsV8L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rwvA1Ebnf89OsV8L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rwvA1Ebnf89OsV8L .cluster text{fill:#333;}#mermaid-svg-rwvA1Ebnf89OsV8L .cluster span{color:#333;}#mermaid-svg-rwvA1Ebnf89OsV8L div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rwvA1Ebnf89OsV8L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rwvA1Ebnf89OsV8L rect.text{fill:none;stroke-width:0;}#mermaid-svg-rwvA1Ebnf89OsV8L .icon-shape,#mermaid-svg-rwvA1Ebnf89OsV8L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rwvA1Ebnf89OsV8L .icon-shape p,#mermaid-svg-rwvA1Ebnf89OsV8L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rwvA1Ebnf89OsV8L .icon-shape .label rect,#mermaid-svg-rwvA1Ebnf89OsV8L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rwvA1Ebnf89OsV8L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rwvA1Ebnf89OsV8L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rwvA1Ebnf89OsV8L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否





开发者创建 feature/xxx 分支
本地开发
Push 到远程
GitHub Actions
自动部署到测试环境
test.example.com/feature/xxx
测试验证
测试通过?
创建 PR 到 master
Code Review
Review 通过?
合并 PR
GitHub Actions
自动部署到预发布环境
example.com/preview
预发布验证
验证通过?
修复问题,重新提 PR
手动触发生产部署
GitHub Actions
部署到生产环境
example.com
清理 CDN 缓存
发送部署通知

分支与环境对应关系

#mermaid-svg-s201exWLa9x2t3IB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-s201exWLa9x2t3IB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-s201exWLa9x2t3IB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-s201exWLa9x2t3IB .error-icon{fill:#552222;}#mermaid-svg-s201exWLa9x2t3IB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-s201exWLa9x2t3IB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-s201exWLa9x2t3IB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-s201exWLa9x2t3IB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-s201exWLa9x2t3IB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-s201exWLa9x2t3IB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-s201exWLa9x2t3IB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-s201exWLa9x2t3IB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-s201exWLa9x2t3IB .marker.cross{stroke:#333333;}#mermaid-svg-s201exWLa9x2t3IB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-s201exWLa9x2t3IB p{margin:0;}#mermaid-svg-s201exWLa9x2t3IB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-s201exWLa9x2t3IB .cluster-label text{fill:#333;}#mermaid-svg-s201exWLa9x2t3IB .cluster-label span{color:#333;}#mermaid-svg-s201exWLa9x2t3IB .cluster-label span p{background-color:transparent;}#mermaid-svg-s201exWLa9x2t3IB .label text,#mermaid-svg-s201exWLa9x2t3IB span{fill:#333;color:#333;}#mermaid-svg-s201exWLa9x2t3IB .node rect,#mermaid-svg-s201exWLa9x2t3IB .node circle,#mermaid-svg-s201exWLa9x2t3IB .node ellipse,#mermaid-svg-s201exWLa9x2t3IB .node polygon,#mermaid-svg-s201exWLa9x2t3IB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-s201exWLa9x2t3IB .rough-node .label text,#mermaid-svg-s201exWLa9x2t3IB .node .label text,#mermaid-svg-s201exWLa9x2t3IB .image-shape .label,#mermaid-svg-s201exWLa9x2t3IB .icon-shape .label{text-anchor:middle;}#mermaid-svg-s201exWLa9x2t3IB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-s201exWLa9x2t3IB .rough-node .label,#mermaid-svg-s201exWLa9x2t3IB .node .label,#mermaid-svg-s201exWLa9x2t3IB .image-shape .label,#mermaid-svg-s201exWLa9x2t3IB .icon-shape .label{text-align:center;}#mermaid-svg-s201exWLa9x2t3IB .node.clickable{cursor:pointer;}#mermaid-svg-s201exWLa9x2t3IB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-s201exWLa9x2t3IB .arrowheadPath{fill:#333333;}#mermaid-svg-s201exWLa9x2t3IB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-s201exWLa9x2t3IB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-s201exWLa9x2t3IB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s201exWLa9x2t3IB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-s201exWLa9x2t3IB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s201exWLa9x2t3IB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-s201exWLa9x2t3IB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-s201exWLa9x2t3IB .cluster text{fill:#333;}#mermaid-svg-s201exWLa9x2t3IB .cluster span{color:#333;}#mermaid-svg-s201exWLa9x2t3IB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-s201exWLa9x2t3IB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-s201exWLa9x2t3IB rect.text{fill:none;stroke-width:0;}#mermaid-svg-s201exWLa9x2t3IB .icon-shape,#mermaid-svg-s201exWLa9x2t3IB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s201exWLa9x2t3IB .icon-shape p,#mermaid-svg-s201exWLa9x2t3IB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-s201exWLa9x2t3IB .icon-shape .label rect,#mermaid-svg-s201exWLa9x2t3IB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s201exWLa9x2t3IB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-s201exWLa9x2t3IB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-s201exWLa9x2t3IB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 环境
分支
push 自动触发
PR 合并自动触发
手动触发
手动触发
feature/xxx
master
release/v1.0.0
测试环境

test.example.com/feature/xxx
预发布环境

example.com/preview
生产环境

example.com

常见问题

Q1: 为什么资源加载 404?

最常见的原因是 base 路径配置不正确。检查以下几点:

  1. Vite 配置中的 base 是否正确读取了 BASE_URL 环境变量
  2. 构建命令是否正确传入了 BASE_URL
  3. Vue Router 的 createWebHistory 是否使用了 import.meta.env.BASE_URL

Q2: 如何支持 SPA 路由?

Azure Blob 静态网站需要配置 404 页面指向 index.html

bash 复制代码
az storage blob service-properties update \
  --account-name mystorageaccount \
  --static-website \
  --index-document index.html \
  --404-document index.html  # 关键配置

Q3: 如何清理旧的分支部署?

可以添加一个定时清理的 workflow:

yaml 复制代码
name: Cleanup old deployments

on:
  schedule:
    # 每周日凌晨 2 点执行
    - cron: '0 2 * * 0'
  workflow_dispatch:

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Delete old feature deployments
        uses: Azure/cli@v2.1.0
        env:
          AZURE_BLOB_CONNECTION_STRING: ${{ secrets.AZURE_BLOB_CONNECTION_STRING }}
        with:
          inlineScript: |
            # 列出所有 feature 目录
            # 删除超过 30 天未修改的目录
            # 具体实现根据需求调整
            echo "Cleanup completed"

Q4: 如何回滚到上一个版本?

方案一:重新触发上一次成功的 workflow

方案二:使用 Git tag 管理版本,回滚时部署指定 tag

bash 复制代码
# 创建版本 tag
git tag release/v1.0.0
git push origin release/v1.0.0

# 回滚时,手动触发 workflow 并选择对应的 tag

Q5: 如何在代码中判断当前环境?

typescript 复制代码
// 判断是否为预发布环境
if (import.meta.env.IS_PREVIEW_ENV) {
  console.log('当前是预发布环境');
}

// 判断构建模式
if (import.meta.env.MODE === 'production') {
  console.log('生产模式');
}

// 使用自定义环境变量
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;

扩展:钉钉通知

如果团队使用钉钉而不是飞书,可以替换通知步骤:

yaml 复制代码
- name: Send DingTalk notification
  env:
    DINGTALK_WEBHOOK: ${{ vars.DINGTALK_WEBHOOK }}
    DEPLOY_RESULT: ${{ needs.deploy.result == 'success' && '成功' || '失败' }}
  run: |
    eventTime=$(TZ='Asia/Shanghai' date +"%Y-%m-%d %H:%M:%S")
    reqData="{
      \"msgtype\": \"markdown\",
      \"markdown\": {
        \"title\": \"部署通知\",
        \"text\": \"### 项目部署$DEPLOY_RESULT\n- 环境:测试环境\n- 时间:$eventTime\n- 操作人:${{ github.triggering_actor }}\"
      }
    }"
    curl -X POST -H "Content-Type: application/json" -d "$reqData" $DINGTALK_WEBHOOK

扩展:使用 Cloudflare Pages 替代 Azure Blob

如果不想使用 Azure,可以直接使用 Cloudflare Pages:

yaml 复制代码
- name: Deploy to Cloudflare Pages
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CF_API_TOKEN }}
    accountId: ${{ secrets.CF_ACCOUNT_ID }}
    command: pages deploy dist --project-name=my-project --branch=${{ github.ref_name }}

Cloudflare Pages 的优势:

  • 自动处理分支预览部署
  • 内置 CDN,无需额外配置
  • 免费额度较高

总结

本文介绍的多环境自动化部署方案,核心要点:

  1. 动态 base 路径 :通过环境变量控制 Vite 的 base 配置,实现分支隔离部署
  2. GitHub Actions 工作流:根据不同触发条件执行不同的部署流程
  3. Azure Blob + Cloudflare:静态资源存储 + CDN 加速的经典组合
  4. 缓存策略:HTML 不缓存,静态资源长期缓存
  5. 通知机制:部署完成后自动通知团队

这套方案已在多个生产项目中验证,可以直接复用到新项目中。根据实际需求,可以灵活调整触发条件、部署目标和通知方式。

相关推荐
香香爱编程1 小时前
vue3自定义顶部弹窗
前端·javascript·vue.js
weelinking2 小时前
【产品】10_搭建前端框架——把你的原型变成真实页面
java·大数据·前端·数据库·人工智能·python·前端框架
蜡台2 小时前
Vue Echart 的 **高阶组件化** 封装思路
前端·javascript·vue.js·echarts
xuankuxiaoyao2 小时前
vue.js 路由第二篇
前端·javascript·vue.js
一 乐2 小时前
图书电子商务网站系统|基于SprinBoot+vue图书电子商务网站设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·图书电子商务网站系统
weifengma-wish2 小时前
通过NPM安装claude code
前端·npm·node.js
yaoxin5211232 小时前
421. Java 日期时间 API - 包结构 & 方法命名规范
java·前端·python
叫我少年2 小时前
ASP.NET Core Razor 语法简述
前端
ZGi.ai10 小时前
人工审查节点:让自动化工作流多一步人工把关
运维·人工智能·自动化·人机协同·智能体工作流·人工审查