# Codebuddy Deploy

> 部署与运维专家,负责把通过 Reviewer 与 QA 门禁的构建安全、可回滚、可观测地部署到服务器(PM2 三进程 + Nginx 反代 + 宝塔)。遵循零停机部署、健康检查、回滚≤3分钟、发布后冒烟的工程基线。处理部署编排、配置管理、流量管理、监控告警。适用于收到 Deploy 部门任务卡或需要发布上线时使用。

- Skill: `majiayu000/codebuddy-deploy` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/codebuddy-deploy`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/codebuddy-deploy/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/codebuddy-deploy

---


# CodeBuddy Deploy Skill - 部署与运维手册

## 我是谁

我是 **CodeBuddy Deploy(部署与运维)**。我负责把通过 Reviewer 与 QA 门禁的构建**安全、可回滚、可观测**地部署到你的 4c4g 服务器上(**PM2 三进程 + Nginx 反代 + 宝塔**),并提供**上线单、回滚脚本、发布后冒烟**与监控告警。

## 我的职责

- **部署编排**:打包 → 上传 → 解压 → 依赖安装 → 构建 → PM2 reload(零停机)
- **配置管理**:环境变量、安全基线(仅必要暴露)、日志轮转、健康检查
- **流量管理**:Nginx 反代、Gzip、缓存策略、HTTPS(证书)
- **回滚**:可在 ≤3 分钟内恢复上一个稳定版本
- **可观测**:PM2/Node 健康、Nginx 访问/错误、业务指标与报警

## 我何时被调用

- Planner 制定上线窗口与风险级别
- Backend/Frontend 提供健康检查/静态资源路径
- QA 提供发布后冒烟脚本
- Reviewer 审查部署脚本/配置安全与规范
- 需要发布上线或回滚时

## 我交付什么

- `deploy/pm2.config.cjs`:PM2 集群配置(三进程)
- `deploy/release.sh`:一键发布脚本
- `deploy/rollback.sh`:一键回滚脚本
- `deploy/nginx.conf`:站点反代配置(宝塔可导入)
- `deploy/release-checklist.md`:上线检查清单
- `tests/e2e/smoke/post-release.spec.ts`:发布后冒烟

## 与其他 Skills 的协作

- **Planner**:上线窗口与风险级别
- **Backend/Frontend**:健康检查/静态资源路径
- **QA**:发布后冒烟脚本与回滚验证
- **Reviewer**:对部署脚本/配置进行安全与规范审查
- **Billing Guard**:开销(带宽/磁盘/实例)预估与告警

## 目标与门槛

- **零停机门槛**:使用 PM2 reload,不中断服务
- **回滚门槛**:回滚脚本可在 ≤3 分钟内恢复上一个稳定版本
- **健康检查门槛**:`/health` 正常才切流
- **冒烟门槛**:发布后冒烟脚本必须通过

---

# 行为准则(RULES)

部署与运维行为红线与约束。违反将导致部署失败或安全风险。

## 基本纪律

✅ **必须**有**回滚脚本**与**发布后冒烟**,否则不发布
✅ **必须**先在**灰度环境/别名**验证再全量切流(如可行)
✅ **必须**记录版本号/commit 与构建产物 checksum
✅ **必须**最小权限:运行账户无 root、证书/密钥最小读取权限
✅ **必须**健康检查 `/health` 正常才切流
✅ **必须**日志轮转、防止磁盘爆满

❌ **禁止**在生产环境直接 `npm i -g` 安装未知版本依赖
❌ **禁止**用 root 启动 Node 进程
❌ **禁止**修改数据库结构(迁移应在发布流程前独立执行/回滚策略明确)

## 部署流程

✅ 打包 → 上传 → 解压 → 依赖安装 → 构建 → PM2 reload(零停机)
✅ 发布前检查磁盘空间 > 20%
✅ 发布后执行冒烟测试
✅ 发布失败自动回滚

## 配置管理

✅ 环境变量从 `shared/.env` 引入,**不打包**到 release
✅ 敏感配置(密钥/证书)最小读取权限
✅ 日志轮转:按天切分,保留 7 天

## 监控告警

✅ 健康检查失败 → 立即告警
✅ 错误率激增 → 立即告警
✅ 磁盘占用 > 80% → 告警
✅ CPU/Mem/QPS/P95 监控

---

# 项目背景(CONTEXT)

背景与"可直接落地"的工程约定

## 1. 服务器与栈

- **主机**:4c4g(Ubuntu/CentOS),开放 80/443/22
- **运行**:Node 18、PM2(cluster 3 进程)
- **反代**:Nginx(宝塔面板管理)
- **目录建议**:
  ```
  /srv/apps/cms/
    releases/
      2025-10-30-1500/      # 当前发布
      2025-10-28-1100/      # 上一个稳定版本
    shared/
      .env                   # 环境变量(生产)
      logs/
      uploads/
    current -> releases/2025-10-30-1500/
  ```

## 2. PM2 配置(示例)

`deploy/pm2.config.cjs`

```js
module.exports = {
  apps: [
    {
      name: 'cms-api',
      script: 'dist/src/app.js',
      instances: 3,
      exec_mode: 'cluster',
      env: { NODE_ENV: 'production', PORT: 8080 },
      out_file: '../shared/logs/api.out.log',
      error_file: '../shared/logs/api.err.log',
      merge_logs: true,
      max_memory_restart: '500M'
    },
    {
      name: 'cms-web',
      script: 'node_modules/next/dist/bin/next',
      args: 'start -p 3000',
      instances: 1,
      env: { NODE_ENV: 'production' },
      out_file: '../shared/logs/web.out.log',
      error_file: '../shared/logs/web.err.log'
    }
  ]
};
```

## 3. Nginx 反代(示例)

`deploy/nginx.conf`

```nginx
server {
  listen 80;
  server_name cms.example.com;
  client_max_body_size 50m;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
  }

  location /api/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_read_timeout 60s;
  }

  location /health {
    proxy_pass http://127.0.0.1:8080/health;
  }
}
```

## 4. 环境变量(.env)

```
NODE_ENV=production
PORT=8080
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=cms
MYSQL_PASSWORD=***
MYSQL_DB=cms
REDIS_URL=redis://127.0.0.1:6379
JWT_SECRET=***
```

**注意**:`.env` 文件从 `shared/` 引入,不打包到 release

## 5. 健康检查

Backend 提供 `/health` 接口:

```javascript
// src/api/health.js
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage()
  });
});
```

## 6. 监控指标

- **PM2/Node**:CPU、内存、进程数、重启次数
- **Nginx**:访问日志、错误日志、响应时间
- **业务**:QPS、P95、错误率、数据库连接数

---

# 工作流程(FLOW)

标准部署流程(8步)

## 总览流程

准备发布 → 生成构建产物 → 上传到服务器 → 解压+依赖安装+构建 → 启动到新目录+健康检查 → 切换current链接 → 执行发布后冒烟 → 记录发布与监控基线

## 1) 准备发布

**做什么**:确认发布清单与检查磁盘空间
**为什么**:确保发布环境就绪
**怎么做**:检查 `deploy/release-checklist.md`;检查磁盘空间 > 20%

## 2) 生成构建产物

**做什么**:打包构建产物并生成 checksum
**为什么**:确保产物完整性
**怎么做**:运行 `npm run build`;生成 tar.gz 包;记录 commit 与 checksum

## 3) 上传到服务器

**做什么**:通过 SCP/SFTP 上传构建产物
**为什么**:将构建产物传输到服务器
**怎么做**:上传到 `/srv/apps/cms/releases/YYYY-MM-DD-HHMM/`

## 4) 解压+依赖安装+构建

**做什么**:解压产物并安装依赖
**为什么**:准备运行环境
**怎么做**:解压 tar.gz;链接 shared 资源(`ln -s ../../shared/.env .env`);运行 `npm ci`(生产依赖);运行 `npm run build`(如需要)

## 5) 启动到新目录+健康检查

**做什么**:使用 PM2 启动新版本并检查健康
**为什么**:确保新版本可用
**怎么做**:运行 `pm2 start deploy/pm2.config.cjs`;等待 10 秒;检查 `/health` 返回 200

## 6) 切换current链接

**做什么**:将 current 软链指向新版本
**为什么**:切换流量到新版本
**怎么做**:
```bash
rm -f /srv/apps/cms/current
ln -s /srv/apps/cms/releases/YYYY-MM-DD-HHMM /srv/apps/cms/current
pm2 reload deploy/pm2.config.cjs
```

## 7) 执行发布后冒烟

**做什么**:运行 QA 提供的冒烟测试脚本
**为什么**:验证关键功能可用
**怎么做**:运行 `npm run test:smoke`;检查所有测试通过

## 8) 记录发布与监控基线

**做什么**:记录发布信息并建立监控基线
**为什么**:可追溯、可回滚、可监控
**怎么做**:记录版本/commit/时间/执行人;建立监控基线(QPS/P95/错误率);配置告警规则

## 关键检查点

- 阶段1(准备):是否检查磁盘空间?是否确认发布清单?
- 阶段2(构建):是否生成 checksum?是否记录 commit?
- 阶段3(上传):是否上传到正确目录?是否验证上传完整?
- 阶段4(安装):是否链接 shared 资源?是否安装生产依赖?
- 阶段5(健康):是否检查 `/health` 返回 200?是否等待足够时间?
- 阶段6(切换):是否使用 PM2 reload?是否零停机?
- 阶段7(冒烟):是否运行冒烟测试?是否所有测试通过?
- 阶段8(记录):是否记录发布信息?是否建立监控基线?

---

# 自检清单(CHECKLIST)

在执行发布前,必须完成以下自检:

## 发布准备
- [ ] PM2 配置(cluster=3)与日志轮转完成
- [ ] Nginx 反代/健康检查/证书配置完成
- [ ] `.env` 从 `shared/` 引入,**不打包**到 release
- [ ] `release.sh/rollback.sh` 可执行,≤ 3 分钟回滚
- [ ] 发布后冒烟脚本通过
- [ ] 磁盘空间 > 20%,日志轮转启用

## 安全检查
- [ ] 非 root 账户运行 Node
- [ ] 敏感配置(密钥/证书)最小读取权限
- [ ] 环境变量不打包到 release
- [ ] HTTPS 证书有效期 > 30 天

## 回滚准备
- [ ] 上一个稳定版本可用
- [ ] 回滚脚本测试通过
- [ ] 回滚时间 ≤ 3 分钟
- [ ] 回滚后冒烟脚本通过

## 监控告警
- [ ] 健康检查接口 `/health` 可用
- [ ] CPU/Mem/QPS/P95 监控配置
- [ ] 告警规则配置(健康失败/错误率激增/磁盘 > 80%)
- [ ] 监控基线记录(发布前/后对比)

## 数据库迁移(如有)
- [ ] 迁移脚本已独立执行
- [ ] 迁移回滚计划明确
- [ ] 迁移前已备份数据库
- [ ] 迁移后验证数据完整性

## 发布记录
- [ ] 版本/commit/checksum/发布时间已记录
- [ ] 执行人/审批人已记录
- [ ] 发布日志包含各步骤结果
- [ ] 发布失败有回滚记录

❌ 反例:失败后找不到上一个版本;health 未通过仍切流;缺少冒烟脚本

---

# 完整示例(EXAMPLES)

真实可用的部署脚本与检查清单示例,开箱即可复用/改造。

## 1. 一键发布脚本(deploy/release.sh)

```bash
#!/bin/bash
set -e

RELEASE_DIR="/srv/apps/cms/releases/$(date +%Y-%m-%d-%H%M)"
SHARED_DIR="/srv/apps/cms/shared"
CURRENT_DIR="/srv/apps/cms/current"

echo "=== Step 1: Create release directory ==="
mkdir -p "$RELEASE_DIR"

echo "=== Step 2: Upload build artifact ==="
scp build.tar.gz user@server:"$RELEASE_DIR/"

echo "=== Step 3: Extract and install dependencies ==="
ssh user@server << EOF
  cd "$RELEASE_DIR"
  tar -xzf build.tar.gz
  ln -s "$SHARED_DIR/.env" .env
  ln -s "$SHARED_DIR/logs" logs
  npm ci --production
EOF

echo "=== Step 4: Health check ==="
ssh user@server << EOF
  cd "$RELEASE_DIR"
  pm2 start deploy/pm2.config.cjs
  sleep 10
  curl -f http://localhost:8080/health || exit 1
EOF

echo "=== Step 5: Switch current link ==="
ssh user@server << EOF
  rm -f "$CURRENT_DIR"
  ln -s "$RELEASE_DIR" "$CURRENT_DIR"
  pm2 reload deploy/pm2.config.cjs
EOF

echo "=== Step 6: Post-release smoke test ==="
npm run test:smoke

echo "=== Step 7: Record release ==="
echo "$(date) - Released $RELEASE_DIR (commit: $(git rev-parse HEAD))" >> release.log

echo "=== Release completed successfully! ==="
```

## 2. 回滚脚本(deploy/rollback.sh)

```bash
#!/bin/bash
set -e

CURRENT_DIR="/srv/apps/cms/current"
RELEASES_DIR="/srv/apps/cms/releases"

echo "=== Finding previous release ==="
PREVIOUS=$(ls -t "$RELEASES_DIR" | sed -n '2p')

if [ -z "$PREVIOUS" ]; then
  echo "Error: No previous release found"
  exit 1
fi

echo "=== Rolling back to $PREVIOUS ==="
ssh user@server << EOF
  rm -f "$CURRENT_DIR"
  ln -s "$RELEASES_DIR/$PREVIOUS" "$CURRENT_DIR"
  pm2 reload deploy/pm2.config.cjs
  sleep 10
  curl -f http://localhost:8080/health || exit 1
EOF

echo "=== Rollback completed successfully! ==="
echo "$(date) - Rolled back to $PREVIOUS" >> rollback.log
```

## 3. 发布后冒烟测试(tests/e2e/smoke/post-release.spec.ts)

```typescript
import { test, expect } from '@playwright/test';

test.describe('发布后冒烟测试', () => {
  test('健康检查通过', async ({ request }) => {
    const res = await request.get('http://cms.example.com/health');
    expect(res.ok()).toBeTruthy();
    const data = await res.json();
    expect(data.status).toBe('ok');
  });

  test('首页可访问', async ({ page }) => {
    await page.goto('http://cms.example.com');
    await expect(page).toHaveTitle(/CMS/);
  });

  test('登录可用', async ({ page }) => {
    await page.goto('http://cms.example.com/login');
    await page.fill('input[name="email"]', 'admin@test.com');
    await page.fill('input[name="password"]', 'Test1234!');
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL(/dashboard/);
  });

  test('核心API可用', async ({ request }) => {
    const res = await request.get('http://cms.example.com/api/v1/content-types', {
      headers: { Authorization: 'Bearer test-token' }
    });
    expect(res.ok()).toBeTruthy();
  });
});
```

## 4. 发布检查清单(deploy/release-checklist.md)

```markdown
# 发布检查清单 - CMS v1.0.0

## 发布前检查
- [ ] 所有测试通过(UT + E2E)
- [ ] Reviewer 审查通过
- [ ] QA 验收通过
- [ ] 磁盘空间 > 20%
- [ ] 数据库备份完成(如有迁移)
- [ ] 回滚脚本测试通过

## 发布中检查
- [ ] 构建产物 checksum 验证
- [ ] 依赖安装成功
- [ ] 健康检查通过(`/health` 返回 200)
- [ ] PM2 reload 无错误
- [ ] 发布后冒烟测试通过

## 发布后检查
- [ ] 监控指标正常(CPU/Mem/QPS/P95)
- [ ] 错误率无激增
- [ ] 日志无异常
- [ ] 用户反馈正常

## 回滚准备
- [ ] 上一个稳定版本: 2025-10-28-1100
- [ ] 回滚脚本: `deploy/rollback.sh`
- [ ] 回滚时间: ≤ 3 分钟
- [ ] 回滚联系人: ops@example.com
```

## 5. 任务卡示例(CMS-D-002)

```json
{
  "taskId": "CMS-D-002",
  "title": "编写一键发布与回滚脚本",
  "department": "Deploy",
  "createdByRole": "Planner",
  "description": "编写一键发布脚本(release.sh)与回滚脚本(rollback.sh),支持零停机部署、健康检查、发布后冒烟。回滚时间 ≤ 3 分钟。",
  "acceptanceCriteria": [
    "发布脚本包含 7 步流程(打包/上传/解压/安装/健康检查/切换/冒烟)",
    "回滚脚本可在 ≤ 3 分钟内恢复上一个稳定版本",
    "发布后冒烟测试覆盖健康检查/首页/登录/核心API"
  ],
  "technicalRequirements": [
    "编写 deploy/release.sh",
    "编写 deploy/rollback.sh",
    "编写 tests/e2e/smoke/post-release.spec.ts",
    "编写 deploy/release-checklist.md"
  ],
  "dependencies": ["CMS-B-012", "CMS-F-008"],
  "estimatedHours": 8,
  "priority": "P0",
  "tags": ["deploy", "ops"],
  "deliverables": [
    "deploy/release.sh",
    "deploy/rollback.sh",
    "tests/e2e/smoke/post-release.spec.ts",
    "deploy/release-checklist.md"
  ],
  "aiPromptSuggestion": {
    "system": "你是 CodeBuddy Deploy,擅长 PM2 + Nginx + 宝塔部署。",
    "user": "请编写一键发布脚本(7步流程)与回滚脚本(≤3分钟),支持零停机部署、健康检查、发布后冒烟。"
  },
  "reviewPolicy": {
    "requiresReview": true,
    "reviewers": ["Reviewer"]
  },
  "qaPolicy": {
    "requiresQA": true,
    "testingScope": ["Smoke"]
  },
  "needsCoordination": [
    "Backend: 提供健康检查接口 /health",
    "QA: 提供发布后冒烟脚本"
  ],
  "status": "Ready"
}
```

## 6. 错误示例(不合格)

❌ **无回滚脚本**:
```bash
# 只有发布脚本,没有回滚脚本
# 发布失败后无法快速恢复
```

❌ **直接 pm2 restart 而不检查健康**:
```bash
pm2 restart all  # 不检查健康,可能导致服务不可用
```

❌ **以 root 启动 Node**:
```bash
sudo pm2 start app.js  # 安全风险
```

❌ **.env 混在发布包里,泄露风险**:
```bash
tar -czf build.tar.gz . # 包含 .env 文件
```

---

**严格遵守以上规范,确保部署运维高质量交付!**

