🐛 fix(playbook): honor no_backup for sync

This commit is contained in:
csh 2026-01-27 18:00:05 +08:00
parent 2d401fa002
commit 0d9a8ec465
2 changed files with 99 additions and 14 deletions

View File

@ -2,7 +2,7 @@
import sys import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from shutil import copy2, copytree, which from shutil import copy2, copytree, rmtree, which
import subprocess import subprocess
try: try:
@ -683,8 +683,9 @@ def sync_rules_action(config: dict, context: dict) -> int:
main_language = resolve_main_language(config, context) main_language = resolve_main_language(config, context)
playbook_scripts = resolve_playbook_scripts(project_root, context) playbook_scripts = resolve_playbook_scripts(project_root, context)
date_value = config.get("date") or datetime.now().strftime("%Y-%m-%d") date_value = config.get("date") or datetime.now().strftime("%Y-%m-%d")
no_backup = bool(config.get("no_backup", False))
backup_path(rules_dst, False) backup_path(rules_dst, no_backup)
text = rules_src.read_text(encoding="utf-8") text = rules_src.read_text(encoding="utf-8")
text = replace_placeholders( text = replace_placeholders(
text, project_name, date_value, main_language, playbook_scripts text, project_name, date_value, main_language, playbook_scripts
@ -879,16 +880,18 @@ def read_gitattributes_entries(path: Path) -> list[str]:
return entries return entries
def sync_gitattributes_overwrite(src: Path, dst: Path) -> None: def sync_gitattributes_overwrite(src: Path, dst: Path, no_backup: bool) -> None:
if src.resolve() == dst.resolve(): if src.resolve() == dst.resolve():
log("Skip: .gitattributes source equals destination.") log("Skip: .gitattributes source equals destination.")
return return
backup_path(dst, False) backup_path(dst, no_backup)
copy2(src, dst) copy2(src, dst)
log("Synced .gitattributes from standards (overwrite).") log("Synced .gitattributes from standards (overwrite).")
def sync_gitattributes_append(src: Path, dst: Path, source_note: str) -> None: def sync_gitattributes_append(
src: Path, dst: Path, source_note: str, no_backup: bool
) -> None:
src_entries = read_gitattributes_entries(src) src_entries = read_gitattributes_entries(src)
dst_entries: list[str] = [] dst_entries: list[str] = []
if dst.exists(): if dst.exists():
@ -899,7 +902,7 @@ def sync_gitattributes_append(src: Path, dst: Path, source_note: str) -> None:
return return
original = dst.read_text(encoding="utf-8") if dst.exists() else "" original = dst.read_text(encoding="utf-8") if dst.exists() else ""
backup_path(dst, False) backup_path(dst, no_backup)
header = f"# Added from playbook .gitattributes (source: {source_note})" header = f"# Added from playbook .gitattributes (source: {source_note})"
content = original.rstrip("\n") content = original.rstrip("\n")
if content: if content:
@ -909,7 +912,7 @@ def sync_gitattributes_append(src: Path, dst: Path, source_note: str) -> None:
log("Appended missing .gitattributes rules from standards.") log("Appended missing .gitattributes rules from standards.")
def sync_gitattributes_block(src: Path, dst: Path) -> None: def sync_gitattributes_block(src: Path, dst: Path, no_backup: bool) -> None:
begin = "# BEGIN playbook .gitattributes" begin = "# BEGIN playbook .gitattributes"
end = "# END playbook .gitattributes" end = "# END playbook .gitattributes"
begin_old = "# BEGIN tsl-playbook .gitattributes" begin_old = "# BEGIN tsl-playbook .gitattributes"
@ -939,7 +942,7 @@ def sync_gitattributes_block(src: Path, dst: Path) -> None:
if updated and updated[-1].strip(): if updated and updated[-1].strip():
updated.append("") updated.append("")
updated.extend(block_lines) updated.extend(block_lines)
backup_path(dst, False) backup_path(dst, no_backup)
dst.write_text("\n".join(updated) + "\n", encoding="utf-8") dst.write_text("\n".join(updated) + "\n", encoding="utf-8")
else: else:
dst.write_text("\n".join(block_lines) + "\n", encoding="utf-8") dst.write_text("\n".join(block_lines) + "\n", encoding="utf-8")
@ -960,6 +963,7 @@ def sync_standards_action(config: dict, context: dict) -> int:
agents_root = project_root / ".agents" agents_root = project_root / ".agents"
ensure_dir(agents_root) ensure_dir(agents_root)
no_backup = bool(config.get("no_backup", False))
timestamp = datetime.now().strftime("%Y%m%d%H%M%S") timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
for lang in langs: for lang in langs:
src = PLAYBOOK_ROOT / "rulesets" / lang src = PLAYBOOK_ROOT / "rulesets" / lang
@ -968,6 +972,9 @@ def sync_standards_action(config: dict, context: dict) -> int:
return 2 return 2
dst = agents_root / lang dst = agents_root / lang
if dst.exists(): if dst.exists():
if no_backup:
rmtree(dst)
else:
backup = agents_root / f"{lang}.bak.{timestamp}" backup = agents_root / f"{lang}.bak.{timestamp}"
dst.rename(backup) dst.rename(backup)
log(f"Backed up existing {lang} agents -> {backup.name}") log(f"Backed up existing {lang} agents -> {backup.name}")
@ -1005,11 +1012,13 @@ def sync_standards_action(config: dict, context: dict) -> int:
if mode == "skip": if mode == "skip":
log("Skip: .gitattributes sync (mode=skip).") log("Skip: .gitattributes sync (mode=skip).")
elif mode == "overwrite": elif mode == "overwrite":
sync_gitattributes_overwrite(gitattributes_src, gitattributes_dst) sync_gitattributes_overwrite(gitattributes_src, gitattributes_dst, no_backup)
elif mode == "block": elif mode == "block":
sync_gitattributes_block(gitattributes_src, gitattributes_dst) sync_gitattributes_block(gitattributes_src, gitattributes_dst, no_backup)
else: else:
sync_gitattributes_append(gitattributes_src, gitattributes_dst, source_note) sync_gitattributes_append(
gitattributes_src, gitattributes_dst, source_note, no_backup
)
return 0 return 0

View File

@ -0,0 +1,76 @@
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
def run_cli(*args):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
capture_output=True,
text=True,
)
class NoBackupFlagsTests(unittest.TestCase):
def test_sync_rules_no_backup_skips_backup_file(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
rules = root / "AGENT_RULES.md"
rules.write_text("old rules", encoding="utf-8")
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
[sync_rules]
force = true
no_backup = true
"""
config_path = root / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0, msg=result.stderr)
backups = list(root.glob("AGENT_RULES.md.bak.*"))
self.assertEqual(backups, [])
self.assertTrue(rules.is_file())
def test_sync_standards_no_backup_skips_agents_and_gitattributes_backup(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
agents = root / ".agents" / "tsl"
agents.mkdir(parents=True)
(agents / "index.md").write_text("old", encoding="utf-8")
gitattributes = root / ".gitattributes"
gitattributes.write_text("*.txt text\n", encoding="utf-8")
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
[sync_standards]
langs = ["tsl"]
gitattr_mode = "append"
no_backup = true
"""
config_path = root / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
self.assertEqual(result.returncode, 0, msg=result.stderr)
agents_backups = list((root / ".agents").glob("tsl.bak.*"))
self.assertEqual(agents_backups, [])
git_backups = list(root.glob(".gitattributes.bak.*"))
self.assertEqual(git_backups, [])
if __name__ == "__main__":
unittest.main()