feat(approve-builds): add --all flag to skip interactive prompts (#10619)

Allow approving all pending build dependencies at once without
interactive selection, useful for CI/CD pipelines and project
bootstrapping scenarios where interactive prompts are not feasible.

close #10136
This commit is contained in:
thilllon
2026-02-25 19:36:49 +09:00
committed by GitHub
parent 84075f96bf
commit 2e8816e83b
3 changed files with 108 additions and 48 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/exec.build-commands": minor
"pnpm": minor
---
Added `--all` flag to `pnpm approve-builds` that approves all pending builds without interactive prompts [#10136](https://github.com/pnpm/pnpm/issues/10136).

View File

@@ -9,7 +9,7 @@ import { rebuild, type RebuildCommandOpts } from '@pnpm/plugin-commands-rebuild'
import { writeSettings } from '@pnpm/config.config-writer'
import { getAutomaticallyIgnoredBuilds } from './getAutomaticallyIgnoredBuilds.js'
export type ApproveBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'allowBuilds'>
export type ApproveBuildsCommandOpts = Pick<Config, 'modulesDir' | 'dir' | 'rootProjectManifest' | 'rootProjectManifestDir' | 'allowBuilds'> & { all?: boolean }
export const commandNames = ['approve-builds']
@@ -22,6 +22,10 @@ export function help (): string {
title: 'Options',
list: [
{
description: 'Approve all pending dependencies without interactive prompts',
name: '--all',
},
{
description: 'Approve dependencies of global packages',
name: '--global',
@@ -35,6 +39,7 @@ export function help (): string {
export function cliOptionsTypes (): Record<string, unknown> {
return {
all: Boolean,
global: Boolean,
}
}
@@ -53,43 +58,48 @@ export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOp
globalInfo('There are no packages awaiting approval')
return
}
const { result } = await enquirer.prompt({
choices: sortUniqueStrings([...automaticallyIgnoredBuilds]),
indicator (state: any, choice: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
return ` ${choice.enabled ? '●' : '○'}`
},
message: 'Choose which packages to build ' +
`(Press ${chalk.cyan('<space>')} to select, ` +
`${chalk.cyan('<a>')} to toggle all, ` +
`${chalk.cyan('<i>')} to invert selection)`,
name: 'result',
pointer: '',
result () {
return this.selected
},
styles: {
dark: chalk.reset,
em: chalk.bgBlack.whiteBright,
success: chalk.reset,
},
type: 'multiselect',
let buildPackages: string[] = []
if (opts.all) {
buildPackages = sortUniqueStrings([...automaticallyIgnoredBuilds])
} else {
const { result } = await enquirer.prompt({
choices: sortUniqueStrings([...automaticallyIgnoredBuilds]),
indicator (state: any, choice: any) { // eslint-disable-line @typescript-eslint/no-explicit-any
return ` ${choice.enabled ? '●' : '○'}`
},
message: 'Choose which packages to build ' +
`(Press ${chalk.cyan('<space>')} to select, ` +
`${chalk.cyan('<a>')} to toggle all, ` +
`${chalk.cyan('<i>')} to invert selection)`,
name: 'result',
pointer: '',
result () {
return this.selected
},
styles: {
dark: chalk.reset,
em: chalk.bgBlack.whiteBright,
success: chalk.reset,
},
type: 'multiselect',
// For Vim users (related: https://github.com/enquirer/enquirer/pull/163)
j () {
return this.down()
},
k () {
return this.up()
},
cancel () {
// By default, canceling the prompt via Ctrl+c throws an empty string.
// The custom cancel function prevents that behavior.
// Otherwise, pnpm CLI would print an error and confuse users.
// See related issue: https://github.com/enquirer/enquirer/issues/225
process.exit(0)
},
} as any) as any // eslint-disable-line @typescript-eslint/no-explicit-any
const buildPackages = result.map(({ value }: { value: string }) => value)
// For Vim users (related: https://github.com/enquirer/enquirer/pull/163)
j () {
return this.down()
},
k () {
return this.up()
},
cancel () {
// By default, canceling the prompt via Ctrl+c throws an empty string.
// The custom cancel function prevents that behavior.
// Otherwise, pnpm CLI would print an error and confuse users.
// See related issue: https://github.com/enquirer/enquirer/issues/225
process.exit(0)
},
} as any) as any // eslint-disable-line @typescript-eslint/no-explicit-any
buildPackages = result.map(({ value }: { value: string }) => value)
}
const ignoredPackages = automaticallyIgnoredBuilds.filter((automaticallyIgnoredBuild) => !buildPackages.includes(automaticallyIgnoredBuild))
const allowBuilds: Record<string, boolean | string> = { ...opts.allowBuilds }
if (ignoredPackages.length) {
@@ -102,19 +112,21 @@ export async function handler (opts: ApproveBuildsCommandOpts & RebuildCommandOp
allowBuilds[pkg] = true
}
}
if (buildPackages.length) {
const confirmed = await enquirer.prompt<{ build: boolean }>({
type: 'confirm',
name: 'build',
message: `The next packages will now be built: ${buildPackages.join(', ')}.
if (!opts.all) {
if (buildPackages.length) {
const confirmed = await enquirer.prompt<{ build: boolean }>({
type: 'confirm',
name: 'build',
message: `The next packages will now be built: ${buildPackages.join(', ')}.
Do you approve?`,
initial: false,
})
if (!confirmed.build) {
return
initial: false,
})
if (!confirmed.build) {
return
}
} else {
globalInfo('All packages were added to allowBuilds with value false.')
}
} else {
globalInfo('All packages were added to allowBuilds with value false.')
}
await writeSettings({
...opts,

View File

@@ -176,6 +176,48 @@ test('should approve builds with package.json that has no allowBuilds field defi
})
})
test('approve all builds with --all flag', async () => {
prepare({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '1.0.0',
'@pnpm.e2e/install-script-example': '*',
},
})
const cliOptions = {
argv: [],
dir: process.cwd(),
registry: `http://localhost:${REGISTRY_MOCK_PORT}`,
}
const config = {
...omit(['reporter'], (await getConfig({
cliOptions,
packageManager: { name: 'pnpm', version: '' },
})).config),
storeDir: path.resolve('store'),
cacheDir: path.resolve('cache'),
pnpmfile: [],
enableGlobalVirtualStore: false,
strictDepBuilds: false,
}
await install.handler({ ...config, argv: { original: [] } })
prompt.mockClear()
await approveBuilds.handler({ ...config, all: true })
expect(prompt).not.toHaveBeenCalled()
const workspaceManifest = readYamlFile<any>(path.resolve('pnpm-workspace.yaml')) // eslint-disable-line
expect(workspaceManifest.allowBuilds).toStrictEqual({
'@pnpm.e2e/install-script-example': true,
'@pnpm.e2e/pre-and-postinstall-scripts-example': true,
})
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-preinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/pre-and-postinstall-scripts-example/generated-by-postinstall.js')).toBeTruthy()
expect(fs.existsSync('node_modules/@pnpm.e2e/install-script-example/generated-by-install.js')).toBeTruthy()
})
test('should retain existing allowBuilds entries when approving builds', async () => {
const temp = tempDir()