chore: enforce standard branch release workflow
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
如需临时绕过,只能在用户明确批准后使用项目文档中记录的显式环境变量。
|
||||
@@ -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`。
|
||||
- 共享用户、计费、空间额度、策略同步相关改动必须保留业务验收记录。
|
||||
@@ -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
|
||||
```
|
||||
|
||||
如果规则与口头指令冲突,先暂停并确认,不要绕过发布闸门。
|
||||
@@ -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` 的结果为准。
|
||||
|
||||
@@ -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、跳过发布包的理由。
|
||||
|
||||
Executable
+70
@@ -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})."
|
||||
Executable
+79
@@ -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}."
|
||||
Executable
+51
@@ -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}."
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user