Skip to content

Commit 2a1192e

Browse files
isaacsruyadorno
authored andcommitted
Do not run interactive exec in CI when a TTY
Credit: @isaacs PR-URL: #2202 Close: #2202 Reviewed-by: @darcyclarke
1 parent 15d7333 commit 2a1192e

File tree

3 files changed

+72
-28
lines changed

3 files changed

+72
-28
lines changed

docs/content/commands/npm-exec.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ npx -c '<cmd> [args...]'
1818
npx -p <pkg>[@<specifier>] -c '<cmd> [args...]'
1919
Run without --call or positional args to open interactive subshell
2020

21-
2221
alias: npm x, npx
2322

2423
common options:
@@ -35,7 +34,8 @@ as running it via `npm run`.
3534

3635
Run without positional arguments or `--call`, this allows you to
3736
interactively run commands in the same sort of shell environment that
38-
`package.json` scripts are run.
37+
`package.json` scripts are run. Interactive mode is not supported in CI
38+
environments when standard input is a TTY, to prevent hangs.
3939

4040
Whatever packages are specified by the `--package` option will be
4141
provided in the `PATH` of the executed command, along with any locally

lib/exec.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ const run = async ({ args, call, pathArr, shell }) => {
8383

8484
npm.log.disableProgress()
8585
try {
86+
if (script === shell) {
87+
if (process.stdin.isTTY) {
88+
if (ciDetect())
89+
return npm.log.warn('exec', 'Interactive mode disabled in CI environment')
90+
output(`\nEntering npm script environment\nType 'exit' or ^D when finished\n`)
91+
}
92+
}
8693
return await runScript({
8794
...npm.flatOptions,
8895
pkg,
@@ -111,7 +118,6 @@ const exec = async args => {
111118

112119
// nothing to maybe install, skip the arborist dance
113120
if (!call && !args.length && !packages.length) {
114-
output(`\nEntering npm script environment\nType 'exit' or ^D when finished\n`)
115121
return await run({
116122
args,
117123
call,
@@ -194,13 +200,12 @@ const exec = async args => {
194200

195201
// no need to install if already present
196202
if (add.length) {
197-
const isTTY = process.stdin.isTTY && process.stdout.isTTY
198203
if (!npm.flatOptions.yes) {
199204
// set -n to always say no
200205
if (npm.flatOptions.yes === false)
201206
throw 'canceled'
202207

203-
if (!isTTY || ciDetect()) {
208+
if (!process.stdin.isTTY || ciDetect()) {
204209
npm.log.warn('exec', `The following package${
205210
add.length === 1 ? ' was' : 's were'
206211
} not found and will be installed: ${

test/lib/exec.js

+62-23
Original file line numberDiff line numberDiff line change
@@ -198,30 +198,69 @@ t.test('npm exec foo, already present locally', async t => {
198198
})
199199

200200
t.test('npm exec <noargs>, run interactive shell', async t => {
201-
ARB_CTOR.length = 0
202-
MKDIRPS.length = 0
203-
ARB_REIFY.length = 0
204-
OUTPUT.length = 0
205-
await exec([], er => {
206-
if (er)
207-
throw er
201+
CI_NAME = null
202+
const { isTTY } = process.stdin
203+
process.stdin.isTTY = true
204+
t.teardown(() => process.stdin.isTTY = isTTY)
205+
206+
const run = async (t, doRun = true) => {
207+
LOG_WARN.length = 0
208+
ARB_CTOR.length = 0
209+
MKDIRPS.length = 0
210+
ARB_REIFY.length = 0
211+
OUTPUT.length = 0
212+
await exec([], er => {
213+
if (er)
214+
throw er
215+
})
216+
t.strictSame(MKDIRPS, [], 'no need to make any dirs')
217+
t.strictSame(ARB_CTOR, [], 'no need to instantiate arborist')
218+
t.strictSame(ARB_REIFY, [], 'no need to reify anything')
219+
t.equal(PROGRESS_ENABLED, true, 'progress re-enabled')
220+
if (doRun) {
221+
t.match(RUN_SCRIPTS, [{
222+
pkg: { scripts: { npx: 'shell-cmd' } },
223+
banner: false,
224+
path: process.cwd(),
225+
stdioString: true,
226+
event: 'npx',
227+
env: { PATH: process.env.PATH },
228+
stdio: 'inherit',
229+
}])
230+
} else
231+
t.strictSame(RUN_SCRIPTS, [])
232+
RUN_SCRIPTS.length = 0
233+
}
234+
235+
t.test('print message when tty and not in CI', async t => {
236+
CI_NAME = null
237+
process.stdin.isTTY = true
238+
await run(t)
239+
t.strictSame(LOG_WARN, [])
240+
t.strictSame(OUTPUT, [
241+
['\nEntering npm script environment\nType \'exit\' or ^D when finished\n'],
242+
], 'printed message about interactive shell')
208243
})
209-
t.strictSame(OUTPUT, [
210-
['\nEntering npm script environment\nType \'exit\' or ^D when finished\n'],
211-
], 'printed message about interactive shell')
212-
t.strictSame(MKDIRPS, [], 'no need to make any dirs')
213-
t.strictSame(ARB_CTOR, [], 'no need to instantiate arborist')
214-
t.strictSame(ARB_REIFY, [], 'no need to reify anything')
215-
t.equal(PROGRESS_ENABLED, true, 'progress re-enabled')
216-
t.match(RUN_SCRIPTS, [{
217-
pkg: { scripts: { npx: 'shell-cmd' } },
218-
banner: false,
219-
path: process.cwd(),
220-
stdioString: true,
221-
event: 'npx',
222-
env: { PATH: process.env.PATH },
223-
stdio: 'inherit',
224-
}])
244+
245+
t.test('no message when not TTY', async t => {
246+
CI_NAME = null
247+
process.stdin.isTTY = false
248+
await run(t)
249+
t.strictSame(LOG_WARN, [])
250+
t.strictSame(OUTPUT, [], 'no message about interactive shell')
251+
})
252+
253+
t.test('print warning when in CI and interactive', async t => {
254+
CI_NAME = 'travis-ci'
255+
process.stdin.isTTY = true
256+
await run(t, false)
257+
t.strictSame(LOG_WARN, [
258+
['exec', 'Interactive mode disabled in CI environment'],
259+
])
260+
t.strictSame(OUTPUT, [], 'no message about interactive shell')
261+
})
262+
263+
t.end()
225264
})
226265

227266
t.test('npm exec foo, not present locally or in central loc', async t => {

0 commit comments

Comments
 (0)