chore: enforce standard branch release workflow

This commit is contained in:
john
2026-07-01 10:50:48 +08:00
parent f9a7584ad4
commit aa6a029530
9 changed files with 323 additions and 9 deletions
+28
View File
@@ -0,0 +1,28 @@
---
description: 标准化分支、测试、发布闸门(始终应用)
alwaysApply: true
---
# 标准化开发测试发布闸门
开始任何开发、修复、发布前,先读:
- `ENGINEERING_WORKFLOW_RULES.md`
- `PRODUCTION_RELEASE_RULES.md`
- `AGENTS.md`
必须遵守:
- 新建分支必须基于最新 `origin/main`,推荐执行 `bash scripts/new-branch.sh feature/xxx`
- 发布前必须重新检查当前分支是否落后 `origin/main`
- 发布来源必须是干净、可追溯的 Git commit
- 禁止从脏工作区、detached HEAD、落后主线的分支发布
- 禁止直接 `rsync` 到 `103/105` 或 SSH 到生产手改源码
发布前必须执行:
```bash
bash scripts/check-release-ready.sh
```
如需临时绕过,只能在用户明确批准后使用项目文档中记录的显式环境变量。
+21
View File
@@ -0,0 +1,21 @@
# Agent 协作说明(Codex / Cloud / Cursor / Claude / 其它 AI 工具通用)
本文件供所有在仓库内工作的 AI 编码助手阅读。仓库级开发、测试、发布规范以 [ENGINEERING_WORKFLOW_RULES.md](ENGINEERING_WORKFLOW_RULES.md) 为准;生产发布规范以 [PRODUCTION_RELEASE_RULES.md](PRODUCTION_RELEASE_RULES.md) 为准。
## 必读:分支与发布闸门
1. 新建分支前必须先同步远端主线,推荐执行 `bash scripts/new-branch.sh feature/xxx`
2. 发布前必须重新检查当前分支是否落后 `origin/main`
3. 发布来源必须是干净、可追溯的 Git commit。
4. 禁止从脏工作区、detached HEAD、落后主线的分支发布。
5. 发布前必须执行:
```bash
bash scripts/check-release-ready.sh
```
## 仓库边界
- `memind_adm` 可以独立开发,但共享用户、权限、策略、技能、计费体系必须继续复用 `Memind` 主实现。
- 发版须 Git commit,禁止本机直 `rsync``103/105`
- 共享用户、计费、空间额度、策略同步相关改动必须保留业务验收记录。
+21
View File
@@ -0,0 +1,21 @@
# Claude 协作说明
本仓库的开发、测试、发布规范与 `AGENTS.md` 同源。开始任何代码修改或发布操作前,先读:
1. [ENGINEERING_WORKFLOW_RULES.md](ENGINEERING_WORKFLOW_RULES.md)
2. [PRODUCTION_RELEASE_RULES.md](PRODUCTION_RELEASE_RULES.md)
3. [AGENTS.md](AGENTS.md)
## 强制规则
- 新建分支必须基于最新 `origin/main`
- 发布前必须重新检查当前分支是否落后 `origin/main`
- 发布来源必须是干净、可追溯的 Git commit。
- 禁止从脏工作区、detached HEAD、落后主线的分支发布。
- 发布前必须执行:
```bash
bash scripts/check-release-ready.sh
```
如果规则与口头指令冲突,先暂停并确认,不要绕过发布闸门。
+40 -4
View File
@@ -7,10 +7,38 @@
## 2. 开发约束
1. 每次开发都必须形成本地 Git commit。
2. 一个 commit 只解决一类问题,避免把功能、环境、运维脚本混在一起。
3. 不允许长期堆积“只有自己知道用途”的未提交改动。
4. `.env`、账号、密钥、日志、pid 不进入 Git。
1. 新建开发分支前必须同步远端主线。推荐统一使用:
```bash
bash scripts/new-branch.sh feature/xxx
```
等价手工命令:
```bash
git fetch origin --prune
git switch main
git pull --ff-only
git switch -c feature/xxx
```
也可以直接从远端主线建分支:
```bash
git fetch origin --prune
git switch -c feature/xxx origin/main
```
2. 每次开发都必须形成本地 Git commit。
3. 一个 commit 只解决一类问题,避免把功能、环境、运维脚本混在一起。
4. 不允许长期堆积“只有自己知道用途”的未提交改动。
5. `.env`、账号、密钥、日志、pid 不进入 Git。
6. 分支开发中如果主线继续变化,发布前必须重新对齐:
```bash
git fetch origin --prune
git rebase origin/main
```
## 3. 测试约束
@@ -24,6 +52,13 @@
2. 测试与生产统一走“打包发布”。
3. 发布来源必须是可追溯 commit,不允许从不明工作区直接出包。
4. 发布前必须有备份,发布后必须有健康检查和业务验收。
5. 发布前必须通过统一闸门:
```bash
bash scripts/check-release-ready.sh
```
6. 分支落后 `origin/main`、工作区有未提交或未跟踪改动、处于 detached HEAD、或没有明确批准却从 `main` / `master` 发布,均禁止发版。
## 5. 文档约束
@@ -35,3 +70,4 @@
1. 发布前要求工作区可读:`git status` 不能混入无关改动。
2. 为每次正式发布保留 manifest、备份包路径、验证结果。
3. 涉及数据库、计费、用户空间、鉴权的改动,要额外保留一份业务验收清单。
4. 每个 AI 工具都必须先读本文件;自动化工具、Git hook、发布脚本以 `scripts/check-release-ready.sh` 的结果为准。
+6 -5
View File
@@ -4,8 +4,9 @@
2. `memind_adm` 生产发布唯一合法路径是:本地已提交代码 -> 本地生成发布包 -> 上传 `103` -> `103` 全量备份当前 `memind_adm` -> 解包到 release 目录 -> 安装依赖/构建 -> 原子切换 live 目录 -> 重启和健康检查。
3. `scripts/release-prod.sh` 是当前唯一允许的生产更新入口,`scripts/rsync_to_server.sh` 只保留为禁用提示,不得再用于发布。
4. 发布包不得携带运行态资产;`.env`、日志、pid 文件、`.mindops/` 等只能从线上现有 live 目录继承。
5. 每次生产发布前必须先做全量备份;发布失败必须自动回滚到切换前的 live 目录
6. 发布清单必须记录:本地 commit、分支、发布时间、发布编号、是否含额外手工环境变更
7. 生产验证至少包含 `http://127.0.0.1:8085/health``http://127.0.0.1:5174/` 的成功检查,并补充本次功能对应的业务路径验收
8. 共享用户体系、计费体系、策略体系、空间额度改动上线后,必须做一次真实登录和 `/admin-api/users` 的业务校验
9. 生产热修复也不能绕过这套流程;“为了快”不是跳过备份、跳过 commit、跳过发布包的理由
5. 每次生产发布前必须先通过 `bash scripts/check-release-ready.sh`,确认分支不落后 `origin/main`、工作区干净、发布来源可追溯
6. 每次生产发布前必须先做全量备份;发布失败必须自动回滚到切换前的 live 目录
7. 发布清单必须记录:本地 commit、分支、发布时间、发布编号、是否含额外手工环境变更
8. 生产验证至少包含 `http://127.0.0.1:8085/health``http://127.0.0.1:5174/` 的成功检查,并补充本次功能对应的业务路径验收
9. 共享用户体系、计费体系、策略体系、空间额度改动上线后,必须做一次真实登录和 `/admin-api/users` 的业务校验
10. 生产热修复也不能绕过这套流程;“为了快”不是跳过备份、跳过 commit、跳过发布包的理由。
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BASE_REF="${BASE_REF:-}"
SKIP_FETCH=0
usage() {
cat <<'EOF'
Usage:
bash scripts/check-branch-baseline.sh [--skip-fetch]
Checks that the current branch is not behind origin/main.
Environment:
BASE_REF Base ref to compare against. Defaults to origin/main when available.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-fetch) SKIP_FETCH=1 ;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
shift
done
git -C "${ROOT}" rev-parse --is-inside-work-tree >/dev/null
if [[ "${SKIP_FETCH}" -ne 1 ]] && git -C "${ROOT}" remote get-url origin >/dev/null 2>&1; then
git -C "${ROOT}" fetch origin --prune
fi
if [[ -z "${BASE_REF}" ]]; then
for candidate in origin/main origin/master main master; do
if git -C "${ROOT}" rev-parse --verify --quiet "${candidate}" >/dev/null; then
BASE_REF="${candidate}"
break
fi
done
fi
branch="$(git -C "${ROOT}" branch --show-current)"
if [[ -z "${branch}" ]]; then
echo "Baseline check failed: detached HEAD is not allowed for development or release." >&2
exit 1
fi
if ! git -C "${ROOT}" rev-parse --verify --quiet "${BASE_REF}" >/dev/null; then
echo "Baseline check failed: base ref not found: ${BASE_REF}" >&2
exit 1
fi
read -r ahead behind < <(git -C "${ROOT}" rev-list --left-right --count "HEAD...${BASE_REF}")
if [[ "${behind}" -ne 0 ]]; then
echo "Baseline check failed: ${branch} is behind ${BASE_REF} by ${behind} commit(s)." >&2
echo "Run: git fetch origin --prune && git rebase ${BASE_REF}" >&2
exit 1
fi
echo "Baseline check passed: ${branch} is current with ${BASE_REF} (ahead ${ahead})."
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BASE_REF="${BASE_REF:-}"
SKIP_FETCH=0
usage() {
cat <<'EOF'
Usage:
bash scripts/check-release-ready.sh [--skip-fetch]
Blocks releases unless the repository is on a named branch, the branch is not
behind origin/main, and the worktree is clean.
Environment:
BASE_REF Base ref to compare against. Defaults to origin/main when available.
ALLOW_MAIN_RELEASE=1 Allow publishing directly from main/master.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-fetch) SKIP_FETCH=1 ;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
shift
done
git -C "${ROOT}" rev-parse --is-inside-work-tree >/dev/null
branch="$(git -C "${ROOT}" branch --show-current)"
if [[ -z "${branch}" ]]; then
echo "Release check failed: detached HEAD is not a release source." >&2
exit 1
fi
case "${branch}" in
main|master)
if [[ "${ALLOW_MAIN_RELEASE:-0}" != "1" ]]; then
echo "Release check failed: publish from a dedicated release/feature branch, not ${branch}." >&2
echo "Set ALLOW_MAIN_RELEASE=1 only for an explicitly approved mainline release." >&2
exit 1
fi
;;
esac
if [[ -n "$(git -C "${ROOT}" status --porcelain=v1 --untracked-files=all)" ]]; then
echo "Release check failed: worktree has uncommitted or untracked changes." >&2
git -C "${ROOT}" status --short --untracked-files=all >&2
exit 1
fi
if [[ "${SKIP_FETCH}" -eq 1 ]]; then
bash "${ROOT}/scripts/check-branch-baseline.sh" --skip-fetch
else
bash "${ROOT}/scripts/check-branch-baseline.sh"
fi
effective_base="${BASE_REF}"
if [[ -z "${effective_base}" ]]; then
for candidate in origin/main origin/master main master; do
if git -C "${ROOT}" rev-parse --verify --quiet "${candidate}" >/dev/null; then
effective_base="${candidate}"
break
fi
done
fi
head_sha="$(git -C "${ROOT}" rev-parse --short HEAD)"
echo "Release check passed: ${branch} @ ${head_sha} is clean and current with ${effective_base}."
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BASE_REF="${BASE_REF:-}"
usage() {
cat <<'EOF'
Usage:
bash scripts/new-branch.sh <branch-name>
Creates a new development branch from the latest origin/main.
Environment:
BASE_REF Base ref to create from. Defaults to origin/main when available.
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" || $# -ne 1 ]]; then
usage
[[ $# -eq 1 ]] && exit 0 || exit 1
fi
branch="$1"
if [[ -n "$(git -C "${ROOT}" status --porcelain=v1 --untracked-files=all)" ]]; then
echo "New branch check failed: current worktree is not clean." >&2
git -C "${ROOT}" status --short --untracked-files=all >&2
exit 1
fi
if git -C "${ROOT}" remote get-url origin >/dev/null 2>&1; then
git -C "${ROOT}" fetch origin --prune
fi
if [[ -z "${BASE_REF}" ]]; then
for candidate in origin/main origin/master main master; do
if git -C "${ROOT}" rev-parse --verify --quiet "${candidate}" >/dev/null; then
BASE_REF="${candidate}"
break
fi
done
fi
git -C "${ROOT}" rev-parse --verify --quiet "${BASE_REF}" >/dev/null || {
echo "New branch check failed: base ref not found: ${BASE_REF}" >&2
exit 1
}
git -C "${ROOT}" switch -c "${branch}" "${BASE_REF}"
echo "Created ${branch} from ${BASE_REF}."
+7
View File
@@ -85,6 +85,13 @@ need_cmd scp
need_cmd tar
need_cmd shasum
if [[ -x "${ROOT}/scripts/check-release-ready.sh" ]]; then
bash "${ROOT}/scripts/check-release-ready.sh"
else
echo "缺少发布前闸门: ${ROOT}/scripts/check-release-ready.sh" >&2
exit 1
fi
[[ -f "${IGNORE_FILE}" ]] || {
echo "缺少忽略文件: ${IGNORE_FILE}" >&2
exit 1