Skip to content

Commit bdbc149

Browse files
authored
feat(core): add --otp to top-level nx release command and detect EOTP errors (#34473)
## Current Behavior When publish fails due to missing OTP code, its not clear as a user who is using the top level command what to do next. ## Expected Behavior Add the --otp flag to the top-level `nx release` command so users can provide a one-time password for 2FA-enabled registries when running the full release orchestration (version + changelog + publish). When publish fails due to an expired or missing OTP (EOTP error), display a helpful warning listing affected projects and the exact command to re-run the publish step in isolation with a new OTP. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
1 parent dc67168 commit bdbc149

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

packages/nx/src/command-line/release/command-object.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export type ReleaseOptions = NxReleaseArgs &
107107
yes?: boolean;
108108
preid?: VersionOptions['preid'];
109109
skipPublish?: boolean;
110+
otp?: number;
110111
};
111112

112113
export type VersionPlanArgs = {
@@ -225,6 +226,11 @@ const releaseCommand: CommandModule<NxReleaseArgs, ReleaseOptions> = {
225226
description:
226227
'Skip publishing by automatically answering no to the confirmation prompt for publishing.',
227228
})
229+
.option('otp', {
230+
type: 'number',
231+
description:
232+
'A one-time password for publishing to a registry that requires 2FA.',
233+
})
228234
.check((argv) => {
229235
if (argv.yes !== undefined && argv.skipPublish !== undefined) {
230236
throw new Error(

packages/nx/src/command-line/release/publish.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
runPreTasksExecution,
1616
} from '../../project-graph/plugins/tasks-execution-hooks';
1717
import { createProjectGraphAsync } from '../../project-graph/project-graph';
18+
import { TaskResult } from '../../tasks-runner/life-cycle';
1819
import { runCommandForTasks } from '../../tasks-runner/run-command';
1920
import {
2021
createOverrides,
@@ -330,6 +331,24 @@ async function runPublishOnProjects(
330331
code: taskData.code,
331332
};
332333
}
334+
335+
// Check for EOTP errors and provide a helpful re-run command
336+
const eotpFailedProjects = getEOTPFailedProjects(taskResults);
337+
if (eotpFailedProjects.length > 0) {
338+
output.warn({
339+
title:
340+
'One or more packages failed to publish because a valid OTP was not provided or has expired.',
341+
bodyLines: [
342+
'Affected projects:',
343+
...eotpFailedProjects.map((p) => ` - ${p}`),
344+
'',
345+
'You can provide a new OTP and re-run the publish step in isolation:',
346+
'',
347+
` ${buildRerunCommand(args)}`,
348+
],
349+
});
350+
}
351+
333352
await runPostTasksExecution({
334353
id,
335354
taskResults,
@@ -342,3 +361,54 @@ async function runPublishOnProjects(
342361

343362
return publishProjectsResult;
344363
}
364+
365+
/**
366+
* Return project names for failed tasks that contain EOTP error indicators in their terminal output.
367+
* npm returns error code "EOTP" in JSON output.
368+
* pnpm returns "EOTP" in error messages.
369+
* Both will appear in the captured terminal output.
370+
*/
371+
function getEOTPFailedProjects(
372+
taskResults: Record<string, TaskResult>
373+
): string[] {
374+
return Object.values(taskResults)
375+
.filter(
376+
(result) =>
377+
result.code !== 0 &&
378+
result.terminalOutput &&
379+
(result.terminalOutput.includes('EOTP') ||
380+
result.terminalOutput.includes('one-time pass') ||
381+
result.terminalOutput.includes('one-time password'))
382+
)
383+
.map((result) => result.task.target.project);
384+
}
385+
386+
function buildRerunCommand(args: PublishOptions): string {
387+
const parts = ['nx release publish'];
388+
389+
if (args.registry) {
390+
parts.push(`--registry=${args.registry}`);
391+
}
392+
if (args.tag) {
393+
parts.push(`--tag=${args.tag}`);
394+
}
395+
if (args.access) {
396+
parts.push(`--access=${args.access}`);
397+
}
398+
if (args.projects?.length) {
399+
parts.push(`--projects=${args.projects.join(',')}`);
400+
}
401+
if (args.groups?.length) {
402+
parts.push(`--groups=${args.groups.join(',')}`);
403+
}
404+
if (args.firstRelease) {
405+
parts.push('--first-release');
406+
}
407+
if (args.verbose) {
408+
parts.push('--verbose');
409+
}
410+
411+
parts.push('--otp=REPLACE_WITH_NEW_OTP');
412+
413+
return parts.join(' ');
414+
}

0 commit comments

Comments
 (0)