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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"