From 44956334110459852d92cfb00f4d3ff840185a7d Mon Sep 17 00:00:00 2001 From: Lyda <1829913225@qq.com> Date: Sat, 11 Oct 2025 19:15:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9Epnpm=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=AE=89=E8=A3=85=E4=B8=8E=E7=BC=93=E5=AD=98=E7=9A=84?= =?UTF-8?q?GitHub=20Action=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-install/README.md | 102 ++++++++++++++ pnpm-install/action.yml | 196 ++++++++++++++++++++++++++ pnpm-install/examples/basic-usage.yml | 35 +++++ 3 files changed, 333 insertions(+) create mode 100644 pnpm-install/README.md create mode 100644 pnpm-install/action.yml create mode 100644 pnpm-install/examples/basic-usage.yml diff --git a/pnpm-install/README.md b/pnpm-install/README.md new file mode 100644 index 0000000..a21a041 --- /dev/null +++ b/pnpm-install/README.md @@ -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` 以避免冲突。 diff --git a/pnpm-install/action.yml b/pnpm-install/action.yml new file mode 100644 index 0000000..e991d87 --- /dev/null +++ b/pnpm-install/action.yml @@ -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 }}" diff --git a/pnpm-install/examples/basic-usage.yml b/pnpm-install/examples/basic-usage.yml new file mode 100644 index 0000000..2af480a --- /dev/null +++ b/pnpm-install/examples/basic-usage.yml @@ -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'] }}"