feat: 重构 pnpm-install action,优化缓存策略和错误提示
- 智能缓存前缀:自动使用项目名或子目录路径,避免 monorepo 冲突 - 优化缓存 hash:优先使用 pnpm-lock.yaml,支持 monorepo 多级目录 - 新增 strict-lockfile-check 参数,默认严格检查 lockfile 修改 - 改进错误提示:添加 emoji 和详细的解决方案说明 - 优化日志输出:使用结构化格式显示缓存信息和安装总结 - 默认启用项目本地 .pnpm-store 清
This commit is contained in:
+133
-54
@@ -6,9 +6,14 @@ branding:
|
||||
|
||||
inputs:
|
||||
cache-prefix:
|
||||
description: '缓存前缀名称'
|
||||
description: '缓存前缀名称(留空则自动使用项目名)'
|
||||
required: false
|
||||
default: 'modules'
|
||||
default: ''
|
||||
|
||||
cache-hash:
|
||||
description: '缓存hash值(留空则自动使用 pnpm-lock.yaml 或 package.json)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
force-install:
|
||||
description: '是否强制安装 (true/false)'
|
||||
@@ -25,15 +30,15 @@ inputs:
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
cache-hash:
|
||||
description: '缓存hash值(推荐使用hashFiles)'
|
||||
strict-lockfile-check:
|
||||
description: '严格检查 lockfile 是否被修改 (true/false)'
|
||||
required: false
|
||||
default: ''
|
||||
default: 'true'
|
||||
|
||||
clean-project-store:
|
||||
description: '安装后清理项目根目录的 .pnpm-store (true/false)'
|
||||
required: false
|
||||
default: 'false'
|
||||
default: 'true'
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
@@ -55,15 +60,17 @@ runs:
|
||||
id: detect
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v pnpm >/dev/null 2>&1; then
|
||||
echo "pnpm 未安装"
|
||||
echo "❌ pnpm 未安装,请先使用 pnpm/action-setup 安装" >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION=$(pnpm --version | tr -d '\n')
|
||||
VERSION=$(pnpm --version 2>/dev/null | tr -d '\n' | tr -d '\r')
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "无法获取pnpm版本"
|
||||
echo "❌ 无法获取 pnpm 版本" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 检测到 pnpm 版本: $VERSION"
|
||||
echo "pnpm-version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "PNPM_VERSION=${VERSION}" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -72,27 +79,61 @@ runs:
|
||||
shell: bash
|
||||
env:
|
||||
PNPM_VERSION: ${{ steps.detect.outputs.pnpm-version }}
|
||||
FALLBACK_HASH: ${{ hashFiles('pnpm-lock.yaml') }}
|
||||
PACKAGE_HASH: ${{ hashFiles('package.json') }}
|
||||
LOCKFILE_HASH: ${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
PACKAGE_HASH: ${{ hashFiles('**/package.json') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# 智能选择最佳 hash 策略
|
||||
if [[ -n "${{ inputs.cache-hash }}" ]]; then
|
||||
CACHE_HASH="${{ inputs.cache-hash }}"
|
||||
elif [[ -n "${FALLBACK_HASH}" ]]; then
|
||||
CACHE_HASH="${FALLBACK_HASH}"
|
||||
HASH_SOURCE="custom"
|
||||
elif [[ -n "${LOCKFILE_HASH}" ]]; then
|
||||
CACHE_HASH="${LOCKFILE_HASH}"
|
||||
HASH_SOURCE="pnpm-lock.yaml"
|
||||
elif [[ -n "${PACKAGE_HASH}" ]]; then
|
||||
CACHE_HASH="${PACKAGE_HASH}"
|
||||
HASH_SOURCE="package.json"
|
||||
else
|
||||
CACHE_HASH=""
|
||||
HASH_SOURCE="none"
|
||||
fi
|
||||
|
||||
# 生成简短的 hash 用于 key
|
||||
if [[ -n "$CACHE_HASH" ]]; then
|
||||
CACHE_HASH_SHORT=$(echo "$CACHE_HASH" | head -c 12)
|
||||
else
|
||||
CACHE_HASH_SHORT="no-hash"
|
||||
CACHE_HASH_SHORT="no-lock"
|
||||
fi
|
||||
|
||||
# 智能获取缓存前缀
|
||||
if [[ -n "${{ inputs.cache-prefix }}" ]]; then
|
||||
CACHE_PREFIX="${{ inputs.cache-prefix }}"
|
||||
else
|
||||
# 获取当前工作目录相对于仓库根目录的路径,避免 monorepo 子包冲突
|
||||
REPO_ROOT="$GITHUB_WORKSPACE"
|
||||
CURRENT_DIR="$(pwd)"
|
||||
|
||||
# 计算相对路径
|
||||
if [[ "$CURRENT_DIR" == "$REPO_ROOT" ]]; then
|
||||
# 在仓库根目录,使用仓库名
|
||||
CACHE_PREFIX=$(basename "$REPO_ROOT" 2>/dev/null || echo "project")
|
||||
else
|
||||
# 在子目录,使用相对路径(替换 / 为 -)
|
||||
REL_PATH="${CURRENT_DIR#$REPO_ROOT/}"
|
||||
CACHE_PREFIX=$(echo "$REL_PATH" | tr '/' '-')
|
||||
fi
|
||||
fi
|
||||
|
||||
# 构建缓存 key
|
||||
PNPM_VERSION="${PNPM_VERSION}"
|
||||
CACHE_KEY="${{ runner.os }}-pnpm-v${PNPM_VERSION}-store-${{ inputs.cache-prefix }}-${CACHE_HASH_SHORT}"
|
||||
RESTORE_PREFIX="${{ runner.os }}-pnpm-v${PNPM_VERSION}-store-${{ inputs.cache-prefix }}-"
|
||||
CACHE_KEY="${{ runner.os }}-${CACHE_PREFIX}-pnpm-v${PNPM_VERSION}-${CACHE_HASH_SHORT}"
|
||||
RESTORE_PREFIX="${{ runner.os }}-${CACHE_PREFIX}-pnpm-v${PNPM_VERSION}-"
|
||||
|
||||
echo "📝 缓存 key: ${CACHE_KEY}"
|
||||
echo "📝 Hash 来源: ${HASH_SOURCE} (${CACHE_HASH_SHORT})"
|
||||
echo "📝 缓存前缀: ${CACHE_PREFIX}"
|
||||
|
||||
echo "key=${CACHE_KEY}" >> "$GITHUB_OUTPUT"
|
||||
echo "restore-prefix=${RESTORE_PREFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "hash=${CACHE_HASH}" >> "$GITHUB_OUTPUT"
|
||||
@@ -107,15 +148,15 @@ runs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STORE_DIR_CANDIDATE=$(pnpm store path --silent 2>/dev/null || true)
|
||||
STORE_DIR_CANDIDATE=$(echo "$STORE_DIR_CANDIDATE" | tail -n1 | tr -d '\r')
|
||||
STORE_DIR_CANDIDATE=$(pnpm store path --silent 2>/dev/null | grep -v '^[[:space:]]*$' | tail -n1 | tr -d '\r\n')
|
||||
|
||||
if [[ -z "$STORE_DIR_CANDIDATE" ]]; then
|
||||
echo "❌ pnpm store path 未返回有效路径。可在运行前设置 PNPM_STORE_DIR=/path/to/store 或检查 pnpm 配置" >&2
|
||||
echo "❌ pnpm store path 未返回有效路径" >&2
|
||||
echo "💡 提示: 可在运行前设置 PNPM_HOME 环境变量或检查 pnpm 配置" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "pnpm store path: $STORE_DIR_CANDIDATE"
|
||||
echo "📦 pnpm store 路径: $STORE_DIR_CANDIDATE"
|
||||
echo "PNPM_STORE_DIR=${STORE_DIR_CANDIDATE}" >> "$GITHUB_ENV"
|
||||
echo "path=${STORE_DIR_CANDIDATE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -132,56 +173,94 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ steps.cache.outputs.cache-hit }}" == "true" ]]; then
|
||||
echo "缓存命中"
|
||||
echo "✅ 缓存完全命中"
|
||||
else
|
||||
echo "缓存未命中"
|
||||
echo "⚠️ 缓存未命中或部分命中,将从网络下载依赖"
|
||||
fi
|
||||
|
||||
- name: 安装依赖
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
STORE_DIR="${PNPM_STORE_DIR:-${{ steps.cache-path.outputs.path }}}"
|
||||
export PNPM_STORE_DIR="$STORE_DIR"
|
||||
export npm_config_store_dir="$PNPM_STORE_DIR"
|
||||
export PNPM_CONFIG_STORE_DIR="$PNPM_STORE_DIR"
|
||||
echo "📦 Using PNPM_STORE_DIR=$PNPM_STORE_DIR"
|
||||
|
||||
# 获取 store 路径(优先使用已配置的路径)
|
||||
STORE_PATH="${{ steps.cache-path.outputs.path }}"
|
||||
echo "📦 pnpm store 路径: $STORE_PATH"
|
||||
|
||||
# 记录安装前的 lockfile 状态
|
||||
LOCKFILE_BEFORE=""
|
||||
if [[ -f "pnpm-lock.yaml" ]]; then
|
||||
LOCKFILE_BEFORE=$(md5sum pnpm-lock.yaml 2>/dev/null || md5 pnpm-lock.yaml 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# 处理自定义安装命令
|
||||
if [[ -n "${{ inputs.install-command }}" ]]; then
|
||||
echo "🔧 使用自定义安装命令: ${{ inputs.install-command }}"
|
||||
eval "${{ inputs.install-command }}"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${{ steps.cache.outputs.cache-hit }}" == "true" ]]; then
|
||||
FLAGS=("--offline" "--frozen-lockfile")
|
||||
else
|
||||
# 构建安装参数
|
||||
FLAGS=("--prefer-offline" "--frozen-lockfile")
|
||||
|
||||
if [[ "${{ inputs.force-install }}" == "true" ]]; then
|
||||
FLAGS+=("--force")
|
||||
echo "⚠️ 强制重新安装模式"
|
||||
fi
|
||||
|
||||
INSTALL_ARGS="${{ inputs.install-args }}"
|
||||
|
||||
# 显示执行命令
|
||||
if [[ -n "$INSTALL_ARGS" ]]; then
|
||||
echo "🔧 执行: pnpm install ${FLAGS[*]} ${INSTALL_ARGS}"
|
||||
pnpm install "${FLAGS[@]}" ${INSTALL_ARGS}
|
||||
else
|
||||
echo "🔧 执行: pnpm install ${FLAGS[*]}"
|
||||
pnpm install "${FLAGS[@]}"
|
||||
fi
|
||||
fi
|
||||
if [[ "${{ inputs.force-install }}" == "true" ]]; then
|
||||
FLAGS+=("--force")
|
||||
|
||||
# 清理项目本地 .pnpm-store (如果存在且与全局 store 不同)
|
||||
if [[ "${{ inputs.clean-project-store }}" == "true" && -d ".pnpm-store" ]]; then
|
||||
GLOBAL_STORE=$(cd "$STORE_PATH" 2>/dev/null && pwd || echo "")
|
||||
LOCAL_STORE=$(cd .pnpm-store 2>/dev/null && pwd || echo "")
|
||||
|
||||
if [[ -n "$GLOBAL_STORE" && -n "$LOCAL_STORE" && "$GLOBAL_STORE" != "$LOCAL_STORE" ]]; then
|
||||
echo "🧹 清理项目本地 .pnpm-store 目录"
|
||||
rm -rf .pnpm-store || true
|
||||
fi
|
||||
fi
|
||||
INSTALL_ARGS="${{ inputs.install-args }}"
|
||||
echo "🔧 执行 pnpm install ${FLAGS[*]} ${INSTALL_ARGS}"
|
||||
if [[ -n "$INSTALL_ARGS" ]]; then
|
||||
pnpm install "${FLAGS[@]}" $INSTALL_ARGS
|
||||
else
|
||||
pnpm install "${FLAGS[@]}"
|
||||
fi
|
||||
if [[ "${{ inputs.clean-project-store }}" == "true" && -d ".pnpm-store" && "$(cd "$PNPM_STORE_DIR" 2>/dev/null && pwd)" != "$(cd .pnpm-store 2>/dev/null && pwd)" ]]; then
|
||||
rm -rf .pnpm-store || true
|
||||
fi
|
||||
echo "🧾 git status --short"
|
||||
CHANGES=$(git status --short || true)
|
||||
if [[ -n "$CHANGES" ]]; then
|
||||
echo "$CHANGES"
|
||||
echo "❌ 安装依赖后检测到工作区存在未提交变更。请检查上述文件,必要时更新配置或在调用前设置 PNPM_STORE_DIR。" >&2
|
||||
exit 1
|
||||
else
|
||||
echo "✅ 工作区保持干净"
|
||||
|
||||
# 严格检查 lockfile 是否被意外修改
|
||||
if [[ "${{ inputs.strict-lockfile-check }}" == "true" && -n "$LOCKFILE_BEFORE" ]]; then
|
||||
LOCKFILE_AFTER=""
|
||||
if [[ -f "pnpm-lock.yaml" ]]; then
|
||||
LOCKFILE_AFTER=$(md5sum pnpm-lock.yaml 2>/dev/null || md5 pnpm-lock.yaml 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [[ "$LOCKFILE_BEFORE" != "$LOCKFILE_AFTER" ]]; then
|
||||
echo "" >&2
|
||||
echo "❌ 检测到 pnpm-lock.yaml 在安装过程中被修改" >&2
|
||||
echo "" >&2
|
||||
echo "📋 变更内容:" >&2
|
||||
git diff pnpm-lock.yaml || true
|
||||
echo "" >&2
|
||||
echo "💡 原因: package.json 与 pnpm-lock.yaml 不同步" >&2
|
||||
echo "💡 解决: 在本地运行 'pnpm install' 并提交更新后的 lockfile" >&2
|
||||
echo "💡 或者: 设置 strict-lockfile-check: 'false' 跳过此检查" >&2
|
||||
echo "" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ 依赖安装完成"
|
||||
|
||||
- name: 总结
|
||||
shell: bash
|
||||
run: |
|
||||
echo "pnpm版本: $PNPM_VERSION"
|
||||
echo "缓存命中: ${{ steps.cache.outputs.cache-hit }}"
|
||||
echo "缓存key: ${{ steps.cache-key.outputs.key }}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📊 pnpm 安装总结"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " pnpm 版本: $PNPM_VERSION"
|
||||
echo " 缓存命中: ${{ steps.cache.outputs.cache-hit }}"
|
||||
echo " 缓存 key: ${{ steps.cache-key.outputs.key }}"
|
||||
echo " Store 路径: ${{ steps.cache-path.outputs.path }}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
Reference in New Issue
Block a user