Files
dingtalk-workspace-cli/internal/upgrade/skill_publish_rename.go
T
玉澜 7581955892 fix(skills): make obsolete-copy retirement non-fatal and harden install
A universal Agent whose obsolete private copy cannot be retired installs
nothing there, yet every entry point counted that retirement failure as an
install failure — aborting `npm install`, `dws skill setup`, and the shell
installers even when the canonical store and all links published correctly,
and skipping the skills-state write. Route retirement failures to a separate
warning path across all surfaces (Go upgrade + skill setup, npm, PowerShell,
install.sh, install-skills.sh, install-event.sh, install-devapp.sh).

Also:
- Add a checked-rename fallback for filesystems that reject the atomic
  no-replace flag (NFS, FUSE, overlayfs); the no-clobber contract is kept and
  the previously unsupported platforms build and work.
- PowerShell multi-mode links only bundle skills, never the shared canonical
  store, so third-party/user skills are no longer fanned into every Agent root.
- Prune ~/.dws/skill-backups to the newest 5 on every surface; encode
  HOME-relative backup names on PowerShell to preserve origin.
- Add simulated-win32 junction coverage and rewrite the tautological
  no-replace test; remove dead code whose tests gave false coverage.
- Soften the overstated Windows ownership-proof comment (NTFS tunneling).
2026-08-15 14:26:11 +08:00

41 lines
1.6 KiB
Go

package upgrade
import (
"errors"
"fmt"
"os"
)
// errNoReplaceRenameUnsupported marks a platform that has no atomic no-replace
// rename primitive at all.
var errNoReplaceRenameUnsupported = errors.New("平台不支持原子 no-replace rename")
var skillPathRenameNoReplaceAtomic = renameSkillPathNoReplaceAtomic
// renameSkillPathNoReplace moves source onto destination and never replaces an
// object that already occupies destination.
//
// The atomic primitives (Linux RENAME_NOREPLACE, Darwin RENAME_EXCL) require
// support from the underlying filesystem and report EINVAL/ENOTSUP when it is
// missing — rename(2) lists only ext4, btrfs, tmpfs and cifs, so NFS, FUSE and
// overlayfs homes reject the flag. Failing there would abort the whole install
// on those filesystems, so they degrade to an explicit existence check plus a
// plain rename: the no-clobber contract is preserved, only its atomicity is
// lost. That check-then-rename window is the same exposure a plain rename
// always had, and callers additionally verify identity after publication.
func renameSkillPathNoReplace(source, destination string) error {
err := skillPathRenameNoReplaceAtomic(source, destination)
if err == nil {
return nil
}
if !errors.Is(err, errNoReplaceRenameUnsupported) && !isNoReplaceRenameUnsupported(err) {
return err
}
if _, statErr := skillPathLstat(destination); statErr == nil {
return &os.LinkError{Op: "rename", Old: source, New: destination, Err: os.ErrExist}
} else if !os.IsNotExist(statErr) {
return fmt.Errorf("检查发布目标失败 %s: %w", destination, statErr)
}
return skillPathRename(source, destination)
}