Files
xgj/pnpm-install/action.yml
T
Lyda f37db72f86 feat: 重构 pnpm-install action,优化缓存策略和错误提示
- 智能缓存前缀:自动使用项目名或子目录路径,避免 monorepo 冲突
- 优化缓存 hash:优先使用 pnpm-lock.yaml,支持 monorepo 多级目录
- 新增 strict-lockfile-check 参数,默认严格检查 lockfile 修改
- 改进错误提示:添加 emoji 和详细的解决方案说明
- 优化日志输出:使用结构化格式显示缓存信息和安装总结
- 默认启用项目本地 .pnpm-store 清
2026-03-11 11:43:52 +08:00

267 lines
9.4 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: 'pnpm依赖安装与缓存'
description: '专注于pnpm的依赖缓存与安装,加速重复执行'
branding:
icon: 'package'
color: 'yellow'
inputs:
cache-prefix:
description: '缓存前缀名称(留空则自动使用项目名)'
required: false
default: ''
cache-hash:
description: '缓存hash值(留空则自动使用 pnpm-lock.yaml 或 package.json'
required: false
default: ''
force-install:
description: '是否强制安装 (true/false)'
required: false
default: 'false'
install-command:
description: '自定义安装命令(若设置则完全覆盖默认命令)'
required: false
default: ''
install-args:
description: '附加到默认安装命令的参数(当未提供 install-command 时生效)'
required: false
default: ''
strict-lockfile-check:
description: '严格检查 lockfile 是否被修改 (true/false)'
required: false
default: 'true'
clean-project-store:
description: '安装后清理项目根目录的 .pnpm-store (true/false)'
required: false
default: 'true'
outputs:
cache-hit:
description: '是否命中缓存 (true/false)'
value: ${{ steps.cache.outputs.cache-hit }}
cache-key:
description: '使用的缓存key'
value: ${{ steps.cache-key.outputs.key }}
cache-path:
description: '缓存路径(用于调试与复用)'
value: ${{ steps.cache-path.outputs.path }}
runs:
using: 'composite'
steps:
- name: 检查pnpm
id: detect
shell: bash
run: |
set -euo pipefail
if ! command -v pnpm >/dev/null 2>&1; then
echo "❌ pnpm 未安装,请先使用 pnpm/action-setup 安装" >&2
exit 1
fi
VERSION=$(pnpm --version 2>/dev/null | tr -d '\n' | tr -d '\r')
if [[ -z "$VERSION" ]]; then
echo "❌ 无法获取 pnpm 版本" >&2
exit 1
fi
echo "✅ 检测到 pnpm 版本: $VERSION"
echo "pnpm-version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "PNPM_VERSION=${VERSION}" >> "$GITHUB_ENV"
- name: 生成缓存key
id: cache-key
shell: bash
env:
PNPM_VERSION: ${{ steps.detect.outputs.pnpm-version }}
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 }}"
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-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 }}-${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"
- name: 确定缓存路径
id: cache-path
shell: bash
run: |
set -euo pipefail
if ! command -v pnpm >/dev/null 2>&1; then
echo "❌ 未找到 pnpm,请先通过 pnpm/action-setup 安装" >&2
exit 1
fi
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 未返回有效路径" >&2
echo "💡 提示: 可在运行前设置 PNPM_HOME 环境变量或检查 pnpm 配置" >&2
exit 1
fi
echo "📦 pnpm store 路径: $STORE_DIR_CANDIDATE"
echo "PNPM_STORE_DIR=${STORE_DIR_CANDIDATE}" >> "$GITHUB_ENV"
echo "path=${STORE_DIR_CANDIDATE}" >> "$GITHUB_OUTPUT"
- name: 拉取缓存
id: cache
uses: actions/cache@v4
with:
path: ${{ steps.cache-path.outputs.path }}
key: ${{ steps.cache-key.outputs.key }}
restore-keys: |
${{ steps.cache-key.outputs.restore-prefix }}
- name: 显示缓存状态
shell: bash
run: |
if [[ "${{ steps.cache.outputs.cache-hit }}" == "true" ]]; then
echo "✅ 缓存完全命中"
else
echo "⚠️ 缓存未命中或部分命中,将从网络下载依赖"
fi
- name: 安装依赖
shell: bash
run: |
set -euo pipefail
# 获取 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 }}"
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
# 清理项目本地 .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
# 严格检查 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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"