229 lines
8.3 KiB
Python
229 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Atomically add a BSL handler, form command, and visible form button."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from diff_1c_patch_workspace import build_diff
|
|
from edit_1c_bsl_routine import edit_workspace as edit_bsl_routine
|
|
from edit_1c_form_button import edit_workspace as edit_form_button
|
|
from edit_1c_form_command import edit_workspace as edit_form_command
|
|
from validate_1c_patch_workspace_semantics import validate_workspace
|
|
|
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
|
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
|
|
|
|
def write_json(path: Path, data: dict[str, Any]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
|
|
|
|
def read_bytes(path: Path) -> bytes:
|
|
return path.read_bytes()
|
|
|
|
|
|
def workspace_working_files(workspace: Path) -> list[Path]:
|
|
manifest = load_json(workspace / "manifest.json")
|
|
paths = []
|
|
for record in manifest.get("files") or []:
|
|
relative = Path(str(record.get("relative_path") or ""))
|
|
if relative.is_absolute() or ".." in relative.parts:
|
|
raise SystemExit(f"Unsafe manifest relative path: {relative}")
|
|
path = workspace / "working" / relative
|
|
if path.exists():
|
|
paths.append(path)
|
|
return paths
|
|
|
|
|
|
def snapshot_working_files(workspace: Path) -> dict[Path, bytes]:
|
|
return {path: read_bytes(path) for path in workspace_working_files(workspace)}
|
|
|
|
|
|
def restore_snapshot(snapshot: dict[Path, bytes]) -> None:
|
|
for path, content in snapshot.items():
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(content)
|
|
|
|
|
|
def decode_routine_text(args: argparse.Namespace) -> str:
|
|
sources = [bool(args.routine_text), bool(args.routine_text_b64), bool(args.routine_file)]
|
|
if sum(sources) != 1:
|
|
raise SystemExit("Use exactly one of --routine-text, --routine-text-b64, or --routine-file.")
|
|
if args.routine_text is not None:
|
|
return args.routine_text
|
|
if args.routine_text_b64:
|
|
return base64.b64decode(args.routine_text_b64).decode("utf-8")
|
|
return Path(args.routine_file).read_text(encoding="utf-8-sig")
|
|
|
|
|
|
def build_result(
|
|
*,
|
|
workspace: Path,
|
|
form_relative_path: str,
|
|
bsl_relative_path: str,
|
|
operation: str,
|
|
routine_text: str,
|
|
command_name: str,
|
|
command_title: str,
|
|
command_action: str,
|
|
button_parent_name: str,
|
|
button_name: str,
|
|
button_title: str,
|
|
keep_on_failure: bool,
|
|
max_patch_chars: int,
|
|
) -> dict[str, Any]:
|
|
snapshot = snapshot_working_files(workspace)
|
|
steps: list[dict[str, Any]] = []
|
|
rolled_back = False
|
|
error: dict[str, Any] | None = None
|
|
|
|
try:
|
|
bsl = edit_bsl_routine(
|
|
workspace,
|
|
bsl_relative_path,
|
|
routine_text,
|
|
operation=operation,
|
|
keep_on_failure=True,
|
|
)
|
|
steps.append({"name": "bsl_routine", "result": bsl})
|
|
if not bsl.get("semantic_validation", {}).get("passed"):
|
|
raise RuntimeError("BSL routine edit failed semantic validation.")
|
|
|
|
command = edit_form_command(
|
|
workspace,
|
|
form_relative_path,
|
|
name=command_name,
|
|
title=command_title,
|
|
action=command_action,
|
|
tooltip=None,
|
|
command_id=None,
|
|
call_type="Override",
|
|
operation=operation,
|
|
keep_on_failure=True,
|
|
)
|
|
steps.append({"name": "form_command", "result": command})
|
|
if not command.get("semantic_validation", {}).get("passed"):
|
|
raise RuntimeError("Form command edit failed semantic validation.")
|
|
|
|
button = edit_form_button(
|
|
workspace,
|
|
form_relative_path,
|
|
parent_name=button_parent_name,
|
|
name=button_name,
|
|
title=button_title,
|
|
command_name=command_name,
|
|
button_id=None,
|
|
button_type="CommandBarButton",
|
|
operation=operation,
|
|
keep_on_failure=True,
|
|
)
|
|
steps.append({"name": "form_button", "result": button})
|
|
if not button.get("semantic_validation", {}).get("passed"):
|
|
raise RuntimeError("Form button edit failed semantic validation.")
|
|
|
|
semantic = validate_workspace(workspace)
|
|
if not semantic.get("passed"):
|
|
raise RuntimeError("Final semantic validation failed.")
|
|
diff = build_diff(workspace, max_patch_chars=max_patch_chars)
|
|
except (Exception, SystemExit) as exc:
|
|
error = {"type": type(exc).__name__, "message": str(exc)}
|
|
semantic = validate_workspace(workspace)
|
|
diff = build_diff(workspace, max_patch_chars=max_patch_chars)
|
|
if not keep_on_failure:
|
|
restore_snapshot(snapshot)
|
|
rolled_back = True
|
|
semantic = validate_workspace(workspace)
|
|
diff = build_diff(workspace, max_patch_chars=max_patch_chars)
|
|
|
|
passed = error is None and bool(semantic.get("passed")) and bool(diff.get("passed"))
|
|
return {
|
|
"schema": "onec_form_button_workflow.v1",
|
|
"workspace": str(workspace),
|
|
"operation": operation,
|
|
"inputs": {
|
|
"form_relative_path": form_relative_path,
|
|
"bsl_relative_path": bsl_relative_path,
|
|
"command_name": command_name,
|
|
"command_title": command_title,
|
|
"command_action": command_action,
|
|
"button_parent_name": button_parent_name,
|
|
"button_name": button_name,
|
|
"button_title": button_title,
|
|
},
|
|
"passed": passed,
|
|
"rolled_back": rolled_back,
|
|
"error": error,
|
|
"steps": steps,
|
|
"semantic_validation": {
|
|
"schema": semantic.get("schema"),
|
|
"passed": semantic.get("passed"),
|
|
"counts": semantic.get("counts"),
|
|
"findings": semantic.get("findings"),
|
|
},
|
|
"diff_summary": diff.get("counts"),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Atomically add BSL handler, form command, and visible button in a 1C patch workspace.")
|
|
parser.add_argument("--workspace", type=Path, required=True)
|
|
parser.add_argument("--form-relative-path", required=True)
|
|
parser.add_argument("--bsl-relative-path", required=True)
|
|
parser.add_argument("--operation", choices=["append", "replace", "upsert"], default="upsert")
|
|
parser.add_argument("--routine-text")
|
|
parser.add_argument("--routine-text-b64")
|
|
parser.add_argument("--routine-file", type=Path)
|
|
parser.add_argument("--command-name", required=True)
|
|
parser.add_argument("--command-title", required=True)
|
|
parser.add_argument("--command-action", required=True)
|
|
parser.add_argument("--button-parent-name", required=True)
|
|
parser.add_argument("--button-name", required=True)
|
|
parser.add_argument("--button-title", required=True)
|
|
parser.add_argument("--keep-on-failure", action="store_true", help="Keep partial edits when any step fails.")
|
|
parser.add_argument("--max-patch-chars", type=int, default=200000)
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
result = build_result(
|
|
workspace=args.workspace,
|
|
form_relative_path=args.form_relative_path,
|
|
bsl_relative_path=args.bsl_relative_path,
|
|
operation=args.operation,
|
|
routine_text=decode_routine_text(args),
|
|
command_name=args.command_name,
|
|
command_title=args.command_title,
|
|
command_action=args.command_action,
|
|
button_parent_name=args.button_parent_name,
|
|
button_name=args.button_name,
|
|
button_title=args.button_title,
|
|
keep_on_failure=args.keep_on_failure,
|
|
max_patch_chars=args.max_patch_chars,
|
|
)
|
|
if args.output:
|
|
write_json(args.output, result)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"output": str(args.output) if args.output else None,
|
|
"passed": result["passed"],
|
|
"rolled_back": result["rolled_back"],
|
|
"error": result["error"],
|
|
"semantic": result["semantic_validation"]["counts"],
|
|
"diff": result["diff_summary"],
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
return 0 if result["passed"] else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|