Skip to content

Commit 564619f

Browse files
aqeelatzkochan
andauthored
fix(install): persist revoked builds to .modules.yaml (#12221) (#12224)
* fix(install): persist revoked builds to .modules.yaml (#12221) After a build-script dependency whose approval was revoked (e.g. via git stash dropping allowBuilds from pnpm-workspace.yaml) is re-added, the revocation detection populated ignoredBuilds in memory but the install path's writeModulesManifest had already run, so .modules.yaml never recorded the revoked packages. pnpm approve-builds then read an empty ignoredBuilds and reported 'no packages awaiting approval'. Re-read the manifest from disk after the revocation scan and write back the updated ignoredBuilds, merging with any entries the install path captured. * refactor(install): address review comments - Inline dead ignoredBuildsFromInstall indirection - Drop unsafe allowBuilds cast in the regression test --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
1 parent d36b6f8 commit 564619f

3 files changed

Lines changed: 61 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"pnpm": patch
3+
---
4+
5+
Fixed `pnpm approve-builds` reporting "no packages awaiting approval" when a build-script dependency whose approval was revoked (e.g. after `git stash` drops the `allowBuilds` from `pnpm-workspace.yaml`) is re-added. The revoked packages are now correctly recorded in `.modules.yaml` so `approve-builds` can find them. [#12221](https://github.com/pnpm/pnpm/issues/12221)

installing/deps-installer/src/install/index.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
type WantedDependency,
3939
} from '@pnpm/installing.deps-resolver'
4040
import { extendProjectsWithTargetDirs, headlessInstall, type InstallationResultStats } from '@pnpm/installing.deps-restorer'
41-
import { writeModulesManifest } from '@pnpm/installing.modules-yaml'
41+
import { readModulesManifest, type StrictModules, writeModulesManifest } from '@pnpm/installing.modules-yaml'
4242
import {
4343
type CatalogSnapshots,
4444
cleanGitBranchLockfiles,
@@ -497,6 +497,7 @@ export async function mutateModules (
497497
if (!opts.ignoreScripts && ignoredBuilds?.size) {
498498
ignoredBuilds = await runUnignoredDependencyBuilds(opts, ignoredBuilds, ctx.wantedLockfile, allowBuild)
499499
}
500+
let revokedBuilds = false
500501
// Detect packages whose build approval was revoked between the previous
501502
// and current install. A package is considered revoked when it was
502503
// previously allowed (true) but is now undecided (undefined). Packages
@@ -519,10 +520,28 @@ export async function mutateModules (
519520
if (allowBuild?.(depPath) === undefined) {
520521
ignoredBuilds ??= new Set()
521522
ignoredBuilds.add(depPath)
523+
revokedBuilds = true
522524
}
523525
}
524526
}
525527
}
528+
if (revokedBuilds && !opts.lockfileOnly && opts.enableModulesDir) {
529+
// The install path already wrote .modules.yaml with the current
530+
// install's state, but it captured ignoredBuilds before the revocation
531+
// scan above added to it. Re-read the manifest from disk so we only
532+
// update ignoredBuilds and don't clobber fields (hoistedDependencies,
533+
// pendingBuilds, etc.) the install just wrote. The current computed
534+
// set is authoritative — runUnignoredDependencyBuilds may have removed
535+
// entries (for packages it successfully rebuilt) that the on-disk
536+
// manifest still records, and those must not be re-introduced.
537+
const writtenManifest = await readModulesManifest(ctx.rootModulesDir)
538+
if (writtenManifest) {
539+
// writeModulesManifest converts ignoredBuilds to an array before
540+
// serializing, so a Set is fine here.
541+
writtenManifest.ignoredBuilds = ignoredBuilds
542+
await writeModulesManifest(ctx.rootModulesDir, writtenManifest as StrictModules)
543+
}
544+
}
526545
ignoredScriptsLogger.debug({
527546
packageNames: ignoredBuilds ? dedupePackageNamesFromIgnoredBuilds(ignoredBuilds) : [],
528547
})

pnpm/test/install/lifecycleScripts.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs'
22
import path from 'node:path'
33

44
import { expect, test } from '@jest/globals'
5+
import { parse } from '@pnpm/deps.path'
56
import { prepare } from '@pnpm/prepare'
67
import type { PackageManifest, ProjectManifest } from '@pnpm/types'
78
import { readWorkspaceManifest } from '@pnpm/workspace.workspace-manifest-reader'
@@ -407,3 +408,38 @@ test('--allow-build flag should error when conflicting with allowBuilds: false',
407408
expect(result.status).toBe(1)
408409
expect(result.stdout.toString()).toContain('The following dependencies are ignored by the root project, but are allowed to be built by the current command: @pnpm.e2e/install-script-example')
409410
})
411+
412+
test('approve-builds works after stashing and re-adding a dependency (#12221)', async () => {
413+
const project = prepare({})
414+
415+
const pkgName = '@pnpm.e2e/pre-and-postinstall-scripts-example'
416+
417+
const firstAdd = execPnpmSync(['add', `${pkgName}@1.0.0`])
418+
expect(firstAdd.status).toBe(1)
419+
expect(firstAdd.stdout.toString()).toContain('Ignored build scripts:')
420+
421+
const firstApprove = execPnpmSync(['approve-builds', '--all'])
422+
expect(firstApprove.status).toBe(0)
423+
424+
const wsManifest = await readWorkspaceManifest(process.cwd())
425+
expect(wsManifest!.allowBuilds?.[pkgName]).toBe(true)
426+
427+
fs.rmSync('package.json', { force: true })
428+
fs.rmSync('pnpm-workspace.yaml', { force: true })
429+
fs.rmSync('pnpm-lock.yaml', { force: true })
430+
431+
fs.writeFileSync('package.json', '{}')
432+
writeYamlFileSync('pnpm-workspace.yaml', {})
433+
434+
const secondAdd = execPnpmSync(['add', `${pkgName}@1.0.0`])
435+
expect(secondAdd.status).toBe(1)
436+
expect(secondAdd.stdout.toString()).toContain('Ignored build scripts:')
437+
438+
const modulesManifest = project.readModulesManifest()
439+
expect(modulesManifest?.ignoredBuilds).toBeDefined()
440+
expect(Array.from(modulesManifest!.ignoredBuilds!).some((dp) => parse(dp).name === pkgName)).toBe(true)
441+
442+
const secondApprove = execPnpmSync(['approve-builds', '--all'])
443+
expect(secondApprove.status).toBe(0)
444+
expect(secondApprove.stdout.toString()).not.toContain('No packages awaiting approval')
445+
})

0 commit comments

Comments
 (0)