From 175b51cec018e93be85e3a02cfd27ad78cfabfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=89=E6=BE=9C?= Date: Fri, 14 Aug 2026 15:45:51 +0800 Subject: [PATCH] fix(skills): retain link identity anchors through rollback --- scripts/install-skills.sh | 21 ++++++++++----------- scripts/install.sh | 27 ++++++++++++--------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/scripts/install-skills.sh b/scripts/install-skills.sh index 33f80429..c5b9a276 100755 --- a/scripts/install-skills.sh +++ b/scripts/install-skills.sh @@ -276,7 +276,8 @@ restore_linked_skill_set() { cleanup_nested_staged_link() { _cnsl_nested="$1/$2" _cnsl_target="$3" - if skill_link_matches "$_cnsl_nested" "$_cnsl_target"; then + _cnsl_inode="${4-}" + if skill_link_matches "$_cnsl_nested" "$_cnsl_target" "$_cnsl_inode"; then rm -f "$_cnsl_nested" fi } @@ -618,20 +619,18 @@ link_canonical_skills_to_base() { _lcs_link_target="$(readlink "$_lcs_staged" 2>/dev/null)" || { restore_linked_skill_set "$_lcs_published" "$_lcs_backups" || true; rm -rf "$_lcs_stage"; return 1 } + # Keep the staged hardlink alive until the whole transaction finishes. It + # pins the inode, so a concurrently recreated link cannot reuse the + # identity recorded in the publication manifest. + _lcs_inode="$(skill_link_inode "$_lcs_staged")" || { + restore_linked_skill_set "$_lcs_published" "$_lcs_backups" || true; rm -rf "$_lcs_stage"; return 1 + } # Hard-link the staged symlink itself. This never overwrites a # concurrently-created destination. If that destination became a # directory, the unique staged basename can only be linked inside it; # post-validation detects that case and removes only our link. - if ! ln -P "$_lcs_staged" "$_lcs_dest" 2>/dev/null || ! skill_link_matches "$_lcs_dest" "$_lcs_link_target"; then - cleanup_nested_staged_link "$_lcs_dest" "$_lcs_stage_name" "$_lcs_link_target" || true - restore_linked_skill_set "$_lcs_published" "$_lcs_backups" || true; rm -rf "$_lcs_stage"; return 1 - fi - _lcs_inode="$(skill_link_inode "$_lcs_dest")" || { - if skill_link_matches "$_lcs_dest" "$_lcs_link_target"; then rm -f "$_lcs_dest" || true; fi - restore_linked_skill_set "$_lcs_published" "$_lcs_backups" || true; rm -rf "$_lcs_stage"; return 1 - } - if ! rm -f "$_lcs_staged"; then - if skill_link_matches "$_lcs_dest" "$_lcs_link_target" "$_lcs_inode"; then rm -f "$_lcs_dest" || true; fi + if ! ln -P "$_lcs_staged" "$_lcs_dest" 2>/dev/null || ! skill_link_matches "$_lcs_dest" "$_lcs_link_target" "$_lcs_inode"; then + cleanup_nested_staged_link "$_lcs_dest" "$_lcs_stage_name" "$_lcs_link_target" "$_lcs_inode" || true restore_linked_skill_set "$_lcs_published" "$_lcs_backups" || true; rm -rf "$_lcs_stage"; return 1 fi if ! printf '%s\n%s\n%s\n' "$_lcs_dest" "$_lcs_link_target" "$_lcs_inode" >> "$_lcs_published"; then diff --git a/scripts/install.sh b/scripts/install.sh index a6586676..45e5a688 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -295,7 +295,8 @@ restore_linked_skill_set() { cleanup_nested_staged_link() { _cnsl_nested="$1/$2" _cnsl_target="$3" - if skill_link_matches "$_cnsl_nested" "$_cnsl_target"; then + _cnsl_inode="${4-}" + if skill_link_matches "$_cnsl_nested" "$_cnsl_target" "$_cnsl_inode"; then rm -f "$_cnsl_nested" fi } @@ -861,24 +862,20 @@ link_canonical_skills_to_base() { rm -rf "$_lcs_stage" return 1 } - # Hard-link the staged symlink itself. This never overwrites a - # concurrently-created destination. If that destination became a - # directory, the unique staged basename can only be linked inside it; - # post-validation detects that case and removes only our link. - if ! ln -P "$_lcs_staged" "$_lcs_dest" 2>/dev/null || ! skill_link_matches "$_lcs_dest" "$_lcs_link_target"; then - cleanup_nested_staged_link "$_lcs_dest" "$_lcs_stage_name" "$_lcs_link_target" || true - restore_linked_skill_set "$_lcs_published" "$_lcs_backups" || true - rm -rf "$_lcs_stage" - return 1 - fi - _lcs_inode="$(skill_link_inode "$_lcs_dest")" || { - if skill_link_matches "$_lcs_dest" "$_lcs_link_target"; then rm -f "$_lcs_dest" || true; fi + # Keep the staged hardlink alive until the whole transaction finishes. It + # pins the inode, so a concurrently recreated link cannot reuse the + # identity recorded in the publication manifest. + _lcs_inode="$(skill_link_inode "$_lcs_staged")" || { restore_linked_skill_set "$_lcs_published" "$_lcs_backups" || true rm -rf "$_lcs_stage" return 1 } - if ! rm -f "$_lcs_staged"; then - if skill_link_matches "$_lcs_dest" "$_lcs_link_target" "$_lcs_inode"; then rm -f "$_lcs_dest" || true; fi + # Hard-link the staged symlink itself. This never overwrites a + # concurrently-created destination. If that destination became a + # directory, the unique staged basename can only be linked inside it; + # post-validation detects that case and removes only our link. + if ! ln -P "$_lcs_staged" "$_lcs_dest" 2>/dev/null || ! skill_link_matches "$_lcs_dest" "$_lcs_link_target" "$_lcs_inode"; then + cleanup_nested_staged_link "$_lcs_dest" "$_lcs_stage_name" "$_lcs_link_target" "$_lcs_inode" || true restore_linked_skill_set "$_lcs_published" "$_lcs_backups" || true rm -rf "$_lcs_stage" return 1