Skip broken symlinks in usr update backups

Avoid aborting self-update backups when /usr contains dangling symlinks, such as host-created virtualenv links inside workdir. Broken file symlinks are logged and skipped so the backup can complete while preserving normal usr files. Fixes #1598

Alessandro committed May 3, 2026 at 01:11 UTC 29c829b389bef11e662869e81ff15cb3506a8c50
2 files changed +29
docker/run/fs/exe/self_update_manager.py
+3
@@ -366,6 +366,9 @@ def create_usr_backup(
366 root_path = Path(root)
367 for filename in files:
368 source_file = root_path / filename
369 + if source_file.is_symlink() and not source_file.exists():
370 + logger.log(f"Skipping broken symlink during usr backup: {source_file}")
371 + continue
372 archive_name = Path("usr") / source_file.relative_to(usr_dir)
373 archive.write(source_file, archive_name.as_posix())
374
tests/test_self_update_tag_filter.py
+26
@@ -1,6 +1,7 @@
1 import importlib.util
2 import sys
3 import types
4 +import zipfile
5 from pathlib import Path
6
7 import pytest
@@ -755,6 +756,31 @@ def test_self_update_manager_queues_update_with_main_latest_defaults(monkeypatch
756 assert captured["payload"]["tag"] == "latest"
757
758
759 +def test_self_update_manager_usr_backup_skips_broken_symlinks(tmp_path):
760 + manager = load_self_update_manager()
761 + repo_dir = tmp_path / "repo"
762 + usr_dir = repo_dir / "usr"
763 + venv_bin = usr_dir / "workdir" / "reachy-mini-mcp" / ".venv" / "bin"
764 + venv_bin.mkdir(parents=True)
765 + (usr_dir / "settings.json").write_text('{"ok": true}\n', encoding="utf-8")
766 + broken_symlink = venv_bin / "python"
767 + broken_symlink.symlink_to("/missing/host/python")
768 +
769 + backup_path = manager.create_usr_backup(
770 + repo_dir=repo_dir,
771 + backup_path=str(tmp_path / "backups"),
772 + backup_name="usr-backup.zip",
773 + conflict_policy="rename",
774 + logger=manager.NullLogger(),
775 + )
776 +
777 + with zipfile.ZipFile(backup_path) as archive:
778 + names = set(archive.namelist())
779 +
780 + assert "usr/settings.json" in names
781 + assert "usr/workdir/reachy-mini-mcp/.venv/bin/python" not in names
782 +
783 +
784 def test_self_update_manager_latest_on_main_uses_current_major_release(monkeypatch):
785 manager = load_self_update_manager()
786 monkeypatch.setattr(