From f37db72f861068caefdd2e7a770c2809e23222ac Mon Sep 17 00:00:00 2001 From: Lyda <1829913225@qq.com> Date: Wed, 11 Mar 2026 11:43:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=20pnpm-install=20act?= =?UTF-8?q?ion=EF=BC=8C=E4=BC=98=E5=8C=96=E7=BC=93=E5=AD=98=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E5=92=8C=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 智能缓存前缀:自动使用项目名或子目录路径,避免 monorepo 冲突 - 优化缓存 hash:优先使用 pnpm-lock.yaml,支持 monorepo 多级目录 - 新增 strict-lockfile-check 参数,默认严格检查 lockfile 修改 - 改进错误提示:添加 emoji 和详细的解决方案说明 - 优化日志输出:使用结构化格式显示缓存信息和安装总结 - 默认启用项目本地 .pnpm-store 清 --- pnpm-install/action.yml | 187 ++++++++++++++++++++++++++++------------ 1 file changed, 133 insertions(+), 54 deletions(-) diff --git a/pnpm-install/action.yml b/pnpm-install/action.yml index f1666b8..a3c69a6 100644 --- a/pnpm-install/action.yml +++ b/pnpm-install/action.yml @@ -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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"