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.
This commit is contained in:
sickn33
2026-03-30 21:32:29 +02:00
parent 9bb2ff6a3a
commit d03a20af42
2 changed files with 38 additions and 2 deletions

View File

@@ -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)

View File

@@ -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()