mirror of
https://git.bjxgj.com/xgj/xgj-actions.git
synced 2025-10-14 13:33:37 +08:00
feat: 新增pnpm依赖安装与缓存的GitHub Action实现
This commit is contained in:
102
pnpm-install/README.md
Normal file
102
pnpm-install/README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# pnpm 依赖安装与缓存 Action
|
||||||
|
|
||||||
|
专为 pnpm 项目设计的 GitHub Action,通过缓存 `node_modules` 或 pnpm store,在 CI/CD 中实现快速复用,二次执行可在 10 秒内完成安装。
|
||||||
|
|
||||||
|
## ✨ 特性
|
||||||
|
|
||||||
|
- **专注 pnpm**:默认缓存 `node_modules`,命中后直接复用,无需重新链接
|
||||||
|
- **双缓存模式**:可在 `node_modules` / `store` 之间切换,满足不同目录约束
|
||||||
|
- **锁文件准确性**:推荐传入 `cache-hash`(如 `hashFiles('pnpm-lock.yaml')`)确保缓存精准失效
|
||||||
|
- **自定义安装**:支持附加参数或完全覆盖安装命令,保留 `force-install` 选项
|
||||||
|
- **环境整洁**:自动设置 `PNPM_STORE_DIR`,可选清理项目根 `.pnpm-store`
|
||||||
|
|
||||||
|
## 📥 输入参数
|
||||||
|
|
||||||
|
| 参数名 | 描述 | 必需 | 默认值 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `cache-mode` | 缓存模式:`node_modules` / `store` | 否 | `node_modules` |
|
||||||
|
| `cache-prefix` | 缓存 key 前缀 | 否 | `modules` |
|
||||||
|
| `node-modules-path` | `node_modules` 目录路径(仅 `cache-mode=node_modules` 时生效) | 否 | `node_modules` |
|
||||||
|
| `force-install` | 是否强制安装(追加 `--force`) | 否 | `false` |
|
||||||
|
| `install-command` | 自定义安装命令,覆盖默认的 `pnpm install` | 否 | `''` |
|
||||||
|
| `install-args` | 附加参数(仅默认命令时生效) | 否 | `''` |
|
||||||
|
| `cache-hash` | 缓存 hash 值(建议:`hashFiles('pnpm-lock.yaml')`) | 否 | `''` |
|
||||||
|
| `clean-project-store` | 安装后是否清理项目根的 `.pnpm-store` | 否 | `false` |
|
||||||
|
|
||||||
|
## 📤 输出参数
|
||||||
|
|
||||||
|
| 参数名 | 描述 |
|
||||||
|
| --- | --- |
|
||||||
|
| `cache-hit` | 缓存是否命中 (`true` / `false`) |
|
||||||
|
| `cache-key` | 实际使用的缓存 key |
|
||||||
|
| `cache-path` | 缓存目录路径(调试/复用用途) |
|
||||||
|
|
||||||
|
## 🚀 快速上手
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: 安装依赖
|
||||||
|
uses: actions/xgj/pnpm-install@v1
|
||||||
|
with:
|
||||||
|
cache-hash: ${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
- 默认缓存 `node_modules`,命中后直接跳过安装步骤,满足 10 秒内完成的目标。
|
||||||
|
- 首次执行或 lock 文件变更时执行 `pnpm install --prefer-offline --frozen-lockfile`,完成后写入缓存。
|
||||||
|
|
||||||
|
## 🔁 缓存模式
|
||||||
|
|
||||||
|
### `cache-mode: node_modules`(默认)
|
||||||
|
|
||||||
|
- **适用场景**:希望二次执行直接复用产物、编译型依赖较多。
|
||||||
|
- **行为**:缓存 `node_modules` 指定目录。命中缓存后跳过安装步骤。
|
||||||
|
- **额外设定**:Action 会自动将 `pnpm node-linker` 设置为 `hoisted`,避免使用 symlink,生成更接近 npm 的目录结构。
|
||||||
|
|
||||||
|
### `cache-mode: store`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
cache-mode: store
|
||||||
|
cache-hash: ${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **适用场景**:项目对 `node_modules` 目录结构有额外处理,或需要保持 `node_modules` 在工作目录内新生成。
|
||||||
|
- **行为**:缓存 `${{ runner.temp }}/.pnpm-store`。命中缓存后执行 `pnpm install --offline --frozen-lockfile`,仅进行符号链接操作。
|
||||||
|
|
||||||
|
## ⚙️ 进阶配置
|
||||||
|
|
||||||
|
- **强制安装**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
force-install: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
当缓存未命中且怀疑存在依赖冲突时,可追加 `--force`。
|
||||||
|
|
||||||
|
- **自定义命令**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
install-command: "pnpm install --prod"
|
||||||
|
```
|
||||||
|
|
||||||
|
完全覆盖默认安装逻辑,适合部署类任务。
|
||||||
|
|
||||||
|
- **清理项目根 `.pnpm-store`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
with:
|
||||||
|
clean-project-store: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
在部分仓库中 `.npmrc` 会指定相对 store 路径,该选项可避免 CI 工作目录残留。
|
||||||
|
|
||||||
|
## 🧰 示例工作流
|
||||||
|
|
||||||
|
参见 `examples/basic-usage.yml`,展示在 CI 中的集成方式。
|
||||||
|
|
||||||
|
## 🛡️ 注意事项
|
||||||
|
|
||||||
|
- 该 Action 假设 Runner 环境已安装 pnpm。若未预装,请在前一步引入 `pnpm/action-setup@v4`。
|
||||||
|
- 强烈建议传入 `cache-hash`,并确保 `pnpm-lock.yaml` 已提交。
|
||||||
|
- GitHub Actions 缓存配额有限,定期清理或调整 `cache-prefix` 以避免冲突。
|
196
pnpm-install/action.yml
Normal file
196
pnpm-install/action.yml
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
name: 'pnpm依赖安装与缓存'
|
||||||
|
description: '专注于pnpm的依赖缓存与安装,加速重复执行'
|
||||||
|
branding:
|
||||||
|
icon: 'package'
|
||||||
|
color: 'yellow'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
cache-mode:
|
||||||
|
description: '缓存模式 (node_modules 或 store)'
|
||||||
|
required: false
|
||||||
|
default: 'node_modules'
|
||||||
|
|
||||||
|
cache-prefix:
|
||||||
|
description: '缓存前缀名称'
|
||||||
|
required: false
|
||||||
|
default: 'modules'
|
||||||
|
|
||||||
|
node-modules-path:
|
||||||
|
description: 'node_modules目录路径(cache-mode=node_modules 时生效)'
|
||||||
|
required: false
|
||||||
|
default: 'node_modules'
|
||||||
|
|
||||||
|
force-install:
|
||||||
|
description: '是否强制安装 (true/false)'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
|
install-command:
|
||||||
|
description: '自定义安装命令(若设置则完全覆盖默认命令)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
install-args:
|
||||||
|
description: '附加到默认安装命令的参数(当未提供 install-command 时生效)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
cache-hash:
|
||||||
|
description: '缓存hash值(推荐使用hashFiles)'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
clean-project-store:
|
||||||
|
description: '安装后清理项目根目录的 .pnpm-store (true/false)'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
|
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: |
|
||||||
|
if ! command -v pnpm >/dev/null 2>&1; then
|
||||||
|
echo "pnpm 未安装"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VERSION=$(pnpm --version | tr -d '\n')
|
||||||
|
if [[ -z "$VERSION" ]]; then
|
||||||
|
echo "无法获取pnpm版本"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
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 }}
|
||||||
|
FALLBACK_HASH: ${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ -n "${{ inputs.cache-hash }}" ]]; then
|
||||||
|
CACHE_HASH="${{ inputs.cache-hash }}"
|
||||||
|
elif [[ -n "${FALLBACK_HASH}" ]]; then
|
||||||
|
CACHE_HASH="${FALLBACK_HASH}"
|
||||||
|
else
|
||||||
|
CACHE_HASH=""
|
||||||
|
fi
|
||||||
|
if [[ -n "$CACHE_HASH" ]]; then
|
||||||
|
CACHE_HASH_SHORT=$(echo "$CACHE_HASH" | head -c 12)
|
||||||
|
else
|
||||||
|
CACHE_HASH_SHORT="no-hash"
|
||||||
|
fi
|
||||||
|
PNPM_VERSION="${PNPM_VERSION}"
|
||||||
|
MODE="${{ inputs.cache-mode }}"
|
||||||
|
if [[ -z "$MODE" ]]; then
|
||||||
|
MODE="node_modules"
|
||||||
|
fi
|
||||||
|
CACHE_KEY="${{ runner.os }}-pnpm-v${PNPM_VERSION}-${MODE}-${{ inputs.cache-prefix }}-${CACHE_HASH_SHORT}"
|
||||||
|
RESTORE_PREFIX="${{ runner.os }}-pnpm-v${PNPM_VERSION}-${MODE}-${{ inputs.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
|
||||||
|
MODE="${{ inputs.cache-mode }}"
|
||||||
|
if [[ -z "$MODE" ]]; then
|
||||||
|
MODE="node_modules"
|
||||||
|
fi
|
||||||
|
if [[ "$MODE" == "node_modules" ]]; then
|
||||||
|
CACHE_PATH="${{ inputs.node-modules-path }}"
|
||||||
|
STORE_DIR="${RUNNER_TEMP:-$HOME}/.pnpm-store"
|
||||||
|
else
|
||||||
|
STORE_DIR="${RUNNER_TEMP:-$HOME}/.pnpm-store"
|
||||||
|
CACHE_PATH="$STORE_DIR"
|
||||||
|
fi
|
||||||
|
mkdir -p "$STORE_DIR"
|
||||||
|
echo "PNPM_STORE_DIR=${STORE_DIR}" >> "$GITHUB_ENV"
|
||||||
|
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
|
||||||
|
echo "缓存命中"
|
||||||
|
else
|
||||||
|
echo "缓存未命中"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
if: (inputs.cache-mode == 'node_modules' && steps.cache.outputs.cache-hit != 'true') || (inputs.cache-mode == 'store')
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
export PNPM_STORE_DIR="${PNPM_STORE_DIR:-${RUNNER_TEMP:-$HOME}/.pnpm-store}"
|
||||||
|
export npm_config_store_dir="$PNPM_STORE_DIR"
|
||||||
|
export PNPM_CONFIG_STORE_DIR="$PNPM_STORE_DIR"
|
||||||
|
pnpm config set store-dir "$PNPM_STORE_DIR" --location=project || true
|
||||||
|
if [[ "${{ inputs.cache-mode }}" == "node_modules" ]]; then
|
||||||
|
export PNPM_NODE_LINKER="hoisted"
|
||||||
|
pnpm config set node-linker hoisted --location=project || true
|
||||||
|
pnpm config set node-linker hoisted --location=global || true
|
||||||
|
echo "🔧 已将 pnpm node-linker 设置为 hoisted,避免 symlink"
|
||||||
|
fi
|
||||||
|
if [[ -n "${{ inputs.install-command }}" ]]; then
|
||||||
|
echo "🔧 使用自定义安装命令: ${{ inputs.install-command }}"
|
||||||
|
eval "${{ inputs.install-command }}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
FLAGS=("--frozen-lockfile")
|
||||||
|
if [[ "${{ inputs.cache-mode }}" == "store" ]]; then
|
||||||
|
if [[ "${{ steps.cache.outputs.cache-hit }}" == "true" ]]; then
|
||||||
|
FLAGS=("--offline" "--frozen-lockfile")
|
||||||
|
else
|
||||||
|
FLAGS=("--prefer-offline" "--frozen-lockfile")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ "${{ inputs.force-install }}" == "true" ]]; then
|
||||||
|
FLAGS+=("--force")
|
||||||
|
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" && "${PNPM_STORE_DIR}" != "$PWD/.pnpm-store" ]]; then
|
||||||
|
rm -rf .pnpm-store || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 总结
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "pnpm版本: $PNPM_VERSION"
|
||||||
|
echo "缓存命中: ${{ steps.cache.outputs.cache-hit }}"
|
||||||
|
echo "缓存key: ${{ steps.cache-key.outputs.key }}"
|
35
pnpm-install/examples/basic-usage.yml
Normal file
35
pnpm-install/examples/basic-usage.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 基础pnpm项目示例
|
||||||
|
name: pnpm项目CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 检出代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 设置 Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
id: deps
|
||||||
|
uses: actions/xgj/pnpm-install@v1
|
||||||
|
with:
|
||||||
|
cache-hash: ${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
|
|
||||||
|
- name: 运行测试
|
||||||
|
run: pnpm test
|
||||||
|
|
||||||
|
- name: 打印缓存状态
|
||||||
|
run: |
|
||||||
|
echo "cache-hit: ${{ steps.deps.outputs['cache-hit'] }}"
|
||||||
|
echo "cache-key: ${{ steps.deps.outputs['cache-key'] }}"
|
Reference in New Issue
Block a user