feat: 重构 pnpm-install action,优化缓存策略和错误提示

- 智能缓存前缀:自动使用项目名或子目录路径,避免 monorepo 冲突
- 优化缓存 hash:优先使用 pnpm-lock.yaml,支持 monorepo 多级目录
- 新增 strict-lockfile-check 参数,默认严格检查 lockfile 修改
- 改进错误提示:添加 emoji 和详细的解决方案说明
- 优化日志输出:使用结构化格式显示缓存信息和安装总结
- 默认启用项目本地 .pnpm-store 清
This commit is contained in:
Lyda
2026-03-11 11:43:52 +08:00
parent 0535fe327b
commit f37db72f86
+122 -43
View File
@@ -6,9 +6,14 @@ branding:
inputs: inputs:
cache-prefix: cache-prefix:
description: '缓存前缀名称' description: '缓存前缀名称(留空则自动使用项目名)'
required: false required: false
default: 'modules' default: ''
cache-hash:
description: '缓存hash值(留空则自动使用 pnpm-lock.yaml 或 package.json'
required: false
default: ''
force-install: force-install:
description: '是否强制安装 (true/false)' description: '是否强制安装 (true/false)'
@@ -25,15 +30,15 @@ inputs:
required: false required: false
default: '' default: ''
cache-hash: strict-lockfile-check:
description: '缓存hash值(推荐使用hashFiles' description: '严格检查 lockfile 是否被修改 (true/false)'
required: false required: false
default: '' default: 'true'
clean-project-store: clean-project-store:
description: '安装后清理项目根目录的 .pnpm-store (true/false)' description: '安装后清理项目根目录的 .pnpm-store (true/false)'
required: false required: false
default: 'false' default: 'true'
outputs: outputs:
cache-hit: cache-hit:
@@ -55,15 +60,17 @@ runs:
id: detect id: detect
shell: bash shell: bash
run: | run: |
set -euo pipefail
if ! command -v pnpm >/dev/null 2>&1; then if ! command -v pnpm >/dev/null 2>&1; then
echo "pnpm 未安装" echo "pnpm 未安装,请先使用 pnpm/action-setup 安装" >&2
exit 1 exit 1
fi fi
VERSION=$(pnpm --version | tr -d '\n') VERSION=$(pnpm --version 2>/dev/null | tr -d '\n' | tr -d '\r')
if [[ -z "$VERSION" ]]; then if [[ -z "$VERSION" ]]; then
echo "无法获取pnpm版本" echo "无法获取 pnpm 版本" >&2
exit 1 exit 1
fi fi
echo "✅ 检测到 pnpm 版本: $VERSION"
echo "pnpm-version=${VERSION}" >> "$GITHUB_OUTPUT" echo "pnpm-version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "PNPM_VERSION=${VERSION}" >> "$GITHUB_ENV" echo "PNPM_VERSION=${VERSION}" >> "$GITHUB_ENV"
@@ -72,27 +79,61 @@ runs:
shell: bash shell: bash
env: env:
PNPM_VERSION: ${{ steps.detect.outputs.pnpm-version }} PNPM_VERSION: ${{ steps.detect.outputs.pnpm-version }}
FALLBACK_HASH: ${{ hashFiles('pnpm-lock.yaml') }} LOCKFILE_HASH: ${{ hashFiles('**/pnpm-lock.yaml') }}
PACKAGE_HASH: ${{ hashFiles('package.json') }} PACKAGE_HASH: ${{ hashFiles('**/package.json') }}
run: | run: |
set -euo pipefail set -euo pipefail
# 智能选择最佳 hash 策略
if [[ -n "${{ inputs.cache-hash }}" ]]; then if [[ -n "${{ inputs.cache-hash }}" ]]; then
CACHE_HASH="${{ inputs.cache-hash }}" CACHE_HASH="${{ inputs.cache-hash }}"
elif [[ -n "${FALLBACK_HASH}" ]]; then HASH_SOURCE="custom"
CACHE_HASH="${FALLBACK_HASH}" elif [[ -n "${LOCKFILE_HASH}" ]]; then
CACHE_HASH="${LOCKFILE_HASH}"
HASH_SOURCE="pnpm-lock.yaml"
elif [[ -n "${PACKAGE_HASH}" ]]; then elif [[ -n "${PACKAGE_HASH}" ]]; then
CACHE_HASH="${PACKAGE_HASH}" CACHE_HASH="${PACKAGE_HASH}"
HASH_SOURCE="package.json"
else else
CACHE_HASH="" CACHE_HASH=""
HASH_SOURCE="none"
fi fi
# 生成简短的 hash 用于 key
if [[ -n "$CACHE_HASH" ]]; then if [[ -n "$CACHE_HASH" ]]; then
CACHE_HASH_SHORT=$(echo "$CACHE_HASH" | head -c 12) CACHE_HASH_SHORT=$(echo "$CACHE_HASH" | head -c 12)
else else
CACHE_HASH_SHORT="no-hash" CACHE_HASH_SHORT="no-lock"
fi 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}" PNPM_VERSION="${PNPM_VERSION}"
CACHE_KEY="${{ runner.os }}-pnpm-v${PNPM_VERSION}-store-${{ inputs.cache-prefix }}-${CACHE_HASH_SHORT}" CACHE_KEY="${{ runner.os }}-${CACHE_PREFIX}-pnpm-v${PNPM_VERSION}-${CACHE_HASH_SHORT}"
RESTORE_PREFIX="${{ runner.os }}-pnpm-v${PNPM_VERSION}-store-${{ inputs.cache-prefix }}-" 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 "key=${CACHE_KEY}" >> "$GITHUB_OUTPUT"
echo "restore-prefix=${RESTORE_PREFIX}" >> "$GITHUB_OUTPUT" echo "restore-prefix=${RESTORE_PREFIX}" >> "$GITHUB_OUTPUT"
echo "hash=${CACHE_HASH}" >> "$GITHUB_OUTPUT" echo "hash=${CACHE_HASH}" >> "$GITHUB_OUTPUT"
@@ -107,15 +148,15 @@ runs:
exit 1 exit 1
fi fi
STORE_DIR_CANDIDATE=$(pnpm store path --silent 2>/dev/null || true) STORE_DIR_CANDIDATE=$(pnpm store path --silent 2>/dev/null | grep -v '^[[:space:]]*$' | tail -n1 | tr -d '\r\n')
STORE_DIR_CANDIDATE=$(echo "$STORE_DIR_CANDIDATE" | tail -n1 | tr -d '\r')
if [[ -z "$STORE_DIR_CANDIDATE" ]]; then 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 exit 1
fi fi
echo "pnpm store path: $STORE_DIR_CANDIDATE" echo "📦 pnpm store 路径: $STORE_DIR_CANDIDATE"
echo "PNPM_STORE_DIR=${STORE_DIR_CANDIDATE}" >> "$GITHUB_ENV" echo "PNPM_STORE_DIR=${STORE_DIR_CANDIDATE}" >> "$GITHUB_ENV"
echo "path=${STORE_DIR_CANDIDATE}" >> "$GITHUB_OUTPUT" echo "path=${STORE_DIR_CANDIDATE}" >> "$GITHUB_OUTPUT"
@@ -132,56 +173,94 @@ runs:
shell: bash shell: bash
run: | run: |
if [[ "${{ steps.cache.outputs.cache-hit }}" == "true" ]]; then if [[ "${{ steps.cache.outputs.cache-hit }}" == "true" ]]; then
echo "缓存命中" echo "✅ 缓存完全命中"
else else
echo "缓存未命中" echo "⚠️ 缓存未命中或部分命中,将从网络下载依赖"
fi fi
- name: 安装依赖 - name: 安装依赖
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
STORE_DIR="${PNPM_STORE_DIR:-${{ steps.cache-path.outputs.path }}}"
export PNPM_STORE_DIR="$STORE_DIR" # 获取 store 路径(优先使用已配置的路径)
export npm_config_store_dir="$PNPM_STORE_DIR" STORE_PATH="${{ steps.cache-path.outputs.path }}"
export PNPM_CONFIG_STORE_DIR="$PNPM_STORE_DIR" echo "📦 pnpm store 路径: $STORE_PATH"
echo "📦 Using PNPM_STORE_DIR=$PNPM_STORE_DIR"
# 记录安装前的 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 if [[ -n "${{ inputs.install-command }}" ]]; then
echo "🔧 使用自定义安装命令: ${{ inputs.install-command }}" echo "🔧 使用自定义安装命令: ${{ inputs.install-command }}"
eval "${{ inputs.install-command }}" eval "${{ inputs.install-command }}"
exit 0
fi
if [[ "${{ steps.cache.outputs.cache-hit }}" == "true" ]]; then
FLAGS=("--offline" "--frozen-lockfile")
else else
# 构建安装参数
FLAGS=("--prefer-offline" "--frozen-lockfile") FLAGS=("--prefer-offline" "--frozen-lockfile")
fi
if [[ "${{ inputs.force-install }}" == "true" ]]; then if [[ "${{ inputs.force-install }}" == "true" ]]; then
FLAGS+=("--force") FLAGS+=("--force")
echo "⚠️ 强制重新安装模式"
fi fi
INSTALL_ARGS="${{ inputs.install-args }}" INSTALL_ARGS="${{ inputs.install-args }}"
echo "🔧 执行 pnpm install ${FLAGS[*]} ${INSTALL_ARGS}"
# 显示执行命令
if [[ -n "$INSTALL_ARGS" ]]; then if [[ -n "$INSTALL_ARGS" ]]; then
pnpm install "${FLAGS[@]}" $INSTALL_ARGS echo "🔧 执行: pnpm install ${FLAGS[*]} ${INSTALL_ARGS}"
pnpm install "${FLAGS[@]}" ${INSTALL_ARGS}
else else
echo "🔧 执行: pnpm install ${FLAGS[*]}"
pnpm install "${FLAGS[@]}" pnpm install "${FLAGS[@]}"
fi 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 fi
# 清理项目本地 .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 rm -rf .pnpm-store || true
fi 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 "✅ 工作区保持干净"
fi fi
# 严格检查 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: 总结 - name: 总结
shell: bash shell: bash
run: | run: |
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 pnpm 安装总结"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " pnpm 版本: $PNPM_VERSION" echo " pnpm 版本: $PNPM_VERSION"
echo " 缓存命中: ${{ steps.cache.outputs.cache-hit }}" echo " 缓存命中: ${{ steps.cache.outputs.cache-hit }}"
echo " 缓存 key: ${{ steps.cache-key.outputs.key }}" echo " 缓存 key: ${{ steps.cache-key.outputs.key }}"
echo " Store 路径: ${{ steps.cache-path.outputs.path }}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"