docs: Git托管+一键部署实现计划(6任务:卫生/推送/deploy.sh/CLAUDE.md/验证/收尾)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
# 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: 核实 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 3: 确认 `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
|
||||
|
||||
# ---- 配置(环境变量可覆盖)----
|
||||
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 "✓ $*"; }
|
||||
|
||||
# ---- 参数解析 ----
|
||||
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
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
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' 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}' 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}}'")"
|
||||
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)
|
||||
[ "$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 打包"
|
||||
(cd "$REPO_DIR" && tar -czf /tmp/keji-server.tgz \
|
||||
--exclude='__pycache__' --exclude='.pytest_cache' --exclude='db.sqlite3' 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` 需在脚本开头(`set -euo pipefail` 之后)加 `SECONDS=0` 重置;落盘即上方最终形态,勿再引入管道判断测试退避的写法(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,日志留档,**未发生任何传输**。
|
||||
|
||||
再注入一次工作区脏检查(`touch /tmp/x && git add --intent-to-add 任意文件` 太绕——直接改一个已跟踪文件加空行)验证"工作区不干净 → 拒绝部署",验完还原。
|
||||
|
||||
- [ ] **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)、部署日志位置
|
||||
|
||||
---
|
||||
|
||||
## 风险提示(执行者必读)
|
||||
|
||||
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);服务器侧命令全部经既有跳板链,不新增任何端口暴露。
|
||||
Reference in New Issue
Block a user