🐛 fix(scripts): force LF for generated text files
This commit is contained in:
parent
c1bce2fb72
commit
7db5e4a697
|
|
@ -285,7 +285,7 @@ def load_progress_lines(progress_path: Path) -> list[str]:
|
|||
|
||||
|
||||
def write_progress_lines(progress_path: Path, lines: list[str]) -> None:
|
||||
progress_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
progress_path.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def get_thread_lock(lock_path: Path) -> threading.Lock:
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
|
|||
lines = build_docs_index_lines(langs)
|
||||
docs_index = dest_prefix / "docs/index.md"
|
||||
ensure_dir(docs_index.parent)
|
||||
docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
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:
|
||||
|
|
@ -431,7 +431,9 @@ def write_snapshot_readme(dest_prefix: Path, deploy_root: str, langs: list[str])
|
|||
f"- `{docs_index_path}`",
|
||||
"- `.agents/index.md`",
|
||||
]
|
||||
(dest_prefix / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
(dest_prefix / "README.md").write_text(
|
||||
"\n".join(lines) + "\n", encoding="utf-8", newline="\n"
|
||||
)
|
||||
|
||||
|
||||
def write_source_file(dest_prefix: Path, langs: list[str]) -> None:
|
||||
|
|
@ -446,7 +448,9 @@ def write_source_file(dest_prefix: Path, langs: list[str]) -> None:
|
|||
f"- Langs: {','.join(langs)}",
|
||||
"- Generated-by: scripts/playbook.py",
|
||||
]
|
||||
(dest_prefix / "SOURCE.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
(dest_prefix / "SOURCE.md").write_text(
|
||||
"\n".join(lines) + "\n", encoding="utf-8", newline="\n"
|
||||
)
|
||||
|
||||
|
||||
def vendor_action(config: dict, context: dict) -> int:
|
||||
|
|
@ -572,7 +576,7 @@ def replace_placeholders_in_file(
|
|||
text = file_path.read_text(encoding="utf-8")
|
||||
updated = replace_placeholders(text, project_name, date_value, playbook_scripts)
|
||||
if updated != text:
|
||||
file_path.write_text(updated, encoding="utf-8")
|
||||
file_path.write_text(updated, encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def resolve_template_target(
|
||||
|
|
@ -648,7 +652,7 @@ def update_agents_section(
|
|||
return
|
||||
|
||||
if not agents_path.exists():
|
||||
agents_path.write_text(template_text + "\n", encoding="utf-8")
|
||||
agents_path.write_text(template_text + "\n", encoding="utf-8", newline="\n")
|
||||
log("Created: AGENTS.md")
|
||||
return
|
||||
|
||||
|
|
@ -669,14 +673,16 @@ def update_agents_section(
|
|||
in_block = False
|
||||
continue
|
||||
updated.append(line)
|
||||
agents_path.write_text("\n".join(updated) + "\n", encoding="utf-8")
|
||||
agents_path.write_text(
|
||||
"\n".join(updated) + "\n", encoding="utf-8", newline="\n"
|
||||
)
|
||||
log("Updated: AGENTS.md (section)")
|
||||
else:
|
||||
if ".agents/index.md" in agents_text:
|
||||
log("Skip: AGENTS.md already references .agents/index.md")
|
||||
return
|
||||
updated = agents_text.rstrip("\n") + "\n\n" + "\n".join(block) + "\n"
|
||||
agents_path.write_text(updated, encoding="utf-8")
|
||||
agents_path.write_text(updated, encoding="utf-8", newline="\n")
|
||||
log("Appended: AGENTS.md (section)")
|
||||
|
||||
|
||||
|
|
@ -789,7 +795,9 @@ def sync_claude_md(project_root: Path, config: dict) -> None:
|
|||
|
||||
if not claude_md.exists():
|
||||
ensure_dir(claude_md.parent)
|
||||
claude_md.write_text("\n".join(block_lines) + "\n", encoding="utf-8")
|
||||
claude_md.write_text(
|
||||
"\n".join(block_lines) + "\n", encoding="utf-8", newline="\n"
|
||||
)
|
||||
log(f"Created {claude_md.relative_to(project_root)} with playbook block.")
|
||||
return
|
||||
|
||||
|
|
@ -811,13 +819,15 @@ def sync_claude_md(project_root: Path, config: dict) -> None:
|
|||
in_block = False
|
||||
continue
|
||||
updated.append(line)
|
||||
claude_md.write_text("\n".join(updated) + "\n", encoding="utf-8")
|
||||
claude_md.write_text(
|
||||
"\n".join(updated) + "\n", encoding="utf-8", newline="\n"
|
||||
)
|
||||
log("Updated CLAUDE.md (playbook block).")
|
||||
elif "@AGENTS.md" in text:
|
||||
log("Skip: CLAUDE.md already references AGENTS.md")
|
||||
else:
|
||||
appended = text.rstrip("\n") + "\n\n" + "\n".join(block_lines) + "\n"
|
||||
claude_md.write_text(appended, encoding="utf-8")
|
||||
claude_md.write_text(appended, encoding="utf-8", newline="\n")
|
||||
log("Appended playbook block to CLAUDE.md")
|
||||
|
||||
|
||||
|
|
@ -854,7 +864,7 @@ def sync_rules_action(config: dict, context: dict) -> int:
|
|||
backup_path(rules_dst, no_backup)
|
||||
text = rules_src.read_text(encoding="utf-8")
|
||||
text = replace_placeholders(text, project_name, date_value, playbook_scripts)
|
||||
rules_dst.write_text(text.rstrip("\n") + "\n", encoding="utf-8")
|
||||
rules_dst.write_text(text.rstrip("\n") + "\n", encoding="utf-8", newline="\n")
|
||||
log("Synced: AGENT_RULES.md")
|
||||
|
||||
local_rules = project_root / "AGENT_RULES.local.md"
|
||||
|
|
@ -870,6 +880,7 @@ def sync_rules_action(config: dict, context: dict) -> int:
|
|||
"- 同一错误发生 2 次以上时的修正规则\n"
|
||||
"- 团队约定的额外约束\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
log("Created: AGENT_RULES.local.md")
|
||||
|
||||
|
|
@ -963,7 +974,7 @@ def update_agents_block(agents_md: Path, block_lines: list[str]) -> None:
|
|||
end = "<!-- playbook:agents:end -->"
|
||||
if not agents_md.exists():
|
||||
content = "# Agent Instructions\n\n" + "\n".join(block_lines) + "\n"
|
||||
agents_md.write_text(content, encoding="utf-8")
|
||||
agents_md.write_text(content, encoding="utf-8", newline="\n")
|
||||
log("Created AGENTS.md")
|
||||
return
|
||||
|
||||
|
|
@ -984,14 +995,14 @@ def update_agents_block(agents_md: Path, block_lines: list[str]) -> None:
|
|||
in_block = False
|
||||
continue
|
||||
updated.append(line)
|
||||
agents_md.write_text("\n".join(updated) + "\n", encoding="utf-8")
|
||||
agents_md.write_text("\n".join(updated) + "\n", encoding="utf-8", newline="\n")
|
||||
log("Updated AGENTS.md (playbook block).")
|
||||
else:
|
||||
if ".agents/index.md" in text:
|
||||
log("Skip: AGENTS.md already references .agents/index.md")
|
||||
return
|
||||
updated = text.rstrip("\n") + "\n\n" + "\n".join(block_lines) + "\n"
|
||||
agents_md.write_text(updated, encoding="utf-8")
|
||||
agents_md.write_text(updated, encoding="utf-8", newline="\n")
|
||||
log("Appended playbook block to AGENTS.md")
|
||||
|
||||
|
||||
|
|
@ -1030,7 +1041,7 @@ def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str |
|
|||
"",
|
||||
f"- {docs_prefix or 'docs/'}",
|
||||
]
|
||||
agents_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
agents_index.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
|
||||
log("Synced .agents/index.md")
|
||||
|
||||
|
||||
|
|
@ -1056,7 +1067,7 @@ def rewrite_docs_links_in_markdown(root: Path, docs_prefix: str, recursive: bool
|
|||
for pattern, replacement in patterns:
|
||||
updated = pattern.sub(replacement, updated)
|
||||
if updated != text:
|
||||
md_path.write_text(updated, encoding="utf-8")
|
||||
md_path.write_text(updated, encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None:
|
||||
|
|
@ -1105,7 +1116,7 @@ def sync_gitattributes_append(
|
|||
if content:
|
||||
content += "\n\n"
|
||||
content += header + "\n" + "\n".join(missing) + "\n"
|
||||
dst.write_text(content, encoding="utf-8")
|
||||
dst.write_text(content, encoding="utf-8", newline="\n")
|
||||
log("Appended missing .gitattributes rules from standards.")
|
||||
|
||||
|
||||
|
|
@ -1140,9 +1151,9 @@ def sync_gitattributes_block(src: Path, dst: Path, no_backup: bool) -> None:
|
|||
updated.append("")
|
||||
updated.extend(block_lines)
|
||||
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", newline="\n")
|
||||
else:
|
||||
dst.write_text("\n".join(block_lines) + "\n", encoding="utf-8")
|
||||
dst.write_text("\n".join(block_lines) + "\n", encoding="utf-8", newline="\n")
|
||||
log("Synced .gitattributes from standards (block).")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import ast
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT_PATHS = sorted((ROOT / "scripts").glob("*.py")) + sorted(
|
||||
(ROOT / ".gitea" / "ci").glob("*.py")
|
||||
)
|
||||
|
||||
|
||||
class ScriptLineEndingTests(unittest.TestCase):
|
||||
def test_script_text_writes_pin_lf_newlines(self):
|
||||
offenders: list[str] = []
|
||||
for path in SCRIPT_PATHS:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
if is_write_text_without_lf_newline(node):
|
||||
offenders.append(f"{path.relative_to(ROOT)}:{node.lineno}")
|
||||
elif is_text_open_for_writing_without_newline(node):
|
||||
offenders.append(f"{path.relative_to(ROOT)}:{node.lineno}")
|
||||
|
||||
self.assertEqual(offenders, [])
|
||||
|
||||
|
||||
def has_lf_newline_keyword(node: ast.Call) -> bool:
|
||||
newline = next(
|
||||
(keyword for keyword in node.keywords if keyword.arg == "newline"),
|
||||
None,
|
||||
)
|
||||
return (
|
||||
newline is not None
|
||||
and isinstance(newline.value, ast.Constant)
|
||||
and newline.value.value == "\n"
|
||||
)
|
||||
|
||||
|
||||
def is_write_text_without_lf_newline(node: ast.Call) -> bool:
|
||||
func = node.func
|
||||
return (
|
||||
isinstance(func, ast.Attribute)
|
||||
and func.attr == "write_text"
|
||||
and not has_lf_newline_keyword(node)
|
||||
)
|
||||
|
||||
|
||||
def is_text_open_for_writing_without_newline(node: ast.Call) -> bool:
|
||||
func = node.func
|
||||
if isinstance(func, ast.Name):
|
||||
is_open = func.id == "open"
|
||||
elif isinstance(func, ast.Attribute):
|
||||
is_open = func.attr == "open"
|
||||
else:
|
||||
is_open = False
|
||||
if not is_open:
|
||||
return False
|
||||
|
||||
mode_node = None
|
||||
if len(node.args) >= 2:
|
||||
mode_node = node.args[1]
|
||||
for keyword in node.keywords:
|
||||
if keyword.arg == "mode":
|
||||
mode_node = keyword.value
|
||||
break
|
||||
|
||||
if mode_node is None:
|
||||
return False
|
||||
if not isinstance(mode_node, ast.Constant) or not isinstance(mode_node.value, str):
|
||||
return False
|
||||
|
||||
mode = mode_node.value
|
||||
writes_text = any(flag in mode for flag in ("w", "a", "x")) and "b" not in mode
|
||||
return writes_text and not has_lf_newline_keyword(node)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue