Files
fangke/docs/superpowers/plans/2026-09-29-git-and-deploy-automation.md
T

360 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Git 托管接入 + 一键部署 — 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 项目推送至自建 git 远程仓 + 固化 `deploy/deploy.sh` 一键部署链(tar 真实路径直落挂载点/失败即停/哈希验证/部署日志),新建 CLAUDE.md 部署约定。
**Architecture:** Part1 git 卫生与推送(零线上影响)→ Part2 bash 部署脚本(固化既有手工链,验证驱动而非单测——脚本的本性是编排)→ Part3 约定落地 → 验证(admin 单目标 → 失败注入 → all 完整,用户已选完整版)。
**Tech Stack:** bash(Git Bash on Windows + ssh/scp 经跳板)、git、pnpm、pytest/vitest 既有套件。
**Spec:** docs/superpowers/specs/2026-09-29-git-and-deploy-automation-design.md
**红线:** 严格不触碰服务器上其他项目(1Panel/OpenResty/其他容器);服务器操作仅限 /opt/cms-cj 与 docker compose tracker-web。
**现状锚点(2026-09-29 实测):**
- 工作区:`M admin/src/assets/images/logo-w.png`、`M admin/src/components/core/base/art-logo/index.vue`、`M admin/src/components/core/views/login/LoginLeftView.vue`、`?? admin/src/assets/images/common/logo.png`、`?? admin/src/assets/images/logo.png`(**一组用户手工 logo 改动,线上已在跑此状态**——此前 admin 部署构建自该工作区)、`?? assets/`(设计稿)、`?? 模拟访客测试页.html`(杂物)
- 分支:m1(当前)/ main;**无 remote**;.gitignore 已含 node_modules/sdk/dist/deploy/data/exports/gpt/ 等;admin/.gitignore(模板自带)已含 dist
- 生产:nginx 挂载 `/opt/cms-cj/admin/dist`、`/opt/cms-cj/sdk/dist`(bind mount);server 重建 `cd /opt/cms-cj/deploy && docker compose up -d --build web`(自动 migrate)
---
### Task 1: Git 卫生处理 + 工作区对齐提交
**Files:**
- Modify: `.gitignore`
- Commit: 上述 5 个 logo 相关文件 + `assets/`
- [ ] **Step 1: .gitignore 追加**(根 .gitignore 尾部):
```gitignore
# 部署脚本日志(首部署前必须忽略——防日志污染工作区卡住下次门禁)
deploy/logs/
# 根目录临时测试页
/模拟访客测试页.html
```
- [ ] **Step 2: 敏感文件扫描**(spec Part 1 要求,推送前必做——m1+main 全历史将进新远程仓):
```bash
git ls-files | grep -iE '\.env$|\.pem$|id_rsa|secret|password' ; echo "exit=$?" # 预期仅 .env.example 类
git ls-files assets/ | head -20 # 目检设计稿清单(6.6MB/11 文件已核,确认无意外内容)
```
- [ ] **Step 3: 核实 logo 改动组内聚后提交**(5 文件是一组:art-logo 改用 common/logo.png、LoginLeftView 引用调整、两个 logo 文件、logo-w 替换——`git diff` 快速确认无其他意外改动混入;**提交前跑 `cd admin && npx vue-tsc --noEmit` 确认引用可解析**):
```bash
git add .gitignore assets/ admin/src/assets/images/ admin/src/components/core/base/art-logo/index.vue admin/src/components/core/views/login/LoginLeftView.vue
git commit -m "chore: 入库设计稿与品牌logo资产,gitignore部署日志与临时页
Co-Authored-By: Claude Code <noreply@anthropic.com>"
```
- [ ] **Step 4: 确认 `git status --porcelain` 为空**(工作区干净——后续推送与部署门禁的前提)
### Task 2: 接入远程仓并推送
- [ ] **Step 1: 添加 remote**:
```bash
git remote add origin http://219.157.255.213:25311/xinyouhe/fangke.git
git remote -v # 核对
```
- [ ] **Step 2: 推送 m1(带 `-u` 建立跟踪)**。**用 `GIT_TERMINAL_PROMPT=0` 防凭证缺失时挂死**:
```bash
GIT_TERMINAL_PROMPT=0 git push -u origin m1
```
Expected: 推送成功。若报认证失败(`terminal prompts disabled` / 401):**停下告知用户**——需用户在本机 Git Credential Manager 配好该服务器凭证(一次性),配好后重试。
- [ ] **Step 3: 推送 main**:`git push origin main`
- [ ] **Step 4: 远程核对**(若 git 服务器有 web 界面让用户看一眼;CLI 侧 `git ls-remote origin | head -5` 确认两分支与最新哈希一致)
---
### Task 3: deploy/deploy.sh(核心交付)
**Files:**
- Create: `deploy/deploy.sh`
- Create: `deploy/logs/.gitkeep`(目录占位,logs 本身被 gitignore——.gitkeep 也忽略则无需,直接 mkdir 即可,脚本自建)
- [ ] **Step 1: 写入完整脚本**:
```bash
#!/usr/bin/env bash
# 客迹一键部署——固化标准部署链(spec: docs/superpowers/specs/2026-09-29-git-and-deploy-automation-design.md)
# 用法: bash deploy/deploy.sh <server|admin|sdk|all> [--skip-tests] [--allow-dirty]
# 红线: 只操作 /opt/cms-cj 与 tracker-web 容器,绝不触碰服务器其他项目
set -euo pipefail
SECONDS=0
# ---- 配置(环境变量可覆盖)----
JUMP_HOST="${JUMP_HOST:-root@8.152.213.143}"
TARGET_HOST="${TARGET_HOST:-root@47.254.176.10}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519}"
REMOTE_DIR="${REMOTE_DIR:-/opt/cms-cj}"
SITE_URL="${SITE_URL:-https://tracker.12418113.xyz}"
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LOG_DIR="$REPO_DIR/deploy/logs"
STAMP="$(date +%Y%m%d-%H%M%S)"
LOG_FILE="$LOG_DIR/deploy-$STAMP.log"
SSH_OPTS=(-i "$SSH_KEY" -o ConnectTimeout=15 -o BatchMode=yes)
SSH=(ssh "${SSH_OPTS[@]}" -J "$JUMP_HOST" "$TARGET_HOST")
SCP=(scp "${SSH_OPTS[@]}" -J "$JUMP_HOST")
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
die() { log "✗ 失败: $*(部署中止,此前已完成的目标保持已部署状态)"; exit 1; }
ok() { log "✓ $*"; }
mkdir -p "$LOG_DIR" # 必须先于参数解析——早期 die() 也要能写日志
# ---- 参数解析 ----
TARGET_CURRENT="preflight"; STEP="-"
[ $# -ge 1 ] || die "用法: bash deploy/deploy.sh <server|admin|sdk|all> [--skip-tests] [--allow-dirty]"
DEPLOY_TARGET="$1"; shift || true
SKIP_TESTS=0; ALLOW_DIRTY=0
for arg in "$@"; do
case "$arg" in
--skip-tests) SKIP_TESTS=1 ;;
--allow-dirty) ALLOW_DIRTY=1 ;;
*) die "未知参数: $arg" ;;
esac
done
case "$DEPLOY_TARGET" in
server|admin|sdk|all) ;;
*) die "目标必须是 server|admin|sdk|all,收到: $DEPLOY_TARGET" ;;
esac
GIT_HASH="$(git -C "$REPO_DIR" rev-parse --short HEAD)"
log "===== 部署开始 target=$DEPLOY_TARGET git=$GIT_HASH ====="
# ---- 前置检查(所有目标必经)----
preflight() {
STEP="前置检查"
if [ "$ALLOW_DIRTY" -eq 0 ] && [ -n "$(git -C "$REPO_DIR" status --porcelain)" ]; then
die "git 工作区不干净(--allow-dirty 可跳过;强烈建议先提交)"
fi
"${SSH[@]}" 'echo ok' >/dev/null 2>&1 || die "跳板/目标服务器 SSH 不可达($JUMP_HOST → $TARGET_HOST)"
ok "工作区干净,SSH 链路可达"
}
# ---- 各目标实现 ----
server_tests() {
[ "$SKIP_TESTS" -eq 1 ] && return 0
STEP="server 测试"
(cd "$REPO_DIR/server" && python -m pytest tracker/tests/ -q >>"$LOG_FILE" 2>&1) \
|| die "pytest 未全过(详见 $LOG_FILE)"
ok "pytest 全过"
}
sdk_tests() {
[ "$SKIP_TESTS" -eq 1 ] && return 0
STEP="sdk 测试"
(cd "$REPO_DIR/sdk" && npx vitest run >>"$LOG_FILE" 2>&1) || die "vitest 未全过"
(cd "$REPO_DIR/sdk" && npx tsc --noEmit >>"$LOG_FILE" 2>&1) || die "tsc 报错"
ok "sdk vitest+tsc 全过"
}
build_admin() {
STEP="admin 构建"
(cd "$REPO_DIR/admin" && NODE_OPTIONS=--max-old-space-size=6144 pnpm build >>"$LOG_FILE" 2>&1) \
|| die "admin 构建失败(本机 OOM 则确认 NODE_OPTIONS 已带)"
ok "admin 构建完成: $(grep -oE 'index-[A-Za-z0-9_-]+\.js' "$REPO_DIR/admin/dist/index.html" | head -1)"
}
build_sdk() {
STEP="sdk 构建"
(cd "$REPO_DIR/sdk" && node build.mjs >>"$LOG_FILE" 2>&1) || die "sdk 构建失败"
local gz; gz=$(gzip -c "$REPO_DIR/sdk/dist/v1.js" | wc -c)
ok "sdk 构建完成: build_id=$(grep -oE '[0-9a-f]{7,9}-2026[0-9]{8}' "$REPO_DIR/sdk/dist/v1.js" | head -1) gzip=${gz}B"
}
transfer() { # $1=包内路径列表(空格分隔,相对 REPO_DIR) $2=远端包名
STEP="传输与落位"
local tarball="/tmp/$2"
(cd "$REPO_DIR" && tar -czf "$tarball" $1) || die "tar 打包失败: $1"
"${SCP[@]}" "$tarball" "$TARGET_HOST:/tmp/" >/dev/null 2>&1 || die "scp 传输失败(经 $JUMP_HOST)"
rm -f "$tarball"
# tar 按真实路径解压——直接落位挂载点(原地合并,无 cp 无暂存)
"${SSH[@]}" "cd $REMOTE_DIR && tar -xzf /tmp/$2 && rm -f /tmp/$2" >/dev/null 2>&1 \
|| die "远端解压失败($REMOTE_DIR)"
ok "已落位: $1"
}
rebuild_web() {
STEP="server 容器重建"
"${SSH[@]}" "cd $REMOTE_DIR/deploy && docker compose up -d --build web" >>"$LOG_FILE" 2>&1 \
|| die "docker compose 重建失败"
sleep 5
local status; status="$("${SSH[@]}" "docker ps --filter name=tracker-web --format '{{.Status}}'")" \
|| die "容器状态查询失败"
echo "$status" | grep -q "^Up" || die "tracker-web 容器状态异常: $status"
ok "tracker-web: $status"
}
verify_site_up() {
STEP="站点探活"
curl -sf -o /dev/null --max-time 20 "$SITE_URL" || die "$SITE_URL 探活失败"
ok "站点 200"
}
verify_admin_hash() {
STEP="admin 哈希验证"
local local_h online_h
local_h=$(sha256sum "$REPO_DIR/admin/dist/index.html" | cut -d' ' -f1)
online_h=$(curl -sf --max-time 20 "$SITE_URL/" | sha256sum | cut -d' ' -f1) \
|| die "线上 index.html 拉取失败"
[ "$local_h" = "$online_h" ] || die "admin index.html 线上哈希与本机产物不一致(部署未生效或缓存)"
ok "admin 哈希一致"
}
verify_sdk_bid() {
STEP="sdk build_id 验证"
local bid; bid="$(grep -oE '[0-9a-f]{7,9}-2026[0-9]{8}' "$REPO_DIR/sdk/dist/v1.js" | head -1)"
[ -n "$bid" ] || die "本机产物无 build_id"
curl -sf --max-time 20 "$SITE_URL/sdk/v1.js" | grep -q "$bid" \
|| die "线上 v1.js 不含新 build_id $bid"
ok "sdk 线上已含 build_id $bid"
}
deploy_server() {
TARGET_CURRENT=server
server_tests
STEP="server 打包"
# 排除清单镜像 server/.dockerignore 本意:Windows venv(97MB)/缓存/本地xdb 绝不上服务器
(cd "$REPO_DIR" && tar -czf /tmp/keji-server.tgz \
--exclude='__pycache__' --exclude='.pytest_cache' --exclude='db.sqlite3' \
--exclude='.venv' --exclude='.cache' --exclude='tracker/data/*.xdb' server/) \
|| die "server 打包失败"
"${SCP[@]}" /tmp/keji-server.tgz "$TARGET_HOST:/tmp/" >/dev/null 2>&1 || die "scp 失败"
rm -f /tmp/keji-server.tgz
"${SSH[@]}" "cd $REMOTE_DIR && tar -xzf /tmp/keji-server.tgz && rm -f /tmp/keji-server.tgz" >/dev/null 2>&1 \
|| die "远端解压失败"
ok "server 源码落位"
rebuild_web
verify_site_up
}
deploy_admin() {
TARGET_CURRENT=admin
build_admin
transfer "admin/dist" "keji-admin.tgz"
verify_admin_hash
}
deploy_sdk() {
TARGET_CURRENT=sdk
sdk_tests
build_sdk
transfer "sdk/dist" "keji-sdk.tgz"
verify_sdk_bid
}
# ---- 编排(all 按契约依赖序: server → admin → sdk)----
preflight
case "$DEPLOY_TARGET" in
server) deploy_server ;;
admin) deploy_admin ;;
sdk) deploy_sdk ;;
all) deploy_server; deploy_admin; deploy_sdk ;;
esac
log "===== 部署完成 target=$DEPLOY_TARGET git=$GIT_HASH 用时 $SECONDS 秒 ====="
```
**实现者注意**:落盘即上方最终形态(含 SECONDS=0/mkdir 前置/tar 全排除清单/die 包装),勿再引入管道判断测试退避的写法(pytest/vitest 退出码即真相,输出重定向进日志)。
- [ ] **Step 2: 语法检查**:`bash -n deploy/deploy.sh`(无输出即通过)
- [ ] **Step 3: 本地无副作用的参数面测试**:`bash deploy/deploy.sh` (无参 → 应打用法并退出 1);`bash deploy/deploy.sh foo` (非法目标 → 退出 1);`bash deploy/deploy.sh admin --bad-flag`(未知参数 → 退出 1)——三条都不应触碰网络
- [ ] **Step 4: 提交**:
```bash
git add deploy/deploy.sh
git commit -m "feat(deploy): 一键部署脚本——tar真实路径直落挂载点/失败即停/哈希验证/部署日志
Co-Authored-By: Claude Code <noreply@anthropic.com>"
```
---
### Task 4: CLAUDE.md 新建 + 记忆更新
**Files:**
- Create: `CLAUDE.md`(仓库根)
- Modify: 我的自动记忆(会话外文件,非仓库)
- [ ] **Step 1: CLAUDE.md**(简短,只放会话必需约定;项目背景大块由用户自己的模板提供):
```markdown
# CLAUDE.md — 客迹(KEJI)项目约定
## 部署(唯一入口)
- 一键部署:`bash deploy/deploy.sh <server|admin|sdk|all> [--skip-tests] [--allow-dirty]`
- 用户说"部署到线上" = 执行上述命令(按改动范围选目标)并汇报验证结果与版本标识
- `all` 顺序固定 server → admin → sdk(SDK 新事件依赖 server 白名单先上线)
- 部署日志:deploy/logs/(gitignore);线上真挂载点 /opt/cms-cj/{admin/dist, sdk/dist}(tar 真实路径直落,禁止 cp/暂存方案)
- 严格不触碰服务器其他项目(1Panel/OpenResty/其他容器)
## 红线(沿袭 spec §4.5 等)
- SDK 任何回调异常不得冒泡宿主页;业务代码禁 v-html(admin README-dev.md)
- 一切改动走 TDD;用中文交流
```
- [ ] **Step 2: 更新自动记忆**(`C:\Users\30997\.claude\projects\d--project-cms-cj\memory\frontend-deploy-target-dir.md`):正文改为指向 `deploy/deploy.sh` 为唯一部署入口 + 保留历史陷阱教训(为何 tar 真实路径直落);`MEMORY.md` 索引行同步改钩子文案。
- [ ] **Step 3: 提交 CLAUDE.md**:
```bash
git add CLAUDE.md
git commit -m "docs: 新建CLAUDE.md——部署唯一入口与红线约定
Co-Authored-By: Claude Code <noreply@anthropic.com>"
```
---
### Task 5: 验证(用户已选完整版;按风险递增序)
- [ ] **Step 1: admin 单目标首验**(零停机路径先通全链):
```bash
bash deploy/deploy.sh admin
```
Expected: 构建→传输→落位→哈希一致四步日志全 ✓,退出码 0;`deploy/logs/` 生成日志文件。
- [ ] **Step 2: 失败注入**(不碰线上——用环境变量覆盖跳板为不可达地址):
```bash
JUMP_HOST=nobody@127.0.0.1 bash deploy/deploy.sh admin; echo "exit=$?"
```
Expected: 前置检查 ✗ SSH 不可达 → 退出码非 0,日志留档,**未发生任何传输**。
再注入工作区脏检查(具体方法):
```bash
echo "" >> CLAUDE.md # 制造未提交改动
bash deploy/deploy.sh admin; echo "exit=$?" # 预期: 工作区不干净 → 拒绝,退出 1,未触碰网络(脏检查先于 SSH 检查)
git checkout -- CLAUDE.md # 还原
```
- [ ] **Step 3: all 完整验证**(含 server 容器秒级重启,用户已确认接受):
```bash
bash deploy/deploy.sh all
```
Expected: pytest→打包→容器 Up→探活→admin 构建/哈希→sdk 测试/build_id 五段全 ✓;线上验证(容器 Up、站点 200、admin 哈希一致、sdk build_id 在线)。
- [ ] **Step 4: 事后观察**:容器日志无异常(`docker logs tracker-web --since 5m` 无 5xx 报错堆栈)、hesucar 采集照常(几分钟后新事件入库抽查)。
---
### Task 6: 收尾推送
- [ ] **Step 1: 推送全部新提交:`git push origin m1`**(deploy.sh/CLAUDE.md/gitignore 等)
- [ ] **Step 2: 汇报**:远程仓两分支状态、脚本用法示例、验证结果摘要(admin 首验/失败注入/all)、部署日志位置;**提醒用户**:其会话粘贴的"项目背景"模板中"部署策略"段更新为指向 `bash deploy/deploy.sh`(spec Part 3 第 2 条,用户侧自行更新)
---
## 风险提示(执行者必读)
1. **Task 2 凭证**:http 远程首次 push 可能需用户侧 Git Credential Manager 配凭证——`GIT_TERMINAL_PROMPT=0` 会快速失败而非挂死,失败即停并告知用户,不得绕过。
2. **Task 5 Step 3 的 server 重启窗口**:秒级采集 API 中断为已知且用户已接受;SDK 重试吸收;若重建后容器异常(非 Up),立即 `docker logs tracker-web` 取因并汇报,不盲目重试。
3. **tar 落位只增不删**:admin/dist 旧哈希文件会累积(index.html 只指向新哈希,无害);不做的清理不要顺手加。
4. 脚本以 `bash deploy/deploy.sh` 调用(Windows 下不依赖 exec bit);服务器侧命令全部经既有跳板链,不新增任何端口暴露。