✨ feat(playbook): add install_mode deployment config
BREAKING CHANGE: [vendor] and playbook.deploy_root are no longer supported. Use [playbook].install_mode and playbook.playbook_root instead.
This commit is contained in:
parent
2c3a559a30
commit
f2ab57b39f
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
name: 🧪 Playbook 测试套件
|
||||
|
||||
on:
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
|
@ -54,7 +55,8 @@ jobs:
|
|||
|
||||
git clone "$REPO_URL" "$REPO_DIR"
|
||||
|
||||
if git -C "$REPO_DIR" cat-file -e "${TARGET_SHA}^{commit}" 2>/dev/null; then
|
||||
if git -C "$REPO_DIR" cat-file -e \
|
||||
"${TARGET_SHA}^{commit}" 2>/dev/null; then
|
||||
git -C "$REPO_DIR" checkout -f "$TARGET_SHA"
|
||||
else
|
||||
if [ -n "$TARGET_REF" ]; then
|
||||
|
|
@ -90,16 +92,11 @@ jobs:
|
|||
set -euo pipefail
|
||||
|
||||
echo "========================================"
|
||||
echo "🧪 Python CLI 测试"
|
||||
echo "🧪 Python 测试"
|
||||
echo "========================================"
|
||||
|
||||
cd "$REPO_DIR"
|
||||
python3 -m unittest discover -s tests/cli -v
|
||||
|
||||
echo "========================================"
|
||||
echo "🧪 Python 扩展测试"
|
||||
echo "========================================"
|
||||
|
||||
echo "覆盖:CLI、subtree/snapshot 部署路线、模板同步、文档一致性"
|
||||
python3 -m unittest discover -s tests -p "test_*.py" -v
|
||||
|
||||
echo "========================================"
|
||||
|
|
|
|||
41
README.md
41
README.md
|
|
@ -58,6 +58,8 @@ python scripts/playbook.py -config playbook.toml
|
|||
```toml
|
||||
[playbook]
|
||||
project_root = "/path/to/project"
|
||||
playbook_root = "docs/standards/playbook"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_rules]
|
||||
# force = true # 可选
|
||||
|
|
@ -81,12 +83,12 @@ project_name = "MyProject"
|
|||
|
||||
```bash
|
||||
# spec 写完后
|
||||
python <deploy_root>/scripts/playbook.py \
|
||||
python <playbook_root>/scripts/playbook.py \
|
||||
-record-spec docs/superpowers/specs/<topic>-design.md \
|
||||
-progress memory-bank/progress.md
|
||||
|
||||
# plan 写完后
|
||||
python <deploy_root>/scripts/playbook.py \
|
||||
python <playbook_root>/scripts/playbook.py \
|
||||
-record-plan docs/superpowers/plans/<topic>.md \
|
||||
-progress memory-bank/progress.md
|
||||
```
|
||||
|
|
@ -199,10 +201,11 @@ TSL 相关问题直接查阅 `rulesets/tsl/index.md` 与 `docs/tsl/`。
|
|||
先区分三个路径概念:
|
||||
|
||||
- `project_root`:目标项目根目录。
|
||||
- `deploy_root`:相对于 `project_root` 的项目内目标目录。
|
||||
- 外部 clone 出来的 Playbook 路径(如 `/opt/playbook`):只是执行部署脚本的位置,不是部署目标。
|
||||
- `playbook_root`:相对于 `project_root` 的项目内 Playbook 根目录。
|
||||
- `install_mode`:`subtree` 表示 Playbook 已由 git subtree 放在项目内;`snapshot` 表示从外部 clone 安装裁剪快照。
|
||||
- 外部 clone 出来的 Playbook 路径(如 `/opt/playbook`):只是执行安装脚本的位置,不是项目内 Playbook 根目录。
|
||||
|
||||
以 TSL 为例,Playbook 在项目内的默认部署根是 `docs/standards/playbook`;如果你把 `deploy_root` 改成 `custom/playbook`,则部署结果会落到 `<project_root>/custom/playbook`,文档和脚本入口也会跟着变成 `custom/playbook/docs/...`、`custom/playbook/scripts/...`。
|
||||
以 TSL 为例,Playbook 在项目内的默认根是 `docs/standards/playbook`;如果你把 `playbook_root` 改成 `custom/playbook`,则快照会落到 `<project_root>/custom/playbook`,文档和脚本入口也会跟着变成 `custom/playbook/docs/...`、`custom/playbook/scripts/...`。
|
||||
|
||||
#### 方式一:`git subtree`
|
||||
|
||||
|
|
@ -214,6 +217,8 @@ git subtree add --prefix docs/standards/playbook https://git.mytsl.cn/csh/playbo
|
|||
cat <<'EOF' > playbook.toml
|
||||
[playbook]
|
||||
project_root = "."
|
||||
playbook_root = "docs/standards/playbook"
|
||||
install_mode = "subtree"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
|
|
@ -249,6 +254,8 @@ git commit -m ":package: deps(playbook): add tsl standards"
|
|||
# playbook.toml
|
||||
[playbook]
|
||||
project_root = "."
|
||||
playbook_root = "docs/standards/playbook"
|
||||
install_mode = "subtree"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl", "cpp"]
|
||||
|
|
@ -277,19 +284,17 @@ git commit -m ":package: deps(playbook): add tsl standards"
|
|||
git clone https://git.mytsl.cn/csh/playbook.git /opt/playbook
|
||||
```
|
||||
|
||||
2. 在目标项目根创建 `playbook.toml`,并用 `deploy_root` 指定项目内的部署根。例如:
|
||||
|
||||
2. 在目标项目根创建 `playbook.toml`,并用 `playbook_root` 指定项目内的 Playbook 根。例如:
|
||||
- `project_root` 写目标项目根目录。
|
||||
- `deploy_root` 写目标项目内的相对路径。
|
||||
- 不要把外部 clone 的路径(如 `/opt/playbook`)写进 `deploy_root`;那只是你执行脚本的位置。
|
||||
- `playbook_root` 写目标项目内的相对路径。
|
||||
- `install_mode = "snapshot"` 表示由外部 clone 安装项目内快照。
|
||||
- 不要把外部 clone 的路径(如 `/opt/playbook`)写进 `playbook_root`;那只是你执行脚本的位置。
|
||||
|
||||
```toml
|
||||
[playbook]
|
||||
project_root = "."
|
||||
deploy_root = "custom/playbook"
|
||||
|
||||
[vendor]
|
||||
langs = ["tsl"]
|
||||
playbook_root = "custom/playbook"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
|
|
@ -308,10 +313,10 @@ git commit -m ":package: deps(playbook): add tsl standards"
|
|||
|
||||
说明:
|
||||
|
||||
- 这里的 `[vendor]` 是“把 Playbook 快照部署进目标项目”的执行步骤,不是第三种正式部署路线。
|
||||
- `deploy_root` 永远表示目标项目内的部署目录;它不是外部 clone 出来的 Playbook 仓库路径。
|
||||
- 外部 clone 场景下必须显式填写 `deploy_root`;脚本不会替你补默认部署目录。
|
||||
- 如果 `deploy_root = "custom/playbook"`,部署后的项目内入口会是 `custom/playbook/scripts/playbook.py`、`custom/playbook/docs/index.md`。
|
||||
- `install_mode = "snapshot"` 会先把 Playbook 裁剪快照安装到目标项目的 `playbook_root`,再执行后续同步动作。
|
||||
- `playbook_root` 永远表示目标项目内的 Playbook 根;它不是外部 clone 出来的 Playbook 仓库路径。
|
||||
- 外部 clone 场景下必须显式填写 `playbook_root`;脚本不会替你补默认项目内根目录。
|
||||
- 如果 `playbook_root = "custom/playbook"`,部署后的项目内入口会是 `custom/playbook/scripts/playbook.py`、`custom/playbook/docs/index.md`。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -347,7 +352,7 @@ git commit -m ":package: deps(playbook): add tsl standards"
|
|||
├── .gitattributes # 行尾/文本规范
|
||||
├── AGENTS.md # Codex 入口(由 playbook 自动生成/更新)
|
||||
├── CLAUDE.md # Claude Code 入口(自动注入 @AGENTS.md)
|
||||
├── <deploy_root>/ # 本 Playbook 在项目内的部署根(默认 docs/standards/playbook)
|
||||
├── <playbook_root>/ # 本 Playbook 在项目内的根(默认 docs/standards/playbook)
|
||||
│ ├── docs/
|
||||
│ ├── rulesets/
|
||||
│ ├── scripts/
|
||||
|
|
|
|||
|
|
@ -110,10 +110,10 @@ no_backup = true
|
|||
如果你的项目已经把本 Playbook 部署到项目内,则在目标项目里执行:
|
||||
|
||||
```bash
|
||||
python <deploy_root>/scripts/playbook.py -config playbook.toml
|
||||
python <playbook_root>/scripts/playbook.py -config playbook.toml
|
||||
```
|
||||
|
||||
其中 `<deploy_root>` 默认为 `docs/standards/playbook`。
|
||||
其中 `<playbook_root>` 默认为 `docs/standards/playbook`。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ python <deploy_root>/scripts/playbook.py -config playbook.toml
|
|||
- Python:`docs/python/style_guide.md`、`docs/python/tooling.md`、`docs/python/configuration.md`
|
||||
|
||||
若你的项目把本 Playbook 部署到项目内,文档根路径为
|
||||
`<deploy_root>/docs/...`;其中 `<deploy_root>` 默认为 `docs/standards/playbook`,也可以按项目配置改成 `custom/playbook` 等自定义目录。
|
||||
`<playbook_root>/docs/...`;其中 `<playbook_root>` 默认为 `docs/standards/playbook`,也可以按项目配置改成 `custom/playbook` 等自定义目录。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,10 @@
|
|||
# 配置文件所在目录默认作为 project_root。
|
||||
|
||||
[playbook]
|
||||
# project_root = "." # 可选:目标项目根目录
|
||||
# deploy_root = "docs/standards/playbook" # 项目内已部署时可省略;从外部 clone 执行时必填;值始终是相对于 project_root 的项目内路径,不是外部 clone 的 playbook 路径
|
||||
# claude_md = "CLAUDE.md" # 可选:CLAUDE.md 位置(默认自动检测:根目录 → .claude/CLAUDE.md → 自动创建)
|
||||
|
||||
[vendor]
|
||||
# 当从外部 clone 的 playbook 向项目内部署快照时启用
|
||||
# 外部 clone 路径只用于执行脚本;快照仍会写入 <project_root>/<deploy_root>
|
||||
# langs = ["tsl"] # 可选:默认仅 tsl
|
||||
# project_root = "." # 可选:目标项目根目录
|
||||
# playbook_root = "docs/standards/playbook" # 项目内 Playbook 根;相对于 project_root;不是外部 clone 的 playbook 路径
|
||||
# install_mode = "subtree" # subtree|snapshot;subtree 表示已在项目内,snapshot 表示从外部 clone 安装快照
|
||||
# claude_md = "CLAUDE.md" # 可选:CLAUDE.md 位置(默认自动检测:根目录 → .claude/CLAUDE.md → 自动创建)
|
||||
|
||||
[sync_rules]
|
||||
# 同步 AGENT_RULES.md(配置节存在即启用)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ except ModuleNotFoundError: # Python < 3.11
|
|||
tomllib = None
|
||||
|
||||
ORDER = [
|
||||
"vendor",
|
||||
"sync_rules",
|
||||
"sync_memory_bank",
|
||||
"sync_prompts",
|
||||
|
|
@ -29,7 +28,14 @@ MAIN_LOOP_SPEC = importlib.util.spec_from_file_location("playbook_main_loop", MA
|
|||
assert MAIN_LOOP_SPEC and MAIN_LOOP_SPEC.loader
|
||||
MAIN_LOOP = importlib.util.module_from_spec(MAIN_LOOP_SPEC)
|
||||
MAIN_LOOP_SPEC.loader.exec_module(MAIN_LOOP)
|
||||
PATH_CONFIG_KEYS = {"project_root", "deploy_root", "agents_home", "codex_home", "skill_link"}
|
||||
PATH_CONFIG_KEYS = {
|
||||
"project_root",
|
||||
"playbook_root",
|
||||
"deploy_root",
|
||||
"agents_home",
|
||||
"codex_home",
|
||||
"skill_link",
|
||||
}
|
||||
DOCS_INDEX_SECTION_HEADINGS = {
|
||||
"common": "## 跨语言(common)",
|
||||
"tsl": "## TSL(tsl/tsf)",
|
||||
|
|
@ -275,13 +281,13 @@ def normalize_relative_dir(raw: object, label: str) -> str:
|
|||
return "." if normalized == "" else normalized
|
||||
|
||||
|
||||
def join_deploy_subpath(root: str, child: str) -> str:
|
||||
def join_playbook_subpath(root: str, child: str) -> str:
|
||||
if root in ("", "."):
|
||||
return child.lstrip("/")
|
||||
return f"{root.rstrip('/')}/{child.lstrip('/')}"
|
||||
|
||||
|
||||
def resolve_in_project_deploy_root(project_root: Path) -> str | None:
|
||||
def resolve_in_project_playbook_root(project_root: Path) -> str | None:
|
||||
try:
|
||||
rel = PLAYBOOK_ROOT.resolve().relative_to(project_root.resolve())
|
||||
if str(rel) != ".":
|
||||
|
|
@ -291,9 +297,8 @@ def resolve_in_project_deploy_root(project_root: Path) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def config_requires_deploy_root(config: dict) -> bool:
|
||||
def config_uses_playbook_root(config: dict) -> bool:
|
||||
for key in (
|
||||
"vendor",
|
||||
"sync_rules",
|
||||
"sync_memory_bank",
|
||||
"sync_prompts",
|
||||
|
|
@ -305,46 +310,74 @@ def config_requires_deploy_root(config: dict) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def resolve_configured_deploy_root(config: dict, project_root: Path) -> str:
|
||||
def validate_removed_config(config: dict) -> None:
|
||||
if "vendor" in config:
|
||||
raise ValueError(
|
||||
"[vendor] is no longer supported; use [playbook].install_mode = "
|
||||
'"snapshot" and configure languages in [sync_standards]'
|
||||
)
|
||||
|
||||
playbook_config = config.get("playbook", {})
|
||||
if not isinstance(playbook_config, dict):
|
||||
raise ValueError("[playbook] must be a table")
|
||||
|
||||
if "deploy_root" in playbook_config:
|
||||
raise ValueError(
|
||||
"playbook.deploy_root is no longer supported; use playbook.playbook_root"
|
||||
)
|
||||
if "intall_mode" in playbook_config:
|
||||
raise ValueError(
|
||||
"playbook.intall_mode is not supported; use playbook.install_mode"
|
||||
)
|
||||
|
||||
|
||||
def resolve_install_mode(config: dict) -> str:
|
||||
playbook_config = config.get("playbook", {})
|
||||
raw = "subtree"
|
||||
if (
|
||||
isinstance(playbook_config, dict)
|
||||
and playbook_config.get("install_mode") is not None
|
||||
):
|
||||
raw = playbook_config.get("install_mode")
|
||||
mode = str(raw).strip().lower()
|
||||
if mode not in ("subtree", "snapshot"):
|
||||
raise ValueError(
|
||||
"playbook.install_mode must be one of: subtree, snapshot"
|
||||
)
|
||||
return mode
|
||||
|
||||
|
||||
def resolve_configured_playbook_root(config: dict, project_root: Path) -> str:
|
||||
playbook_config = config.get("playbook", {})
|
||||
raw = None
|
||||
if isinstance(playbook_config, dict):
|
||||
raw = playbook_config.get("deploy_root")
|
||||
vendor_config = config.get("vendor", {})
|
||||
if isinstance(vendor_config, dict) and vendor_config.get("target_dir") is not None:
|
||||
raise ValueError(
|
||||
"vendor.target_dir is no longer supported; use [playbook].deploy_root"
|
||||
)
|
||||
raw = playbook_config.get("playbook_root")
|
||||
if raw is not None and str(raw).strip():
|
||||
return normalize_relative_dir(raw, "deploy_root")
|
||||
return normalize_relative_dir(raw, "playbook_root")
|
||||
|
||||
in_project_deploy_root = resolve_in_project_deploy_root(project_root)
|
||||
if in_project_deploy_root is not None:
|
||||
return in_project_deploy_root
|
||||
in_project_playbook_root = resolve_in_project_playbook_root(project_root)
|
||||
if in_project_playbook_root is not None:
|
||||
return in_project_playbook_root
|
||||
|
||||
if config_requires_deploy_root(config):
|
||||
if resolve_install_mode(config) == "snapshot" or config_uses_playbook_root(config):
|
||||
raise ValueError(
|
||||
"playbook.deploy_root is required when running from an external clone; "
|
||||
"set it to the target project's relative deployment path"
|
||||
"playbook.playbook_root is required when running from an external clone; "
|
||||
"set it to the target project's relative Playbook path"
|
||||
)
|
||||
|
||||
return "docs/standards/playbook"
|
||||
|
||||
|
||||
def resolve_deploy_root(context: dict) -> str:
|
||||
project_root: Path = context["project_root"]
|
||||
in_project_deploy_root = resolve_in_project_deploy_root(project_root)
|
||||
if in_project_deploy_root is not None:
|
||||
return in_project_deploy_root
|
||||
return context["deploy_root"]
|
||||
def resolve_playbook_root(context: dict) -> str:
|
||||
return context["playbook_root"]
|
||||
|
||||
|
||||
def resolve_docs_prefix(context: dict) -> str:
|
||||
return join_deploy_subpath(resolve_deploy_root(context), "docs")
|
||||
return join_playbook_subpath(resolve_playbook_root(context), "docs")
|
||||
|
||||
|
||||
def resolve_playbook_scripts(context: dict) -> str:
|
||||
return join_deploy_subpath(resolve_deploy_root(context), "scripts")
|
||||
return join_playbook_subpath(resolve_playbook_root(context), "scripts")
|
||||
|
||||
|
||||
def read_git_commit(root: Path) -> str:
|
||||
|
|
@ -408,9 +441,11 @@ def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
|
|||
docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def write_snapshot_readme(dest_prefix: Path, deploy_root: str, langs: list[str]) -> None:
|
||||
scripts_path = join_deploy_subpath(deploy_root, "scripts/playbook.py")
|
||||
docs_index_path = join_deploy_subpath(deploy_root, "docs/index.md")
|
||||
def write_snapshot_readme(
|
||||
dest_prefix: Path, playbook_root: str, langs: list[str]
|
||||
) -> None:
|
||||
scripts_path = join_playbook_subpath(playbook_root, "scripts/playbook.py")
|
||||
docs_index_path = join_playbook_subpath(playbook_root, "docs/index.md")
|
||||
lines = [
|
||||
"# Playbook(裁剪快照)",
|
||||
"",
|
||||
|
|
@ -424,7 +459,7 @@ def write_snapshot_readme(dest_prefix: Path, deploy_root: str, langs: list[str])
|
|||
f"python {scripts_path} -config playbook.toml",
|
||||
"```",
|
||||
"",
|
||||
f"配置示例:`{join_deploy_subpath(deploy_root, 'playbook.toml.example')}`",
|
||||
f"配置示例:`{join_playbook_subpath(playbook_root, 'playbook.toml.example')}`",
|
||||
"",
|
||||
"文档入口:",
|
||||
"",
|
||||
|
|
@ -453,23 +488,51 @@ def write_source_file(dest_prefix: Path, langs: list[str]) -> None:
|
|||
)
|
||||
|
||||
|
||||
def vendor_action(config: dict, context: dict) -> int:
|
||||
def snapshot_langs(config: dict) -> list[str]:
|
||||
sync_standards = config.get("sync_standards")
|
||||
if isinstance(sync_standards, dict):
|
||||
return normalize_langs(sync_standards.get("langs"))
|
||||
return normalize_langs(None)
|
||||
|
||||
|
||||
def is_generated_snapshot(path: Path) -> bool:
|
||||
source = path / "SOURCE.md"
|
||||
if not source.is_file():
|
||||
return False
|
||||
try:
|
||||
langs = normalize_langs(config.get("langs"))
|
||||
return "Generated-by: scripts/playbook.py" in source.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def install_snapshot(config: dict, context: dict) -> int:
|
||||
try:
|
||||
langs = snapshot_langs(config)
|
||||
except ValueError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
deploy_root = context["deploy_root"]
|
||||
target_path = Path(deploy_root)
|
||||
|
||||
playbook_root = context["playbook_root"]
|
||||
target_path = Path(playbook_root)
|
||||
project_root: Path = context["project_root"]
|
||||
dest_prefix = project_root / target_path
|
||||
dest_standards = dest_prefix.parent
|
||||
|
||||
if dest_prefix.resolve() == PLAYBOOK_ROOT.resolve():
|
||||
log(f"Snapshot already installed at {dest_prefix}")
|
||||
return 0
|
||||
|
||||
ensure_dir(dest_standards)
|
||||
|
||||
if dest_prefix.exists():
|
||||
if not is_generated_snapshot(dest_prefix):
|
||||
print(
|
||||
"ERROR: refusing to replace existing playbook_root because it is "
|
||||
"not a generated snapshot; use install_mode = \"subtree\" for a "
|
||||
"git subtree-managed Playbook",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
backup = dest_standards / f"{dest_prefix.name}.bak.{timestamp}"
|
||||
dest_prefix.rename(backup)
|
||||
|
|
@ -535,13 +598,29 @@ def vendor_action(config: dict, context: dict) -> int:
|
|||
copy2(example_config, dest_prefix / "playbook.toml.example")
|
||||
|
||||
write_docs_index(dest_prefix, langs)
|
||||
write_snapshot_readme(dest_prefix, deploy_root, langs)
|
||||
write_snapshot_readme(dest_prefix, playbook_root, langs)
|
||||
write_source_file(dest_prefix, langs)
|
||||
|
||||
log(f"Vendored snapshot -> {dest_prefix}")
|
||||
log(f"Installed snapshot -> {dest_prefix}")
|
||||
return 0
|
||||
|
||||
|
||||
def validate_subtree_install(context: dict) -> int:
|
||||
project_root: Path = context["project_root"]
|
||||
playbook_root = context["playbook_root"]
|
||||
expected_root = (project_root / playbook_root).resolve()
|
||||
if PLAYBOOK_ROOT.resolve() == expected_root:
|
||||
return 0
|
||||
|
||||
print(
|
||||
"ERROR: install_mode = \"subtree\" requires running the project-local "
|
||||
f"Playbook script at "
|
||||
f"{join_playbook_subpath(playbook_root, 'scripts/playbook.py')}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
|
||||
def replace_placeholders(
|
||||
text: str,
|
||||
project_name: str | None,
|
||||
|
|
@ -1413,8 +1492,6 @@ def format_md_action(config: dict, context: dict) -> int:
|
|||
|
||||
def run_action(name: str, config: dict, context: dict) -> int:
|
||||
print(f"[action] {name}")
|
||||
if name == "vendor":
|
||||
return vendor_action(config, context)
|
||||
if name == "sync_rules":
|
||||
return sync_rules_action(config, context)
|
||||
if name == "sync_memory_bank":
|
||||
|
|
@ -1489,6 +1566,13 @@ def main(argv: list[str]) -> int:
|
|||
return 2
|
||||
|
||||
config = load_config(config_path)
|
||||
try:
|
||||
validate_removed_config(config)
|
||||
install_mode = resolve_install_mode(config)
|
||||
except ValueError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
playbook_config = config.get("playbook", {})
|
||||
project_root = playbook_config.get("project_root")
|
||||
if project_root:
|
||||
|
|
@ -1499,7 +1583,7 @@ def main(argv: list[str]) -> int:
|
|||
root = config_path.parent
|
||||
resolved_root = root.resolve()
|
||||
try:
|
||||
deploy_root = resolve_configured_deploy_root(config, resolved_root)
|
||||
playbook_root = resolve_configured_playbook_root(config, resolved_root)
|
||||
except ValueError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
|
@ -1507,9 +1591,19 @@ def main(argv: list[str]) -> int:
|
|||
"project_root": resolved_root,
|
||||
"config_path": config_path.resolve(),
|
||||
"config": config,
|
||||
"deploy_root": deploy_root,
|
||||
"install_mode": install_mode,
|
||||
"playbook_root": playbook_root,
|
||||
}
|
||||
|
||||
if install_mode == "snapshot":
|
||||
result = install_snapshot(config, context)
|
||||
if result != 0:
|
||||
return result
|
||||
elif config_uses_playbook_root(config):
|
||||
result = validate_subtree_install(context)
|
||||
if result != 0:
|
||||
return result
|
||||
|
||||
if should_sync_agents(config):
|
||||
result = sync_agents_template(context)
|
||||
if result != 0:
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ templates/
|
|||
# playbook.toml
|
||||
[playbook]
|
||||
project_root = "/path/to/project"
|
||||
playbook_root = "docs/standards/playbook"
|
||||
install_mode = "snapshot"
|
||||
|
||||
# 同步 AGENT_RULES.md(配置节存在即启用)
|
||||
[sync_rules]
|
||||
|
|
@ -98,15 +100,15 @@ project_name = "MyProject"
|
|||
```
|
||||
|
||||
```bash
|
||||
python <deploy_root>/scripts/playbook.py -config playbook.toml
|
||||
python <playbook_root>/scripts/playbook.py -config playbook.toml
|
||||
```
|
||||
|
||||
参数说明见 `playbook.toml.example`(仓库根目录)或项目内的
|
||||
`<deploy_root>/playbook.toml.example`。
|
||||
`<playbook_root>/playbook.toml.example`。
|
||||
|
||||
其中 `<deploy_root>` 默认为 `docs/standards/playbook`,
|
||||
其中 `<playbook_root>` 默认为 `docs/standards/playbook`,
|
||||
也可以按项目配置改成 `custom/playbook` 等自定义目录;
|
||||
对应文档入口会变成 `<deploy_root>/docs/...`。
|
||||
对应文档入口会变成 `<playbook_root>/docs/...`。
|
||||
|
||||
如果你当前是在 **外部 clone 的 playbook 仓库** 中执行,而不是在目标项目内执行快照,请使用:
|
||||
|
||||
|
|
@ -114,8 +116,8 @@ python <deploy_root>/scripts/playbook.py -config playbook.toml
|
|||
python scripts/playbook.py -config playbook.toml
|
||||
```
|
||||
|
||||
此时 `[vendor]` 会把快照写入 `<project_root>/<deploy_root>`;
|
||||
后续再在目标项目内使用 `<deploy_root>/scripts/playbook.py`
|
||||
此时 `install_mode = "snapshot"` 会把快照写入 `<project_root>/<playbook_root>`;
|
||||
后续再在目标项目内使用 `<playbook_root>/scripts/playbook.py`
|
||||
做同步更新。
|
||||
|
||||
### 配置节说明
|
||||
|
|
@ -296,8 +298,7 @@ project/
|
|||
|
||||
### ci/、cpp/、python/
|
||||
|
||||
语言和 CI 配置模板。通过 playbook.py 的 `[vendor]`
|
||||
复制到快照中:
|
||||
语言和 CI 配置模板。`install_mode = "snapshot"` 安装快照时会复制这些模板:
|
||||
|
||||
- `ci/gitea/`:Gitea Actions 工作流与辅助脚本。
|
||||
部署到快照 `templates/ci/`
|
||||
|
|
@ -307,7 +308,7 @@ project/
|
|||
- `python/`:`pyproject.toml`、`.editorconfig` 等文件。
|
||||
部署到快照 `templates/python/`
|
||||
|
||||
> 注意:这些模板通过 `[vendor]` 复制到快照的 `templates/` 目录,需手动从快照复制到项目根目录使用。
|
||||
> 注意:这些模板会复制到快照的 `templates/` 目录,需手动从快照复制到项目根目录使用。
|
||||
> 其中 `ci/gitea/` 应按 `templates/ci/README.md` 的说明,整块复制 `.gitea/` 目录,而不只是复制 workflows。
|
||||
|
||||
**使用方式**:
|
||||
|
|
@ -316,14 +317,16 @@ project/
|
|||
# playbook.toml - 生成包含这些模板的快照
|
||||
[playbook]
|
||||
project_root = "/path/to/project"
|
||||
playbook_root = "docs/standards/playbook"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[vendor]
|
||||
[sync_standards]
|
||||
langs = ["tsl", "cpp", "python"]
|
||||
```
|
||||
|
||||
```bash
|
||||
python scripts/playbook.py -config playbook.toml
|
||||
# 然后手动从 <deploy_root>/templates/ 复制所需配置到项目根目录
|
||||
# 然后手动从 <playbook_root>/templates/ 复制所需配置到项目根目录
|
||||
```
|
||||
|
||||
## 与 playbook 其他部分的关系
|
||||
|
|
@ -335,14 +338,14 @@ playbook/
|
|||
├── docs/ # 权威静态文档
|
||||
├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等
|
||||
└── scripts/
|
||||
└── playbook.py # 统一入口:vendor / sync_*
|
||||
└── playbook.py # 统一入口:snapshot install / sync_*
|
||||
```
|
||||
|
||||
## 完整部署流程
|
||||
|
||||
```bash
|
||||
# 1. 准备配置并执行统一入口
|
||||
python <deploy_root>/scripts/playbook.py -config playbook.toml
|
||||
python <playbook_root>/scripts/playbook.py -config playbook.toml
|
||||
|
||||
# 2. 编辑 memory-bank/*.md 填写项目信息
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ tests/
|
|||
├── test_no_backup_flags.py # no_backup 行为测试
|
||||
├── test_playbook_typing_imports.py # playbook.py typing 导入兼容性测试
|
||||
├── test_sync_directory_actions.py # sync_memory_bank/sync_prompts 行为测试
|
||||
├── test_vendor_snapshot_templates.py # vendor 快照模板完整性测试
|
||||
├── test_vendor_snapshot_templates.py # snapshot 快照模板完整性测试
|
||||
├── test_main_loop_cli.py # main_loop CLI 测试
|
||||
├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试
|
||||
├── test_sync_templates_placeholders.py # 占位符替换测试(sync_rules/sync_standards)
|
||||
|
|
@ -70,7 +70,7 @@ sh tests/integration/check_doc_links.sh
|
|||
|
||||
- CLI 参数解析与帮助信息
|
||||
- TOML 配置解析与动作顺序
|
||||
- vendor/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地
|
||||
- snapshot install/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地
|
||||
|
||||
### 2. 模板验证测试 (templates/)
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,8 @@ class PlaybookCliTests(unittest.TestCase):
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "."
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[format_md]
|
||||
|
||||
|
|
@ -142,7 +143,7 @@ langs = ["tsl"]
|
|||
self.assertIn("sync_standards", output)
|
||||
self.assertIn("format_md", output)
|
||||
|
||||
def test_format_md_only_does_not_require_deploy_root(self):
|
||||
def test_format_md_only_does_not_require_playbook_root(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
|
|
@ -157,15 +158,55 @@ project_root = "{tmp_dir}"
|
|||
|
||||
self.assertEqual(result.returncode, 0)
|
||||
|
||||
def test_vendor_creates_snapshot(self):
|
||||
def test_vendor_section_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[vendor]
|
||||
langs = ["tsl"]
|
||||
"""
|
||||
config_path = write_config(root, "playbook.toml", config_body)
|
||||
|
||||
result = run_cli("-config", str(config_path))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("[vendor]", result.stdout + result.stderr)
|
||||
|
||||
def test_deploy_root_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[vendor]
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
"""
|
||||
config_path = write_config(root, "playbook.toml", config_body)
|
||||
|
||||
result = run_cli("-config", str(config_path))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("deploy_root", result.stdout + result.stderr)
|
||||
self.assertIn("playbook_root", result.stdout + result.stderr)
|
||||
|
||||
def test_snapshot_install_creates_snapshot(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
"""
|
||||
config_path = write_config(root, "playbook.toml", config_body)
|
||||
|
|
@ -176,15 +217,16 @@ langs = ["tsl"]
|
|||
self.assertEqual(result.returncode, 0)
|
||||
self.assertTrue(snapshot.is_file())
|
||||
|
||||
def test_vendor_docs_index_uses_new_tsl_entrypoints(self):
|
||||
def test_snapshot_docs_index_uses_new_tsl_entrypoints(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[vendor]
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
"""
|
||||
config_path = write_config(root, "playbook.toml", config_body)
|
||||
|
|
@ -201,14 +243,15 @@ langs = ["tsl"]
|
|||
self.assertIn("`tsl/reference/index.md`", text)
|
||||
self.assertNotIn("`tsl/syntax_book/index.md`", text)
|
||||
|
||||
def test_external_clone_requires_explicit_deploy_root(self):
|
||||
def test_external_clone_requires_explicit_playbook_root(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[vendor]
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
"""
|
||||
config_path = write_config(root, "playbook.toml", config_body)
|
||||
|
|
@ -216,14 +259,34 @@ langs = ["tsl"]
|
|||
result = run_cli("-config", str(config_path))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("deploy_root", result.stdout + result.stderr)
|
||||
self.assertIn("playbook_root", result.stdout + result.stderr)
|
||||
|
||||
def test_subtree_mode_requires_project_local_script(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "subtree"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
"""
|
||||
config_path = write_config(root, "playbook.toml", config_body)
|
||||
|
||||
result = run_cli("-config", str(config_path))
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("project-local Playbook script", result.stdout + result.stderr)
|
||||
|
||||
def test_sync_memory_bank_creates_memory_bank(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_memory_bank]
|
||||
project_name = "Demo"
|
||||
|
|
@ -242,7 +305,8 @@ project_name = "Demo"
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
|
|
@ -265,7 +329,8 @@ langs = ["tsl"]
|
|||
f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
|
|
@ -282,7 +347,8 @@ no_backup = true
|
|||
f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl", "cpp"]
|
||||
|
|
@ -304,7 +370,8 @@ no_backup = true
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl", "markdown"]
|
||||
|
|
@ -326,7 +393,8 @@ langs = ["tsl", "markdown"]
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
|
|
@ -352,7 +420,8 @@ langs = ["tsl"]
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
agents_home = "{target}"
|
||||
|
|
@ -453,7 +522,8 @@ skills = ["brainstorming"]
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_root}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
agents_home = "{target}"
|
||||
|
|
@ -475,7 +545,8 @@ skills = ["karpathy-guidelines"]
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
agents_home = "{target}"
|
||||
|
|
@ -496,7 +567,8 @@ skills = ["tsl-guide"]
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
codex_home = "{target}"
|
||||
|
|
@ -511,17 +583,15 @@ skills = ["brainstorming"]
|
|||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("codex_home", result.stdout + result.stderr)
|
||||
|
||||
def test_external_clone_flow_rewrites_links_with_configured_deploy_root(self):
|
||||
def test_external_clone_flow_rewrites_links_with_configured_playbook_root(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
agents_home = root / "agents-home"
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
|
||||
[vendor]
|
||||
langs = ["tsl"]
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
|
|
@ -545,23 +615,28 @@ skills = ["style-cleanup"]
|
|||
def test_deployed_snapshot_rewrites_links_from_snapshot_location(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
vendor_config = write_config(
|
||||
install_config = write_config(
|
||||
root,
|
||||
"vendor.toml",
|
||||
"install.toml",
|
||||
f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[vendor]
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
""",
|
||||
)
|
||||
|
||||
vendor_result = run_cli("-config", str(vendor_config))
|
||||
self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stdout + vendor_result.stderr)
|
||||
install_result = run_cli("-config", str(install_config))
|
||||
self.assertEqual(
|
||||
install_result.returncode,
|
||||
0,
|
||||
msg=install_result.stdout + install_result.stderr,
|
||||
)
|
||||
|
||||
vendored_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py"
|
||||
snapshot_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py"
|
||||
agents_home = root / "local-agents"
|
||||
sync_config = write_config(
|
||||
root,
|
||||
|
|
@ -569,7 +644,8 @@ langs = ["tsl"]
|
|||
f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
|
|
@ -582,7 +658,7 @@ skills = ["style-cleanup"]
|
|||
""",
|
||||
)
|
||||
|
||||
sync_result = run_script(vendored_script, "-config", str(sync_config))
|
||||
sync_result = run_script(snapshot_script, "-config", str(sync_config))
|
||||
self.assertEqual(sync_result.returncode, 0, msg=sync_result.stdout + sync_result.stderr)
|
||||
self.assert_style_cleanup_tsl_docs_prefix(
|
||||
root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs"
|
||||
|
|
@ -593,7 +669,8 @@ skills = ["style-cleanup"]
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_memory_bank]
|
||||
project_name = "Demo"
|
||||
|
|
@ -619,7 +696,8 @@ project_name = "Demo"
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_memory_bank]
|
||||
project_name = "Demo"
|
||||
|
|
@ -650,7 +728,8 @@ project_name = "Demo"
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_memory_bank]
|
||||
project_name = "Demo"
|
||||
|
|
@ -682,7 +761,8 @@ project_name = "Demo"
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_memory_bank]
|
||||
project_name = "Demo"
|
||||
|
|
@ -708,7 +788,8 @@ project_name = "Demo"
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_memory_bank]
|
||||
project_name = "Demo"
|
||||
|
|
@ -730,7 +811,8 @@ project_name = "Demo"
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
agents_home = "{agents_home}"
|
||||
|
|
@ -759,7 +841,8 @@ skills = ["commit-message"]
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
agents_home = "{agents_home}"
|
||||
|
|
@ -784,7 +867,8 @@ no_backup = true
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
agents_home = "{agents_home}"
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ class DeploymentRoutesE2ETests(unittest.TestCase):
|
|||
"""
|
||||
[playbook]
|
||||
project_root = "."
|
||||
playbook_root = "docs/standards/playbook"
|
||||
install_mode = "subtree"
|
||||
|
||||
[sync_rules]
|
||||
no_backup = true
|
||||
|
|
@ -119,7 +121,7 @@ no_backup = true
|
|||
self.assertIn("@AGENT_RULES.md", text)
|
||||
self.assertIn("<!-- playbook:claude:start -->", text)
|
||||
|
||||
def test_external_clone_deployment_vendors_snapshot_and_updates_claude(self):
|
||||
def test_external_clone_deployment_installs_snapshot_and_updates_claude(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
tmp_root = Path(tmp_dir)
|
||||
external_clone = tmp_root / "playbook"
|
||||
|
|
@ -137,10 +139,8 @@ no_backup = true
|
|||
f"""
|
||||
[playbook]
|
||||
project_root = "."
|
||||
deploy_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}"
|
||||
|
||||
[vendor]
|
||||
langs = ["tsl", "markdown"]
|
||||
playbook_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_rules]
|
||||
no_backup = true
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ class GitattributesModeTests(unittest.TestCase):
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = \"{root}\"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = [\"tsl\"]
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ class NoBackupFlagsTests(unittest.TestCase):
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_rules]
|
||||
force = true
|
||||
|
|
@ -56,7 +57,8 @@ no_backup = true
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
|
|
@ -85,7 +87,8 @@ no_backup = true
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[install_skills]
|
||||
agents_home = "{root / 'agents'}"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ class SyncDirectoryActionsTests(unittest.TestCase):
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_memory_bank]
|
||||
project_name = "Demo"
|
||||
|
|
@ -54,7 +55,8 @@ project_name = "Demo"
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_prompts]
|
||||
"""
|
||||
|
|
@ -82,7 +84,8 @@ deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_memory_bank]
|
||||
project_name = "Demo"
|
||||
|
|
|
|||
|
|
@ -222,7 +222,8 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = \"{tmp_dir}\"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_rules]
|
||||
|
||||
|
|
@ -267,31 +268,16 @@ langs = [\"cpp\", \"tsl\"]
|
|||
self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text)
|
||||
self.assertFalse(rules_text.endswith("\n\n"))
|
||||
|
||||
def test_sync_standards_rewrites_typescript_docs_prefix_for_vendored_playbook(self):
|
||||
def test_sync_standards_rewrites_typescript_docs_prefix_for_snapshot_playbook(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
vendor_config = root / "vendor.toml"
|
||||
vendor_config.write_text(
|
||||
install_config = root / "install.toml"
|
||||
install_config.write_text(
|
||||
f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
|
||||
[vendor]
|
||||
langs = ["typescript"]
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
vendor_result = run_cli("-config", str(vendor_config))
|
||||
self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stderr)
|
||||
|
||||
sync_config = root / "sync.toml"
|
||||
sync_config.write_text(
|
||||
f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["typescript"]
|
||||
|
|
@ -299,11 +285,28 @@ langs = ["typescript"]
|
|||
encoding="utf-8",
|
||||
)
|
||||
|
||||
vendored_script = (
|
||||
install_result = run_cli("-config", str(install_config))
|
||||
self.assertEqual(install_result.returncode, 0, msg=install_result.stderr)
|
||||
|
||||
sync_config = root / "sync.toml"
|
||||
sync_config.write_text(
|
||||
f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_standards]
|
||||
langs = ["typescript"]
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
snapshot_script = (
|
||||
root / "docs" / "standards" / "playbook" / "scripts" / "playbook.py"
|
||||
)
|
||||
sync_result = run_script(
|
||||
vendored_script, "-config", str(sync_config), cwd=root
|
||||
snapshot_script, "-config", str(sync_config), cwd=root
|
||||
)
|
||||
self.assertEqual(sync_result.returncode, 0, msg=sync_result.stderr)
|
||||
|
||||
|
|
@ -317,7 +320,8 @@ langs = ["typescript"]
|
|||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[sync_rules]
|
||||
|
||||
|
|
|
|||
|
|
@ -66,19 +66,20 @@ class TslEntrypointsConsistencyTests(unittest.TestCase):
|
|||
self.assertIn("方式一:git subtree", text)
|
||||
self.assertIn("方式二:外部 clone 后执行部署", text)
|
||||
self.assertIn("`project_root`:目标项目根目录", text)
|
||||
self.assertIn("`deploy_root`:相对于 `project_root` 的项目内目标目录", text)
|
||||
self.assertIn("`playbook_root`:相对于 `project_root` 的项目内 Playbook 根目录", text)
|
||||
self.assertIn("不是外部 clone 出来的 Playbook 仓库路径", text)
|
||||
self.assertIn("外部 clone 场景下必须显式填写 `deploy_root`", text)
|
||||
self.assertIn("外部 clone 场景下必须显式填写 `playbook_root`", text)
|
||||
self.assertNotIn("方式二:手动复制快照", text)
|
||||
self.assertNotIn("方式三:CLI 裁剪复制", text)
|
||||
self.assertNotIn("如果省略 `deploy_root`,默认仍部署到 `docs/standards/playbook`", text)
|
||||
self.assertNotIn("如果省略 `playbook_root`,默认仍部署到 `docs/standards/playbook`", text)
|
||||
|
||||
def test_playbook_example_defines_deploy_root_as_target_path(self):
|
||||
def test_playbook_example_defines_playbook_root_as_target_path(self):
|
||||
text = PLAYBOOK_EXAMPLE.read_text(encoding="utf-8")
|
||||
self.assertIn('deploy_root = "docs/standards/playbook"', text)
|
||||
self.assertIn('playbook_root = "docs/standards/playbook"', text)
|
||||
self.assertIn('install_mode = "subtree"', text)
|
||||
self.assertIn("相对于 project_root", text)
|
||||
self.assertIn("不是外部 clone 的 playbook 路径", text)
|
||||
self.assertIn("从外部 clone 执行时必填", text)
|
||||
self.assertIn("snapshot 表示从外部 clone 安装快照", text)
|
||||
self.assertNotIn("target_dir", text)
|
||||
|
||||
def test_deployment_docs_do_not_reference_legacy_terms(self):
|
||||
|
|
|
|||
|
|
@ -17,15 +17,16 @@ def run_cli(*args):
|
|||
)
|
||||
|
||||
|
||||
class VendorSnapshotTemplatesTests(unittest.TestCase):
|
||||
def test_vendor_includes_core_templates(self):
|
||||
class SnapshotTemplatesTests(unittest.TestCase):
|
||||
def test_snapshot_install_includes_core_templates(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_body = f"""
|
||||
[playbook]
|
||||
project_root = "{tmp_dir}"
|
||||
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
|
||||
install_mode = "snapshot"
|
||||
|
||||
[vendor]
|
||||
[sync_standards]
|
||||
langs = ["tsl"]
|
||||
"""
|
||||
config_path = Path(tmp_dir) / "playbook.toml"
|
||||
|
|
|
|||
Loading…
Reference in New Issue