diff --git a/.cursor/rules/engineering-workflow.mdc b/.cursor/rules/engineering-workflow.mdc new file mode 100644 index 0000000..815ca6e --- /dev/null +++ b/.cursor/rules/engineering-workflow.mdc @@ -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 +``` + +如需临时绕过,只能在用户明确批准后使用项目文档中记录的显式环境变量。 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fb64db5 --- /dev/null +++ b/AGENTS.md @@ -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`。 +- 共享用户、计费、空间额度、策略同步相关改动必须保留业务验收记录。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4ec8555 --- /dev/null +++ b/CLAUDE.md @@ -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 +``` + +如果规则与口头指令冲突,先暂停并确认,不要绕过发布闸门。 diff --git a/ENGINEERING_WORKFLOW_RULES.md b/ENGINEERING_WORKFLOW_RULES.md index c29e90c..a2ac7ce 100644 --- a/ENGINEERING_WORKFLOW_RULES.md +++ b/ENGINEERING_WORKFLOW_RULES.md @@ -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` 的结果为准。 diff --git a/PRODUCTION_RELEASE_RULES.md b/PRODUCTION_RELEASE_RULES.md index d75209f..130da61 100644 --- a/PRODUCTION_RELEASE_RULES.md +++ b/PRODUCTION_RELEASE_RULES.md @@ -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、跳过发布包的理由。 diff --git a/scripts/check-branch-baseline.sh b/scripts/check-branch-baseline.sh new file mode 100755 index 0000000..a67328c --- /dev/null +++ b/scripts/check-branch-baseline.sh @@ -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})." diff --git a/scripts/check-release-ready.sh b/scripts/check-release-ready.sh new file mode 100755 index 0000000..00ccd8e --- /dev/null +++ b/scripts/check-release-ready.sh @@ -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}." diff --git a/scripts/new-branch.sh b/scripts/new-branch.sh new file mode 100755 index 0000000..4a40f75 --- /dev/null +++ b/scripts/new-branch.sh @@ -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 + +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}." diff --git a/scripts/release-prod.sh b/scripts/release-prod.sh index 64fb65a..bdaabe3 100755 --- a/scripts/release-prod.sh +++ b/scripts/release-prod.sh @@ -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