mirror of
https://github.com/pnpm/action-setup.git
synced 2026-08-28 21:23:45 +08:00
Compare commits
1 Commits
master
...
c0a6b0ff36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0a6b0ff36 |
48
.github/workflows/test.yaml
vendored
48
.github/workflows/test.yaml
vendored
@@ -329,3 +329,51 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
cache_lockfile_verification:
|
||||
# The action caches pnpm's lockfile verification log, which lives in
|
||||
# `cacheDir` — a directory pnpm resolves per platform and does not print.
|
||||
# Guard the action's copy of that default against pnpm's own.
|
||||
name: 'Lockfile verification cache (${{ matrix.os }})'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Set up a project with a supply-chain policy
|
||||
# A one-minute floor activates the verification without holding back
|
||||
# any version the install resolves.
|
||||
run: |
|
||||
echo '{"dependencies":{"is-odd":"3.0.1"}}' > package.json
|
||||
printf 'packages:\n - .\nminimumReleaseAge: 1\n' > pnpm-workspace.yaml
|
||||
shell: bash
|
||||
|
||||
- uses: ./
|
||||
with:
|
||||
version: '12.0.0-rc.4'
|
||||
cache: true
|
||||
run_install: |
|
||||
- args: [--no-frozen-lockfile]
|
||||
|
||||
- name: 'Test: pnpm wrote the verification log where the action looks for it'
|
||||
run: |
|
||||
set -e
|
||||
case "$RUNNER_OS" in
|
||||
Linux) cacheDir="${XDG_CACHE_HOME:-$HOME/.cache}/pnpm" ;;
|
||||
macOS) cacheDir="$HOME/Library/Caches/pnpm" ;;
|
||||
Windows) cacheDir="$(cygpath -u "$LOCALAPPDATA")/pnpm-cache" ;;
|
||||
*) echo "Unexpected RUNNER_OS: $RUNNER_OS"; exit 1 ;;
|
||||
esac
|
||||
echo "Expecting the verification log in ${cacheDir}"
|
||||
if [ ! -f "${cacheDir}/lockfile-verified.jsonl" ]; then
|
||||
echo "No lockfile-verified.jsonl there; the action would cache nothing"
|
||||
ls -la "${cacheDir}" || true
|
||||
exit 1
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
@@ -94,7 +94,7 @@ If `run_install` is a YAML string representation of either an object or an array
|
||||
|
||||
### `cache`
|
||||
|
||||
**Optional** (_type:_ `boolean`, _default:_ `false`) Whether to cache the pnpm store directory.
|
||||
**Optional** (_type:_ `boolean`, _default:_ `false`) Whether to cache the pnpm store directory and, on pnpm v11 and newer, the results of pnpm's lockfile verification against the configured supply-chain policies. Both are keyed on the lockfile's content hash.
|
||||
|
||||
### `cache_dependency_path`
|
||||
|
||||
@@ -208,6 +208,8 @@ jobs:
|
||||
|
||||
**Note:** You don't need to run `pnpm store prune` at the end; post-action has already taken care of that.
|
||||
|
||||
Besides the store, this also caches pnpm's lockfile verification results (pnpm v11 and newer). Repositories that configure supply-chain policies such as `minimumReleaseAge` or `trustPolicy` make pnpm check every lockfile entry against the registry on each install; that check depends only on the lockfile and the policies, so its result is cached and reused until the lockfile changes.
|
||||
|
||||
### Cache dependencies from multiple lockfiles
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -16,7 +16,10 @@ inputs:
|
||||
required: false
|
||||
default: 'null'
|
||||
cache:
|
||||
description: Whether to cache the pnpm store directory
|
||||
description: |
|
||||
Whether to cache the pnpm store directory and, on pnpm v11 and newer,
|
||||
the results of pnpm's lockfile verification against the configured
|
||||
supply-chain policies. Both are keyed on the lockfile's content hash.
|
||||
required: false
|
||||
default: 'false'
|
||||
cache_dependency_path:
|
||||
|
||||
294
dist/index.js
vendored
294
dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -4,16 +4,22 @@ import { getExecOutput } from '@actions/exec'
|
||||
import { hashFiles } from '@actions/glob'
|
||||
import os from 'os'
|
||||
import { Inputs } from '../inputs'
|
||||
import { restoreVerificationCache } from '../lockfile-verification-cache'
|
||||
|
||||
export async function runRestoreCache(inputs: Inputs) {
|
||||
const cachePath = await getCacheDirectory()
|
||||
saveState('cache_path', cachePath)
|
||||
|
||||
const fileHash = await hashFiles(inputs.cacheDependencyPath)
|
||||
if (!fileHash) {
|
||||
throw new Error('Some specified paths were not resolved, unable to cache dependencies.')
|
||||
}
|
||||
|
||||
await runRestoreStoreCache(fileHash)
|
||||
await restoreVerificationCache(fileHash)
|
||||
}
|
||||
|
||||
async function runRestoreStoreCache(fileHash: string) {
|
||||
const cachePath = await getCacheDirectory()
|
||||
saveState('cache_path', cachePath)
|
||||
|
||||
const primaryKey = `pnpm-cache-${process.env.RUNNER_OS}-${os.arch()}-${fileHash}`
|
||||
debug(`Primary key is ${primaryKey}`)
|
||||
saveState('cache_primary_key', primaryKey)
|
||||
|
||||
@@ -3,6 +3,7 @@ import restoreCache from './cache-restore'
|
||||
import saveCache from './cache-save'
|
||||
import getInputs, { Inputs } from './inputs'
|
||||
import installPnpm from './install-pnpm'
|
||||
import { saveVerificationCache } from './lockfile-verification-cache'
|
||||
import setOutputs from './outputs'
|
||||
import pnpmInstall from './pnpm-install'
|
||||
import pruneStore from './pnpm-store-prune'
|
||||
@@ -32,6 +33,9 @@ async function runMain() {
|
||||
|
||||
async function runPost() {
|
||||
const inputs = JSON.parse(getState('inputs')) as Inputs
|
||||
// Saved ahead of the prune because `pnpm store prune` drops the
|
||||
// verification log along with the rest of the store's derived state.
|
||||
await saveVerificationCache()
|
||||
pruneStore(inputs)
|
||||
await saveCache(inputs)
|
||||
}
|
||||
|
||||
95
src/lockfile-verification-cache/index.ts
Normal file
95
src/lockfile-verification-cache/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { restoreCache, saveCache } from '@actions/cache'
|
||||
import { debug, getState, info, saveState, warning } from '@actions/core'
|
||||
import { getExecOutput } from '@actions/exec'
|
||||
import { existsSync } from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { removeWindowsExtendedPathPrefix } from '../windows-path'
|
||||
|
||||
/**
|
||||
* pnpm v11+ verifies every lockfile entry against the configured
|
||||
* supply-chain policies (`minimumReleaseAge`, `trustPolicy`, …) and memoizes
|
||||
* the verdict in this file, so the next install with the same lockfile and
|
||||
* the same policies skips the registry round-trips entirely. Without it a CI
|
||||
* job re-verifies the whole lockfile on every run, which on a large
|
||||
* repository costs more than the install itself.
|
||||
*/
|
||||
const VERIFICATION_CACHE_FILE = 'lockfile-verified.jsonl'
|
||||
|
||||
const PATH_STATE = 'lockfile_verification_cache_path'
|
||||
const KEY_STATE = 'lockfile_verification_cache_key'
|
||||
const RESTORED_STATE = 'lockfile_verification_cache_restored'
|
||||
|
||||
/**
|
||||
* The verdict is only valid for the exact lockfile content it was recorded
|
||||
* for, so this cache is keyed on the same lockfile hash as the store cache
|
||||
* but restored without prefix fallback: an older entry could never be used.
|
||||
*/
|
||||
export async function restoreVerificationCache(lockfileHash: string): Promise<void> {
|
||||
try {
|
||||
const cacheFilePath = path.join(await getPnpmCacheDirectory(), VERIFICATION_CACHE_FILE)
|
||||
const key = `pnpm-lockfile-verified-${process.env.RUNNER_OS}-${os.arch()}-${lockfileHash}`
|
||||
saveState(PATH_STATE, cacheFilePath)
|
||||
saveState(KEY_STATE, key)
|
||||
debug(`Lockfile verification cache path is ${cacheFilePath}, key is ${key}`)
|
||||
|
||||
const restoredKey = await restoreCache([cacheFilePath], key)
|
||||
if (!restoredKey) {
|
||||
info('Lockfile verification cache is not found')
|
||||
return
|
||||
}
|
||||
|
||||
saveState(RESTORED_STATE, 'true')
|
||||
info(`Lockfile verification cache restored from key: ${restoredKey}`)
|
||||
} catch (error) {
|
||||
// The gate only costs time, never correctness — a job that cannot reuse
|
||||
// a past verdict re-verifies and moves on.
|
||||
warning(`Failed to restore the lockfile verification cache: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveVerificationCache(): Promise<void> {
|
||||
if (getState(RESTORED_STATE) === 'true') return
|
||||
|
||||
const cacheFilePath = getState(PATH_STATE)
|
||||
const key = getState(KEY_STATE)
|
||||
if (!cacheFilePath || !key || !existsSync(cacheFilePath)) return
|
||||
|
||||
try {
|
||||
const cacheId = await saveCache([cacheFilePath], key)
|
||||
if (cacheId === -1) return
|
||||
info(`Lockfile verification cache saved with the key: ${key}`)
|
||||
} catch (error) {
|
||||
warning(`Failed to save the lockfile verification cache: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function getPnpmCacheDirectory(): Promise<string> {
|
||||
const { stdout } = await getExecOutput('pnpm config get cacheDir', undefined, {
|
||||
silent: true,
|
||||
ignoreReturnCode: true,
|
||||
})
|
||||
const configured = stdout.trim()
|
||||
// `pnpm config get` reports settings, not defaults: an unset `cacheDir`
|
||||
// prints `undefined` and the default has to be derived here.
|
||||
if (configured && configured !== 'undefined') {
|
||||
return removeWindowsExtendedPathPrefix(configured)
|
||||
}
|
||||
return defaultPnpmCacheDirectory()
|
||||
}
|
||||
|
||||
/** Mirrors pnpm's own `cacheDir` default. */
|
||||
function defaultPnpmCacheDirectory(): string {
|
||||
const { XDG_CACHE_HOME, LOCALAPPDATA } = process.env
|
||||
if (XDG_CACHE_HOME) return path.join(XDG_CACHE_HOME, 'pnpm')
|
||||
|
||||
const homeDir = os.homedir()
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return path.join(homeDir, 'Library', 'Caches', 'pnpm')
|
||||
case 'win32':
|
||||
return LOCALAPPDATA ? path.join(LOCALAPPDATA, 'pnpm-cache') : path.join(homeDir, '.pnpm-cache')
|
||||
default:
|
||||
return path.join(homeDir, '.cache', 'pnpm')
|
||||
}
|
||||
}
|
||||
19
src/windows-path/index.ts
Normal file
19
src/windows-path/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* pnpm may report an extended-length path on Windows. The `?` in that prefix
|
||||
* is interpreted as a wildcard by `@actions/cache`, which rejects it as a glob
|
||||
* in the root segment. Cache APIs do not need the extended-length form, so
|
||||
* convert it back to a regular drive or UNC path.
|
||||
*/
|
||||
export function removeWindowsExtendedPathPrefix(cachePath: string): string {
|
||||
const extendedPathPrefix = '\\\\?\\'
|
||||
if (!cachePath.startsWith(extendedPathPrefix)) return cachePath
|
||||
|
||||
const pathWithoutPrefix = cachePath.slice(extendedPathPrefix.length)
|
||||
const uncPrefix = 'UNC\\'
|
||||
if (pathWithoutPrefix.toUpperCase().startsWith(uncPrefix)) {
|
||||
return `\\\\${pathWithoutPrefix.slice(uncPrefix.length)}`
|
||||
}
|
||||
return pathWithoutPrefix
|
||||
}
|
||||
|
||||
export default removeWindowsExtendedPathPrefix
|
||||
Reference in New Issue
Block a user