feat: 重构 pnpm-install action,优化缓存策略和错误提示
- 智能缓存前缀:自动使用项目名或子目录路径,避免 monorepo 冲突 - 优化缓存 hash:优先使用 pnpm-lock.yaml,支持 monorepo 多级目录 - 新增 strict-lockfile-check 参数,默认严格检查 lockfile 修改 - 改进错误提示:添加 emoji 和详细的解决方案说明 - 优化日志输出:使用结构化格式显示缓存信息和安装总结 - 默认启用项目本地 .pnpm-store 清
This commit is contained in:
+125
-46
@@ -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 "pnpm版本: $PNPM_VERSION"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo "缓存命中: ${{ steps.cache.outputs.cache-hit }}"
|
echo "📊 pnpm 安装总结"
|
||||||
echo "缓存key: ${{ steps.cache-key.outputs.key }}"
|
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