Files
xgj/npm-install/action.yml

339 lines
13 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: 'npm依赖安装与缓存'
description: '自动缓存和安装npm依赖支持多种包管理器'
branding:
icon: 'package'
color: 'blue'
inputs:
package-manager:
description: '包管理器类型 (npm, pnpm, yarn)'
required: false
default: 'pnpm'
pnpm-version:
description: 'pnpm 版本(当 package-manager=pnpm 时生效)'
required: false
default: '10'
skip-pnpm-setup:
description: '当 package-manager=pnpm 时是否跳过 pnpm/action-setup (true/false)'
required: false
default: 'false'
cache-mode:
description: '缓存模式node_modules 或 store'
required: false
default: 'store'
optimize-install-flags:
description: '是否启用安装参数优化pnpm+store时自动使用 --offline/--prefer-offline 与 --frozen-lockfile(true/false)'
required: false
default: 'true'
cache-prefix:
description: '缓存前缀名称'
required: false
default: 'modules'
node-modules-path:
description: 'node_modules目录路径'
required: false
default: 'node_modules'
force-install:
description: '是否强制安装 (true/false)'
required: false
default: 'false'
enable-git-stash:
description: '安装后是否执行git stash (true/false)'
required: false
default: 'false'
install-command:
description: '自定义安装命令(可选,会覆盖默认命令)'
required: false
default: ''
install-args:
description: '附加到默认安装命令的参数(当未提供 install-command 时生效)'
required: false
default: ''
clean-project-store:
description: '在安装前清理项目根的 .pnpm-store 残留true/false'
required: false
default: 'false'
cache-hash:
description: '缓存hash值推荐使用hashFiles计算'
required: false
default: ""
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: 生成缓存key
id: cache-key
shell: bash
env:
FALLBACK_HASH: ${{ hashFiles('package.json') }}
run: |
# 确定使用的hash值
if [[ -n "${{ inputs.cache-hash }}" && "${{ inputs.cache-hash }}" != "" ]]; then
CACHE_HASH="${{ inputs.cache-hash }}"
echo "✅ 使用用户传入的hash值"
elif [[ -n "${FALLBACK_HASH}" && "${FALLBACK_HASH}" != "" ]]; then
CACHE_HASH="${FALLBACK_HASH}"
echo "📝 未提供hash使用package.json作为fallback"
else
CACHE_HASH=""
echo "⚠️ 警告: 无法获取hash值使用默认值"
fi
# 截取hash的前12位
if [[ -n "${CACHE_HASH}" && "${CACHE_HASH}" != "" ]]; then
CACHE_HASH_SHORT=$(echo "${CACHE_HASH}" | head -c 12)
echo "✅ 成功计算缓存hash: ${CACHE_HASH_SHORT}"
else
CACHE_HASH_SHORT="no-hash"
echo "⚠️ 使用默认hash值: ${CACHE_HASH_SHORT}"
fi
# 生成包管理器后缀,避免不同包管理器/版本的缓存互相污染
MANAGER="${{ inputs.package-manager }}"
MANAGER_SUFFIX="$MANAGER"
if [[ "$MANAGER" == "pnpm" && -n "${{ inputs.pnpm-version }}" && "${{ inputs.pnpm-version }}" != "" ]]; then
MANAGER_SUFFIX="${MANAGER}-v${{ inputs.pnpm-version }}"
fi
# 模式后缀隔离不同缓存模式node_modules vs store
MODE_SUFFIX="${{ inputs.cache-mode }}"
if [[ -z "$MODE_SUFFIX" ]]; then MODE_SUFFIX="node_modules"; fi
# 构建缓存key<OS>-<manager[-vX]>-<mode>-<prefix>-<hash>
CACHE_KEY="${{ runner.os }}-${MANAGER_SUFFIX}-${MODE_SUFFIX}-${{ inputs.cache-prefix }}-${CACHE_HASH_SHORT}"
# 恢复前缀:用于 restore-keys防止不同包管理器/模式的回退误命中
RESTORE_PREFIX="${{ runner.os }}-${MANAGER_SUFFIX}-${MODE_SUFFIX}-${{ inputs.cache-prefix }}-"
echo "key=${CACHE_KEY}" >> $GITHUB_OUTPUT
echo "restore-prefix=${RESTORE_PREFIX}" >> $GITHUB_OUTPUT
echo "使用hash: ${CACHE_HASH}"
echo "缓存key: ${CACHE_KEY}"
- name: 确保 pnpm 可用
if: inputs.package-manager == 'pnpm' && inputs.skip-pnpm-setup != 'true'
uses: pnpm/action-setup@v4
with:
version: ${{ inputs.pnpm-version }}
run_install: false
- name: 确定缓存路径
id: cache-path
shell: bash
run: |
MODE="${{ inputs.cache-mode }}"
MANAGER="${{ inputs.package-manager }}"
if [[ -z "$MODE" ]]; then MODE="node_modules"; fi
if [[ "$MODE" == "node_modules" ]]; then
CACHE_PATH="${{ inputs.node-modules-path }}"
# 即使只缓存 node_modulespnpm 也会使用 store。为避免在项目根生成 .pnpm-store这里同样固定 PNPM_STORE_DIR
if [[ "$MANAGER" == "pnpm" ]]; then
DEFAULT_PNPM_STORE="${RUNNER_TEMP:-$HOME}/.pnpm-store"
echo "PNPM_STORE_DIR=${DEFAULT_PNPM_STORE}" >> "$GITHUB_ENV"
fi
else
case "$MANAGER" in
"npm")
# npm 的全局缓存目录
CACHE_PATH="$HOME/.npm"
;;
"pnpm")
# 固定 pnpm store 路径到 runner 的临时目录或 HOME避免在项目根生成 .pnpm-store
# 说明:一些仓库的 .npmrc 可能配置了 store-dir=.pnpm-store会导致在工作目录创建 .pnpm-store
# 这里通过设置 PNPM_STORE_DIR 环境变量进行覆盖,确保缓存路径稳定可控
DEFAULT_PNPM_STORE="${RUNNER_TEMP:-$HOME}/.pnpm-store"
# 将目录导出到环境,供后续安装步骤使用
echo "PNPM_STORE_DIR=${DEFAULT_PNPM_STORE}" >> "$GITHUB_ENV"
CACHE_PATH="${DEFAULT_PNPM_STORE}"
;;
"yarn")
# yarn v1 默认缓存目录yarn berry 采用不同机制,这里聚焦 v1 常见场景)
CACHE_PATH="$HOME/.cache/yarn"
;;
*)
echo "❌ 不支持的包管理器: $MANAGER"
exit 1
;;
esac
fi
# 打印最终缓存目录,便于调试与确认
echo "📁 最终缓存目录: ${CACHE_PATH}"
if [[ "$MANAGER" == "pnpm" ]]; then
echo "📦 PNPM_STORE_DIR=${PNPM_STORE_DIR:-$DEFAULT_PNPM_STORE}"
fi
echo "path=${CACHE_PATH}" >> $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
if [[ "${{ inputs.cache-mode }}" == "store" ]]; then
echo "✅ 缓存命中store将执行快速链接安装不会下载包仅链接"
else
echo "✅ 缓存命中,跳过依赖安装"
fi
else
echo "⚠️ 缓存未命中,开始安装依赖"
fi
- name: 安装依赖
if: (inputs.cache-mode == 'node_modules' && steps.cache.outputs.cache-hit != 'true') || (inputs.cache-mode == 'store')
shell: bash
run: |
# 若使用 pnpm在本步骤内始终显式设置 PNPM_STORE_DIR覆盖可能存在的相对配置
if [[ "${{ inputs.package-manager }}" == "pnpm" ]]; then
if [[ "${{ inputs.cache-mode }}" == "store" ]]; then
# store 模式cache-path 的 path 即为期望的 store 目录
export PNPM_STORE_DIR="${{ steps.cache-path.outputs.path }}"
else
# node_modules 模式:不要回退到 cache-path那是 node_modules 目录),而是使用 RUNNER_TEMP/HOME
export PNPM_STORE_DIR="${PNPM_STORE_DIR:-${RUNNER_TEMP:-$HOME}/.pnpm-store}"
fi
# 通过多通道环境变量覆盖(兼容不同版本/解析顺序)
export npm_config_store_dir="${PNPM_STORE_DIR}"
export PNPM_CONFIG_STORE_DIR="${PNPM_STORE_DIR}"
echo "🧩 已设置 PNPM_STORE_DIR=${PNPM_STORE_DIR}"
echo "🔎 pnpm 配置: store-dir=$(pnpm config get store-dir || echo '<unknown>')"
fi
# 如果提供了自定义安装命令,使用自定义命令
if [[ -n "${{ inputs.install-command }}" ]]; then
echo "🔧 使用自定义安装命令: ${{ inputs.install-command }}"
${{ inputs.install-command }}
else
INSTALL_ARGS="${{ inputs.install-args }}"
if [[ -n "$INSTALL_ARGS" ]]; then
echo " 附加安装参数: $INSTALL_ARGS"
fi
# 根据模式与缓存命中优化安装参数(尽量离线加速)
EXTRA_FLAGS=""
if [[ "${{ inputs.optimize-install-flags }}" == "true" && "${{ inputs.package-manager }}" == "pnpm" && "${{ inputs.cache-mode }}" == "store" ]]; then
if [[ "${{ steps.cache.outputs.cache-hit }}" == "true" ]]; then
# 缓存命中:使用完全离线与锁定安装,避免网络请求
EXTRA_FLAGS="--offline --frozen-lockfile"
else
# 缓存未命中:尽量离线,但允许必要网络;同时锁定避免解析差异
EXTRA_FLAGS="--prefer-offline --frozen-lockfile"
fi
echo "⚡ pnpm安装优化参数: $EXTRA_FLAGS"
fi
# 根据包管理器选择安装命令
case "${{ inputs.package-manager }}" in
"npm")
if [[ "${{ inputs.force-install }}" == "true" ]]; then
echo "🔧 使用npm强制安装"
npm install --force ${INSTALL_ARGS}
else
echo "🔧 使用npm安装"
npm install ${INSTALL_ARGS}
fi
;;
"pnpm")
if [[ "${{ inputs.force-install }}" == "true" ]]; then
echo "🔧 使用pnpm强制安装"
pnpm install --force ${EXTRA_FLAGS} ${INSTALL_ARGS}
else
echo "🔧 使用pnpm安装"
pnpm install ${EXTRA_FLAGS} ${INSTALL_ARGS}
fi
;;
"yarn")
if [[ "${{ inputs.force-install }}" == "true" ]]; then
echo "🔧 使用yarn强制安装"
yarn install --force ${INSTALL_ARGS}
else
echo "🔧 使用yarn安装"
yarn install ${INSTALL_ARGS}
fi
;;
*)
echo "❌ 不支持的包管理器: ${{ inputs.package-manager }}"
exit 1
;;
esac
fi
# 可选清理:如启用并发现项目根存在残留的 .pnpm-store且与目标目录不同则清理
if [[ "${{ inputs.package-manager }}" == "pnpm" && "${{ inputs.clean-project-store }}" == "true" ]]; then
# 进一步在项目级设置覆盖一次,杜绝 .npmrc 相对路径带来的影响(仅对本 CI 工作目录生效)
pnpm config set store-dir "${PNPM_STORE_DIR}" --location=project || true
if [[ -d ".pnpm-store" && "${PNPM_STORE_DIR}" != "$PWD/.pnpm-store" ]]; then
echo "🧹 清理项目根的残留 .pnpm-store目标store为 ${PNPM_STORE_DIR}"
rm -rf .pnpm-store || true
fi
fi
echo "✅ 依赖安装完成"
- name: 执行Git Stash
if: steps.cache.outputs.cache-hit != 'true' && inputs.enable-git-stash == 'true'
shell: bash
run: |
echo "🔄 执行git stash..."
git stash
echo "✅ git stash完成"
- name: 安装总结
shell: bash
run: |
echo "📦 依赖安装总结:"
echo " - 包管理器: ${{ inputs.package-manager }}"
echo " - 缓存命中: ${{ steps.cache.outputs.cache-hit }}"
echo " - 缓存key: ${{ steps.cache-key.outputs.key }}"
if [[ "${{ steps.cache.outputs.cache-hit }}" != "true" ]]; then
echo " - 强制安装: ${{ inputs.force-install }}"
echo " - Git Stash: ${{ inputs.enable-git-stash }}"
fi
# 诊断工作目录是否存在 .pnpm-store 以及其 Git 状态
echo "\n🧪 目录状态自检:"
echo " - PNPM_STORE_DIR: ${PNPM_STORE_DIR:-<unset>}"
if [[ -d ".pnpm-store" ]]; then
echo " - 工作目录存在 .pnpm-store 目录"
if git rev-parse --git-dir >/dev/null 2>&1; then
if git ls-files --error-unmatch .pnpm-store >/dev/null 2>&1; then
echo " - Git 状态: 已被跟踪 (tracked)"
elif git check-ignore -q .pnpm-store; then
echo " - Git 状态: 被忽略 (ignored)"
else
echo " - Git 状态: 未跟踪 (untracked)"
fi
else
echo " - Git 仓库: 未检测到 (非 Git 工作目录)"
fi
else
echo " - 工作目录未发现 .pnpm-store 目录 ✅"
fi