From 57934383e7e0e65b9a853e930f5ef88dcfb0d49f Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 15 Oct 2026 10:33:54 +0400 Subject: [PATCH] =?UTF-8?q?fix(release):=20=D1=81=D0=B1=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20agent.py=20=D0=BD=D0=B5=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D1=8F=D0=B5=D1=82=20=D0=BF=D1=80=D0=B5=D0=B6?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20agent.py=20(T2-I1)=20(#32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Откат удаляет agent.py только при первой публикации; при сбое до переименования прежний файл не трогается. Тест: os.replace падает на agent.py.prev — release/ байт-в-байт прежний. Co-Authored-By: Claude Opus 5.5 --- src/jira_hub/release.py | 11 ++++++----- tests/test_release.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/jira_hub/release.py b/src/jira_hub/release.py index 1f1c7d6..14101a1 100644 --- a/src/jira_hub/release.py +++ b/src/jira_hub/release.py @@ -74,7 +74,7 @@ def publish(release_dir, source) -> dict: manifest = {"version": version, "sha256": hashlib.sha256(data).hexdigest(), "size": len(data), "published_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat()} agent, prev_file = rd / "agent.py", rd / "agent.py.prev" - moved = False + existed, moved = agent.exists(), False try: secure_dir(rd) if agent.exists(): @@ -83,7 +83,7 @@ def publish(release_dir, source) -> dict: _write_atomic(agent, data) _write_atomic(rd / "manifest.json", json.dumps(manifest, ensure_ascii=False).encode("utf-8")) except OSError as e: - _undo(agent, prev_file, moved) + _undo(agent, prev_file, moved, existed) raise PublishError(f"не удалось записать публикацию: {e}") from e try: prev_file.unlink(missing_ok=True) # и прежний, и оставленный упавшей публикацией (риск в design) @@ -92,13 +92,14 @@ def publish(release_dir, source) -> dict: return manifest -def _undo(agent: Path, prev_file: Path, moved: bool) -> None: +def _undo(agent: Path, prev_file: Path, moved: bool, existed: bool) -> None: """Вернуть release/ к прежней публикации после сбоя записи: agent.py.prev на место agent.py; если прежнего не было — - убрать записанный agent.py. Сбой возврата — в лог: команда всё равно завершается сообщением.""" + убрать записанный agent.py. Прежний agent.py, которого не сдвинули (сбой до переименования), не трогаем. + Сбой возврата — в лог: команда всё равно завершается сообщением.""" try: if moved: os.replace(prev_file, agent) - else: + elif not existed: agent.unlink(missing_ok=True) except OSError as e: log.error("release: прежняя публикация не восстановлена (%s): %s", prev_file if moved else agent, e) diff --git a/tests/test_release.py b/tests/test_release.py index 1d777db..597089f 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -247,3 +247,23 @@ def test_load_non_utf8_with_matching_sha_is_none(tmp_path, caplog): assert release.current(rd)["version"] == 6 assert release.load(rd) is None assert "agent.py не в UTF-8" in caplog.text + + +def test_move_failure_keeps_previous_agent(tmp_path, monkeypatch): + """T2-I1: сбой переименования agent.py -> agent.py.prev при существующем agent.py — прежний agent.py не трогаем.""" + rd = tmp_path / "release" + release.publish(rd, agent_file(tmp_path, 8)) + before = _listing(rd) + real = release.os.replace + + def fake(src, dst, *a, **kw): + if Path(dst).name == "agent.py.prev": + raise OSError(13, "Permission denied") + return real(src, dst, *a, **kw) + + monkeypatch.setattr(release.os, "replace", fake) + with pytest.raises(release.PublishError, match=r"^не удалось записать публикацию: "): + release.publish(rd, agent_file(tmp_path, 9)) + monkeypatch.undo() + assert _listing(rd) == before + assert release.load(rd)["version"] == 8