Skip to content

Commit a651fc4

Browse files
committed
Fix publish hanging indefinitely
Adds stdin inheritance and timeout to prevent publish from hanging when: - npm/yarn prompts for authentication or OTP - Lifecycle scripts run in watch mode - Other unexpected blocking conditions occur The timeout (3 minutes) matches git network operations timeout. Fixes #312
1 parent 3811182 commit a651fc4

File tree

3 files changed

+71
-11
lines changed

3 files changed

+71
-11
lines changed

readme.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,59 @@ npm ERR! 403 Forbidden - GET https://registry.yarnpkg.com/-/package/my-awesome-p
337337
}
338338
```
339339

340+
### np hangs during the "Publishing package" step
341+
342+
If `np` hangs indefinitely during publishing, common causes include:
343+
344+
**Lifecycle scripts that don't exit**
345+
346+
npm automatically runs lifecycle hooks like `prepublish`, `publish`, and `postpublish` during publishing. If these scripts don't exit (e.g., running in watch mode), `np` will hang.
347+
348+
```json
349+
{
350+
"scripts": {
351+
"test": "vitest",
352+
"publish": "npm run test && np"
353+
}
354+
}
355+
```
356+
357+
**Solution**: Don't name your scripts `publish`, `prepublish`, or `postpublish` (these are reserved npm lifecycle hooks). Use names like `release` instead:
358+
359+
```json
360+
{
361+
"scripts": {
362+
"test": "vitest run",
363+
"release": "np"
364+
}
365+
}
366+
```
367+
368+
**Tests running in watch mode**
369+
370+
If your test script runs in watch mode, it won't exit after running tests.
371+
372+
**Solution**: Ensure your test command exits after running:
373+
374+
```json
375+
{
376+
"scripts": {
377+
"test": "vitest run",
378+
"test:dev": "vitest"
379+
}
380+
}
381+
```
382+
383+
**Registry configuration issues**
384+
385+
A missing trailing slash in `.npmrc` registry configuration can cause hangs.
386+
387+
**Solution**: Ensure registry URLs have a trailing slash:
388+
389+
```npmrc
390+
@ORG:registry=https://npm.pkg.github.com/
391+
```
392+
340393
## Maintainers
341394

342395
- [Sindre Sorhus](https://github.com/sindresorhus)

source/npm/publish.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,17 @@ export const getPackagePublishArguments = options => {
1818
return arguments_;
1919
};
2020

21+
// 3 minutes timeout for publish operations (like git network operations)
22+
// Publishing can take time for large packages or slow connections
23+
const publishTimeout = 180_000;
24+
2125
export function runPublish(arguments_, options = {}) {
22-
const execaOptions = {};
26+
const execaOptions = {
27+
// Inherit stdin to allow password/OTP prompts from npm/yarn
28+
stdin: 'inherit',
29+
// Timeout to prevent infinite hangs (e.g., from lifecycle scripts in watch mode)
30+
timeout: publishTimeout,
31+
};
2332

2433
// `npm` 8.5+ has a bug where `npm publish <folder>` publishes from cwd instead of <folder>.
2534
// We work around this by changing cwd to the target directory.
@@ -28,14 +37,5 @@ export function runPublish(arguments_, options = {}) {
2837
execaOptions.cwd = options.cwd;
2938
}
3039

31-
const cp = execa(...arguments_, execaOptions);
32-
33-
cp.stdout.on('data', chunk => {
34-
// https://github.com/yarnpkg/berry/blob/a3e5695186f2aec3a68810acafc6c9b1e45191da/packages/plugin-npm/sources/npmHttpUtils.ts#L541
35-
if (chunk.toString('utf8').includes('One-time password:')) {
36-
cp.kill();
37-
}
38-
});
39-
40-
return cp;
40+
return execa(...arguments_, execaOptions);
4141
}

test/npm/publish.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ test('runPublish uses cwd option when provided', async t => {
3333
const result = await runPublish(['echo', ['test']], {cwd: '/tmp'});
3434
t.is(result.cwd, '/tmp');
3535
});
36+
37+
test('runPublish sets stdin to inherit and includes timeout', async t => {
38+
const result = runPublish(['echo', ['test']]);
39+
t.not(result, undefined);
40+
// Process should complete successfully with our default options
41+
await t.notThrowsAsync(result);
42+
});

0 commit comments

Comments
 (0)