mirror of
https://github.com/pnpm/action-setup.git
synced 2026-08-13 16:01:31 +00:00
fix: upload the verification log right after the install writes it
Saving in the post step left the whole job between the install and the upload. Anything running in that window — the job's tests, its build, a dependency's own install scripts — can rewrite the log on disk, and the job's own cache write would then publish a record claiming some other lockfile passed verification, for every later job to restore and trust. No cache credentials needed: the attacker rides the write the job performs anyway. The log is complete the moment the install finishes, so it is uploaded there. The post step still covers a job that installs in a step of its own, where that is the first point the log is known to be final; the save is idempotent across the two, and the process-local flags exist because main and post do not share state within a run.
This commit is contained in:
@@ -221,6 +221,8 @@ The action restores and saves that file on every run, independently of the `cach
|
|||||||
|
|
||||||
Reusing a verdict is not a weaker check: pnpm re-verifies whenever the lockfile content changes, and whenever the recorded policy is looser than the one now configured.
|
Reusing a verdict is not a weaker check: pnpm re-verifies whenever the lockfile content changes, and whenever the recorded policy is looser than the one now configured.
|
||||||
|
|
||||||
|
The log is uploaded as soon as the install that produced it finishes, not at the end of the job, so nothing the job runs afterwards — its tests, its build, a dependency's own scripts — can alter what later jobs restore. A job that installs in a step of its own rather than through this action is saved at the end of the job instead, since that is the first moment the log is known to be complete.
|
||||||
|
|
||||||
### Cache dependencies from multiple lockfiles
|
### Cache dependencies from multiple lockfiles
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
Vendored
+122
-122
File diff suppressed because one or more lines are too long
+4
-1
@@ -29,11 +29,14 @@ async function runMain() {
|
|||||||
await restoreCache(inputs)
|
await restoreCache(inputs)
|
||||||
|
|
||||||
pnpmInstall(inputs)
|
pnpmInstall(inputs)
|
||||||
|
await saveVerificationCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runPost() {
|
async function runPost() {
|
||||||
const inputs = JSON.parse(getState('inputs')) as Inputs
|
const inputs = JSON.parse(getState('inputs')) as Inputs
|
||||||
// pnpm versions before pnpm/pnpm#13893 delete the log during a store prune.
|
// Covers a job that installs in a later step of its own; when this action
|
||||||
|
// installed, the log was already saved then. Runs before the prune because
|
||||||
|
// pnpm versions before pnpm/pnpm#13893 delete the log during one.
|
||||||
await saveVerificationCache()
|
await saveVerificationCache()
|
||||||
pruneStore(inputs)
|
pruneStore(inputs)
|
||||||
await saveCache(inputs)
|
await saveCache(inputs)
|
||||||
|
|||||||
@@ -15,7 +15,18 @@ const VERIFICATION_CACHE_FILE = 'lockfile-verified.jsonl'
|
|||||||
|
|
||||||
const PATH_STATE = 'lockfile_verification_cache_path'
|
const PATH_STATE = 'lockfile_verification_cache_path'
|
||||||
const KEY_STATE = 'lockfile_verification_cache_key'
|
const KEY_STATE = 'lockfile_verification_cache_key'
|
||||||
const RESTORED_STATE = 'lockfile_verification_cache_restored'
|
const STORED_STATE = 'lockfile_verification_cache_stored'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the log lives and under which key it belongs in the cache. Held in
|
||||||
|
* memory as well as in the action's state because the main and post steps run
|
||||||
|
* as separate processes, and state written by one is only readable by the
|
||||||
|
* other.
|
||||||
|
*/
|
||||||
|
let target: { cacheFilePath: string, key: string } | undefined
|
||||||
|
|
||||||
|
/** Whether this process already restored or saved the log. */
|
||||||
|
let stored = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The verdict is only valid for the exact lockfile content it was recorded
|
* The verdict is only valid for the exact lockfile content it was recorded
|
||||||
@@ -26,6 +37,7 @@ export async function restoreVerificationCache(lockfileHash: string): Promise<vo
|
|||||||
try {
|
try {
|
||||||
const cacheFilePath = path.join(await getPnpmCacheDirectory(), VERIFICATION_CACHE_FILE)
|
const cacheFilePath = path.join(await getPnpmCacheDirectory(), VERIFICATION_CACHE_FILE)
|
||||||
const key = `pnpm-lockfile-verified-${process.env.RUNNER_OS}-${os.arch()}-${lockfileHash}`
|
const key = `pnpm-lockfile-verified-${process.env.RUNNER_OS}-${os.arch()}-${lockfileHash}`
|
||||||
|
target = { cacheFilePath, key }
|
||||||
saveState(PATH_STATE, cacheFilePath)
|
saveState(PATH_STATE, cacheFilePath)
|
||||||
saveState(KEY_STATE, key)
|
saveState(KEY_STATE, key)
|
||||||
debug(`Lockfile verification cache path is ${cacheFilePath}, key is ${key}`)
|
debug(`Lockfile verification cache path is ${cacheFilePath}, key is ${key}`)
|
||||||
@@ -36,7 +48,8 @@ export async function restoreVerificationCache(lockfileHash: string): Promise<vo
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
saveState(RESTORED_STATE, 'true')
|
stored = true
|
||||||
|
saveState(STORED_STATE, 'true')
|
||||||
info(`Lockfile verification cache restored from key: ${restoredKey}`)
|
info(`Lockfile verification cache restored from key: ${restoredKey}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// The gate only costs time, never correctness — a job that cannot reuse
|
// The gate only costs time, never correctness — a job that cannot reuse
|
||||||
@@ -45,16 +58,26 @@ export async function restoreVerificationCache(lockfileHash: string): Promise<vo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploaded as soon as the install that produced the log finishes, rather than
|
||||||
|
* at the end of the job: whatever a job runs after installing — its tests, its
|
||||||
|
* build, a dependency's own scripts — can rewrite the log on disk, and the
|
||||||
|
* job's own cache write would then publish that for later jobs to trust.
|
||||||
|
*
|
||||||
|
* Safe to call more than once; the second call is a no-op.
|
||||||
|
*/
|
||||||
export async function saveVerificationCache(): Promise<void> {
|
export async function saveVerificationCache(): Promise<void> {
|
||||||
if (getState(RESTORED_STATE) === 'true') return
|
if (stored || getState(STORED_STATE) === 'true') return
|
||||||
|
|
||||||
const cacheFilePath = getState(PATH_STATE)
|
const cacheFilePath = target?.cacheFilePath ?? getState(PATH_STATE)
|
||||||
const key = getState(KEY_STATE)
|
const key = target?.key ?? getState(KEY_STATE)
|
||||||
if (!cacheFilePath || !key || !existsSync(cacheFilePath)) return
|
if (!cacheFilePath || !key || !existsSync(cacheFilePath)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cacheId = await saveCache([cacheFilePath], key)
|
const cacheId = await saveCache([cacheFilePath], key)
|
||||||
if (cacheId === -1) return
|
if (cacheId === -1) return
|
||||||
|
stored = true
|
||||||
|
saveState(STORED_STATE, 'true')
|
||||||
info(`Lockfile verification cache saved with the key: ${key}`)
|
info(`Lockfile verification cache saved with the key: ${key}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
warning(`Failed to save the lockfile verification cache: ${(error as Error).message}`)
|
warning(`Failed to save the lockfile verification cache: ${(error as Error).message}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user