Files
dingtalk-workspace-cli/scripts/release/verify-package-managers.sh
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

386 lines
14 KiB
Bash
Executable File

#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
DIST_DIR="${DWS_PACKAGE_DIST_DIR:-$ROOT/dist}"
FORMULA_PATH="$DIST_DIR/homebrew/dingtalk-workspace-cli-local.rb"
NPM_STAGE_DIR="$DIST_DIR/npm/dingtalk-workspace-cli"
RUN_NPM=1
RUN_BREW=1
EXPECTED_VERSION=""
VERIFY_SKILL_TARGETS=1
say() {
printf '%s\n' "$*"
}
err() {
printf 'error: %s\n' "$*" >&2
exit 1
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || err "missing required command: $1"
}
need_file() {
[ -f "$1" ] || err "required file not found: $1"
}
usage() {
printf '%s\n' "usage: $0 [--npm-only|--brew-only] [--expected-version <vX.Y.Z>] [--skip-skill-targets]" >&2
}
mode_seen=0
while [ "$#" -gt 0 ]; do
case "$1" in
--npm-only)
[ "$mode_seen" -eq 0 ] || { usage; exit 2; }
RUN_NPM=1; RUN_BREW=0; mode_seen=1; shift
;;
--brew-only)
[ "$mode_seen" -eq 0 ] || { usage; exit 2; }
RUN_NPM=0; RUN_BREW=1; mode_seen=1; shift
;;
--expected-version)
[ "$#" -ge 2 ] || { usage; exit 2; }
EXPECTED_VERSION="${2#v}"
shift 2
;;
--skip-skill-targets) VERIFY_SKILL_TARGETS=0; shift ;;
-h|--help) usage; exit 0 ;;
*) usage; exit 2 ;;
esac
done
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-package-verify-XXXXXX")"
# vercel-labs/skills agents.ts (c6f69c6), exactly 76 upstream IDs.
# Format: id|universal(1/0)|effective default global root; '-' has no global root.
UPSTREAM_AGENT_REGISTRY='aider-desk|0|.aider-desk/skills
amp|1|.config/agents/skills
antigravity|1|.gemini/antigravity/skills
antigravity-cli|1|.gemini/antigravity-cli/skills
astrbot|0|.astrbot/data/skills
autohand-code|0|.autohand/skills
augment|0|.augment/skills
bob|0|.bob/skills
claude-code|0|.claude/skills
openclaw|0|.openclaw/skills
cline|1|.agents/skills
codearts-agent|0|.codeartsdoer/skills
codebuddy|0|.codebuddy/skills
codemaker|0|.codemaker/skills
codestudio|0|.codestudio/skills
codex|1|.codex/skills
command-code|0|.commandcode/skills
continue|0|.continue/skills
cortex|0|.snowflake/cortex/skills
crush|0|.config/crush/skills
cursor|1|.cursor/skills
deepagents|1|.deepagents/agent/skills
devin|0|.config/devin/skills
dexto|1|.agents/skills
droid|0|.factory/skills
eve|0|-
firebender|1|.firebender/skills
forgecode|0|.forge/skills
gemini-cli|1|.gemini/skills
github-copilot|1|.copilot/skills
goose|0|.config/goose/skills
grok|0|.grok/skills
hermes-agent|0|.hermes/skills
inference-sh|0|.inferencesh/skills
jazz|0|.jazz/skills
junie|0|.junie/skills
iflow-cli|0|.iflow/skills
kilo|0|.kilocode/skills
kimchi|0|.config/kimchi/harness/skills
kimi-code-cli|1|.agents/skills
kiro-cli|0|.kiro/skills
kode|0|.kode/skills
lingma|0|.lingma/skills
loaf|1|.agents/skills
mcpjam|0|.mcpjam/skills
minimax-code|0|.minimax/skills
mistral-vibe|0|.vibe/skills
moxby|0|.moxby/skills
mux|0|.mux/skills
opencode|1|.config/opencode/skills
openhands|0|.openhands/skills
ona|0|.ona/skills
pi|0|.pi/agent/skills
qoder|0|.qoder/skills
qoder-cn|0|.qoder-cn/skills
qwen-code|0|.qwen/skills
replit|1|.config/agents/skills
reasonix|0|.reasonix/skills
rovodev|0|.rovodev/skills
roo|0|.roo/skills
tabnine-cli|0|.tabnine/agent/skills
terramind|0|.terramind/skills
tinycloud|0|.tinycloud/skills
trae|0|.trae/skills
trae-cn|0|.trae-cn/skills
warp|1|.agents/skills
windsurf|0|.codeium/windsurf/skills
zed|1|.agents/skills
zcode|0|.zcode/skills
zencoder|0|.zencoder/skills
zenflow|0|.zencoder/skills
neovate|0|.neovate/skills
pochi|0|.pochi/skills
promptscript|1|-
adal|0|.adal/skills
universal|1|.config/agents/skills'
# DWS-only compatibility and old wrong global paths. Qoderwork remains a
# non-universal install target; the other entries verify migration cleanup.
LEGACY_AGENT_CLEANUP_REGISTRY='dws-qoderwork|0|.qoderwork/skills
dws-legacy-github|1|.github/skills
dws-legacy-amp|1|.amp/skills
dws-legacy-cline|1|.cline/skills
dws-legacy-windsurf|1|.windsurf/skills'
cleanup() {
if [ "$RUN_BREW" -eq 1 ] && command -v brew >/dev/null 2>&1 && [ -n "${BREW_TAP_NAME:-}" ]; then
HOME="$TMP_ROOT/brew-home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
brew uninstall --force dingtalk-workspace-cli-local >/dev/null 2>&1 || true
HOME="$TMP_ROOT/brew-home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
brew untap --force "$BREW_TAP_NAME" >/dev/null 2>&1 || true
fi
rm -rf "$TMP_ROOT"
}
trap cleanup EXIT INT TERM
seed_specific_agent_homes() {
home_root="$1"
seen=''
for row in $UPSTREAM_AGENT_REGISTRY $LEGACY_AGENT_CLEANUP_REGISTRY; do
id=${row%%|*}; rest=${row#*|}; path=${rest#*|}
[ "$path" != '-' ] || continue
base=$(resolve_agent_base "$home_root" "$id" "$path")
[ -n "$base" ] || continue
key=$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')
printf '%s\n' "$seen" | grep -Fqx "$key" && continue
seen="${seen}${key}
"
mkdir -p "${base%/skills}"
done
}
resolve_agent_base() {
home_root="$1"; id="$2"; default_path="$3"
case "$id" in
autohand-code) printf '%s\n' "${AUTOHAND_HOME:-$home_root/.autohand}/skills" ;;
claude-code) printf '%s\n' "${CLAUDE_CONFIG_DIR:-$home_root/.claude}/skills" ;;
codex) printf '%s\n' "${CODEX_HOME:-$home_root/.codex}/skills" ;;
grok) printf '%s\n' "${GROK_HOME:-$home_root/.grok}/skills" ;;
hermes-agent) printf '%s\n' "${HERMES_HOME:-$home_root/.hermes}/skills" ;;
mistral-vibe) printf '%s\n' "${VIBE_HOME:-$home_root/.vibe}/skills" ;;
amp|replit|universal) printf '%s\n' "${XDG_CONFIG_HOME:-$home_root/.config}/agents/skills" ;;
crush|devin|goose|kimchi|opencode) printf '%s\n' "${XDG_CONFIG_HOME:-$home_root/.config}/${default_path#.config/}" ;;
openclaw)
for alias in .openclaw .clawdbot .moltbot; do
if [ -d "$home_root/$alias" ]; then printf '%s\n' "$home_root/$alias/skills"; return; fi
done
printf '%s\n' "$home_root/.openclaw/skills"
;;
*) printf '%s\n' "$home_root/$default_path" ;;
esac
}
# Package verification must never inherit the runner's real Agent roots. In
# particular, hosted Linux runners can define XDG_CONFIG_HOME globally; two
# parallel verification jobs would then publish links into the same directory
# and make one another's links appear broken. Keep every supported override
# inside the scenario-specific temporary HOME instead.
with_isolated_agent_env() (
isolated_home=$1
shift
HOME="$isolated_home"
AUTOHAND_HOME="$isolated_home/.autohand"
CLAUDE_CONFIG_DIR="$isolated_home/.claude"
CODEX_HOME="$isolated_home/.codex"
GROK_HOME="$isolated_home/.grok"
HERMES_HOME="$isolated_home/.hermes"
VIBE_HOME="$isolated_home/.vibe"
XDG_CONFIG_HOME="$isolated_home/.config"
export HOME AUTOHAND_HOME CLAUDE_CONFIG_DIR CODEX_HOME GROK_HOME HERMES_HOME VIBE_HOME XDG_CONFIG_HOME
"$@"
)
verify_skill_base() {
home_root="$1"
base="$2"
need_file "$home_root/$base/dingtalk-shared/SKILL.md"
need_file "$home_root/$base/dingtalk-misc/SKILL.md"
[ ! -e "$home_root/$base/dws" ] || err "unexpected mono Skill layout found in $home_root/$base/dws"
}
verify_compatible_skill_base() {
home_root="$1"; base="$2"; canonical="$home_root/.agents/skills"
for skill_md in "$canonical"/*/SKILL.md; do
[ -f "$skill_md" ] || continue
name=$(basename "$(dirname "$skill_md")")
target="$base/$name"
if [ -L "$target" ]; then
link_target="$(readlink "$target")" || err "could not read canonical Skill link at $target"
# Relative link targets are the headline invariant of the canonical store:
# an absolute target breaks as soon as the home directory moves or the
# store is relocated, so reject it even though it resolves today.
case "$link_target" in
/*) err "canonical Skill link must be relative at $target (target=$link_target)" ;;
esac
linked_real="$(CDPATH= cd -- "$target" 2>/dev/null && pwd -P)" || err "broken canonical Skill link at $target"
canonical_real="$(CDPATH= cd -- "$canonical/$name" && pwd -P)" || err "canonical Skill missing at $canonical/$name"
[ "$linked_real" = "$canonical_real" ] || err "Skill link does not resolve to canonical source at $target"
elif [ -d "$target" ]; then
diff -qr "$canonical/$name" "$target" >/dev/null || err "Skill copy fallback differs from canonical source at $target"
else
err "canonical Skill compatibility target missing at $target"
fi
done
[ ! -e "$base/dws" ] && [ ! -L "$base/dws" ] || err "unexpected mono Skill layout found in $base/dws"
}
# Universal Agents read the canonical store directly, so they must own no copy
# of ANY canonical Skill. Iterate the real store instead of a hardcoded name
# list: a hardcoded list silently passed leftover copies of every other
# dingtalk-* Skill from earlier betas.
verify_universal_skill_base() {
home_root="$1"; base="$2"; canonical="$home_root/.agents/skills"
for skill_md in "$canonical"/*/SKILL.md; do
[ -f "$skill_md" ] || continue
name=$(basename "$(dirname "$skill_md")")
{ [ ! -e "$base/$name" ] && [ ! -L "$base/$name" ]; } || \
err "unexpected duplicate Skill found in universal Agent root $base/$name"
done
# The mono layout is absent from a multi canonical store, so gate it directly.
{ [ ! -e "$base/dws" ] && [ ! -L "$base/dws" ]; } || \
err "unexpected duplicate Skill found in universal Agent root $base/dws"
}
verify_skill_targets() {
home_root="$1"
# The universal-convention canonical store is always present, regardless of
# which concrete Agent homes existed at install time.
verify_skill_base "$home_root" ".agents/skills"
seen=''
for row in $UPSTREAM_AGENT_REGISTRY $LEGACY_AGENT_CLEANUP_REGISTRY; do
id=${row%%|*}; rest=${row#*|}; universal=${rest%%|*}; path=${rest#*|}
[ "$path" != '-' ] || continue
base=$(resolve_agent_base "$home_root" "$id" "$path")
[ -n "$base" ] || continue
key=$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')
printf '%s\n' "$seen" | grep -Fqx "$key" && continue
seen="${seen}${key}
"
[ "$base" != "$home_root/.agents/skills" ] || continue
[ -d "${base%/skills}" ] || continue
if [ "$universal" -eq 1 ]; then
verify_universal_skill_base "$home_root" "$base"
continue
fi
verify_compatible_skill_base "$home_root" "$base"
done
}
verify_npm_install() {
tarball_path="$1"
scenario="$2"
npm_home="$TMP_ROOT/npm-home-$scenario"
npm_prefix="$TMP_ROOT/npm-prefix-$scenario"
npm_cache="$TMP_ROOT/npm-cache-$scenario"
mkdir -p "$npm_home" "$npm_prefix" "$npm_cache"
if [ "$scenario" = "specific-agent-roots" ]; then
with_isolated_agent_env "$npm_home" seed_specific_agent_homes "$npm_home"
fi
say "==> verifying npm package install ($scenario)"
with_isolated_agent_env "$npm_home" env npm_config_cache="$npm_cache" npm_config_prefix="$npm_prefix" \
npm install -g "$tarball_path" >/dev/null
[ -x "$npm_prefix/bin/dws" ] || err "npm install did not expose dws in $npm_prefix/bin"
with_isolated_agent_env "$npm_home" "$npm_prefix/bin/dws" --help >/dev/null
if [ -n "$EXPECTED_VERSION" ]; then
vendor_bin="$npm_prefix/lib/node_modules/dingtalk-workspace-cli/vendor/dws"
need_file "$vendor_bin"
LC_ALL=C grep -aFq "v$EXPECTED_VERSION" "$vendor_bin" || \
err "npm-installed binary does not contain expected version marker v$EXPECTED_VERSION"
EXPECTED_VERSION="$EXPECTED_VERSION" node -e '
const pkg = require(process.argv[1]);
if (pkg.version !== process.env.EXPECTED_VERSION) process.exit(1);
' "$NPM_STAGE_DIR/package.json" || err "npm package.json version mismatch"
fi
[ "$VERIFY_SKILL_TARGETS" -eq 0 ] || with_isolated_agent_env "$npm_home" verify_skill_targets "$npm_home"
with_isolated_agent_env "$npm_home" env npm_config_cache="$npm_cache" npm_config_prefix="$npm_prefix" \
npm uninstall -g dingtalk-workspace-cli >/dev/null
}
verify_npm() {
need_cmd npm
need_cmd node
need_cmd tar
need_cmd unzip
need_cmd diff
need_file "$NPM_STAGE_DIR/package.json"
tarball_name="$(
cd "$NPM_STAGE_DIR"
HOME="$TMP_ROOT/npm-pack-home" npm_config_cache="$TMP_ROOT/npm-pack-cache" npm pack --silent
)"
tarball_path="$NPM_STAGE_DIR/$tarball_name"
need_file "$tarball_path"
verify_npm_install "$tarball_path" "specific-agent-roots"
verify_npm_install "$tarball_path" "generic-fallback"
rm -f "$tarball_path"
}
verify_brew() {
need_cmd brew
need_file "$FORMULA_PATH"
brew_home="$TMP_ROOT/brew-home"
mkdir -p "$brew_home"
BREW_TAP_NAME="local/dws-package-verify-$$"
say "==> verifying Homebrew formula install"
HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
brew uninstall --force dingtalk-workspace-cli-local >/dev/null 2>&1 || true
HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
brew untap --force "$BREW_TAP_NAME" >/dev/null 2>&1 || true
HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
brew tap-new --no-git "$BREW_TAP_NAME" >/dev/null
tap_repo="$(
HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
brew --repository "$BREW_TAP_NAME"
)"
mkdir -p "$tap_repo/Formula"
cp "$FORMULA_PATH" "$tap_repo/Formula/dingtalk-workspace-cli-local.rb"
HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
brew install -y "$BREW_TAP_NAME/dingtalk-workspace-cli-local" >/dev/null
prefix="$(
HOME="$brew_home" HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 \
brew --prefix dingtalk-workspace-cli-local
)"
[ -x "$prefix/bin/dws" ] || err "brew install did not create $prefix/bin/dws"
"$prefix/bin/dws" --help >/dev/null
if [ -n "$EXPECTED_VERSION" ]; then
LC_ALL=C grep -aFq "v$EXPECTED_VERSION" "$prefix/bin/dws" || \
err "Homebrew-installed binary does not contain expected version marker v$EXPECTED_VERSION"
fi
need_file "$prefix/share/dingtalk-workspace-cli-local/skills/dws/SKILL.md"
}
need_file "$DIST_DIR/dws-skills.zip"
[ "$RUN_NPM" -eq 0 ] || verify_npm
[ "$RUN_BREW" -eq 0 ] || verify_brew
say "Package-manager verification complete."