From d03a20af424a710f83a1e3de8a76dead429bb9d5 Mon Sep 17 00:00:00 2001 From: sickn33 Date: Mon, 30 Mar 2026 21:32:29 +0200 Subject: [PATCH] fix(release): retry plugin cleanup during bundle sync Handle transient ENOTEMPTY failures when rebuilding root and bundle plugin skill directories during the release sync flow, and cover the retry behavior with a unit test. --- tools/scripts/sync_editorial_bundles.py | 22 +++++++++++++++++-- tools/scripts/tests/test_editorial_bundles.py | 18 +++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/tools/scripts/sync_editorial_bundles.py b/tools/scripts/sync_editorial_bundles.py index 46bb7708..d1f04cce 100644 --- a/tools/scripts/sync_editorial_bundles.py +++ b/tools/scripts/sync_editorial_bundles.py @@ -2,9 +2,11 @@ from __future__ import annotations import argparse +import errno import json import re import shutil +import time from pathlib import Path from typing import Any @@ -568,11 +570,27 @@ def _render_codex_marketplace( } +def _remove_tree(path: Path, retries: int = 3, delay_seconds: float = 0.1) -> None: + last_error: OSError | None = None + for attempt in range(retries): + try: + shutil.rmtree(path) + return + except OSError as exc: + if exc.errno != errno.ENOTEMPTY or attempt == retries - 1: + raise + last_error = exc + time.sleep(delay_seconds * (attempt + 1)) + + if last_error is not None: + raise last_error + + def _materialize_plugin_skills(root: Path, destination_root: Path, skill_ids: list[str]) -> None: if destination_root.is_symlink() or destination_root.is_file(): destination_root.unlink() elif destination_root.exists(): - shutil.rmtree(destination_root) + _remove_tree(destination_root) destination_root.mkdir(parents=True, exist_ok=True) for skill_id in skill_ids: @@ -628,7 +646,7 @@ def _sync_bundle_plugin_directory( plugin_name = _bundle_plugin_name(bundle["id"]) plugin_root = root / "plugins" / plugin_name if plugin_root.exists(): - shutil.rmtree(plugin_root) + _remove_tree(plugin_root) bundle_skills_root = plugin_root / "skills" bundle_skills_root.mkdir(parents=True, exist_ok=True) diff --git a/tools/scripts/tests/test_editorial_bundles.py b/tools/scripts/tests/test_editorial_bundles.py index c57e1ba6..c30089a3 100644 --- a/tools/scripts/tests/test_editorial_bundles.py +++ b/tools/scripts/tests/test_editorial_bundles.py @@ -1,6 +1,8 @@ import importlib.util +import errno import pathlib import sys +from unittest import mock import unittest @@ -142,6 +144,22 @@ class EditorialBundlesTests(unittest.TestCase): f"Claude root plugin inclusion mismatch for {skill_id}", ) + def test_remove_tree_retries_on_enotempty(self): + target = REPO_ROOT / "plugins" / "antigravity-awesome-skills" / "skills" + calls = {"count": 0} + + def flaky_rmtree(path): + calls["count"] += 1 + if calls["count"] == 1: + raise OSError(errno.ENOTEMPTY, "Directory not empty") + + with mock.patch.object(editorial_bundles.shutil, "rmtree", side_effect=flaky_rmtree): + with mock.patch.object(editorial_bundles.time, "sleep") as sleep_mock: + editorial_bundles._remove_tree(target) + + self.assertEqual(calls["count"], 2) + sleep_mock.assert_called_once() + if __name__ == "__main__": unittest.main()