Skip to content

Commit 3c5965f

Browse files
jaysooclaude
authored andcommitted
feat(misc): lock in CNW variant 2 with deferred connection (#34416)
## Current Behavior CNW (Create Nx Workspace) has A/B testing logic that randomly selects between variants 0, 1, and 2 for the Nx Cloud connection flow. Each variant shows different prompts and banners. ## Expected Behavior Lock in variant 2 as the permanent behavior: - **No cloud prompt** - users are not asked about Nx Cloud during workspace creation - **Deferred connection** - no `nxCloudId` is written to `nx.json` (uses `skipCloudConnect: true`) - **Variant 2 banner** - shows "Enable remote caching and automatic fixes when CI fails" with a link to complete setup later ### Changes - Simplified `ab-testing.ts` - removed caching, random selection; `getFlowVariant()` always returns `'2'` - `shouldShowCloudPrompt()` always returns `false` - `determineNxCloudV2()` returns `'github'` with `skipCloudConnect: true` for deferred connection - Removed variant 1 banner logic from `messages.ts` - Updated tests to reflect the locked-in behavior ## Demo https://www.loom.com/share/7f688eed6052428cbe91dd9db837cbbd ## Related Issue(s) Closes CLOUD-4255 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> (cherry picked from commit 950265f)
1 parent 9438bbf commit 3c5965f

8 files changed

Lines changed: 92 additions & 251 deletions

File tree

packages/create-nx-workspace/bin/create-nx-workspace.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,8 @@ async function normalizeArgsMiddleware(
645645
Object.assign(argv, {
646646
nxCloud,
647647
useGitHub: nxCloud !== 'skip',
648+
// Deferred connection: skip cloud connect but show banner (CLOUD-4255)
649+
skipCloudConnect: nxCloud !== 'skip',
648650
completionMessageKey,
649651
packageManager,
650652
defaultBase: 'main',
@@ -695,6 +697,7 @@ async function normalizeArgsMiddleware(
695697
let nxCloud: string;
696698
let useGitHub: boolean | undefined;
697699
let completionMessageKey: string | undefined;
700+
let skipCloudConnect = false;
698701

699702
if (argv.skipGit === true) {
700703
nxCloud = 'skip';
@@ -712,11 +715,14 @@ async function normalizeArgsMiddleware(
712715
useGitHub = nxCloud !== 'skip';
713716
completionMessageKey =
714717
nxCloud === 'skip' ? undefined : getCompletionMessageKeyForVariant();
718+
// Deferred connection: skip cloud connect but show banner (CLOUD-4255)
719+
skipCloudConnect = nxCloud !== 'skip';
715720
}
716721

717722
Object.assign(argv, {
718723
nxCloud,
719724
useGitHub,
725+
skipCloudConnect,
720726
completionMessageKey,
721727
packageManager,
722728
defaultBase,

packages/create-nx-workspace/src/internal-utils/prompts.ts

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@ import * as yargs from 'yargs';
22
import * as enquirer from 'enquirer';
33
import * as chalk from 'chalk';
44

5-
import {
6-
MessageKey,
7-
messages,
8-
shouldShowCloudPrompt,
9-
getFlowVariant,
10-
} from '../utils/nx/ab-testing';
5+
import { MessageKey, messages } from '../utils/nx/ab-testing';
116
import { deduceDefaultBase } from '../utils/git/default-base';
127
import { isGitAvailable } from '../utils/git/git';
138
import {
@@ -50,38 +45,9 @@ export async function determineNxCloudV2(
5045
return 'skip';
5146
}
5247

53-
// Variant 2: skip prompt entirely, auto-connect (CLOUD-4235)
54-
if (!shouldShowCloudPrompt()) {
55-
return 'github';
56-
}
57-
58-
// Variant 1: updated messaging; Variant 0: control (CLOUD-4235)
59-
const variant = getFlowVariant();
60-
const message =
61-
variant === '1'
62-
? 'Try the full Nx experience?'
63-
: 'Try the full Nx platform?';
64-
const footer =
65-
variant === '1'
66-
? '\nSet up remote caching and CI that fixes itself in less than 5 minutes.'
67-
: '\nAutomatically fix broken PRs, 70% faster CI: https://nx.dev/nx-cloud';
68-
69-
const promptConfig = {
70-
name: 'nxCloud',
71-
message,
72-
type: 'autocomplete',
73-
choices: [
74-
{ value: 'yes', name: 'Yes' },
75-
{ value: 'skip', name: 'Skip' },
76-
],
77-
initial: 0,
78-
footer: () => chalk.dim(footer),
79-
};
80-
81-
const result = await enquirer.prompt<{ nxCloud: 'github' | 'skip' }>([
82-
promptConfig as any, // types in enquirer are not up to date
83-
]);
84-
return result.nxCloud;
48+
// Auto-select GitHub flow for deferred connection (variant 2 locked in - CLOUD-4255)
49+
// Note: skipCloudConnect=true prevents actual connection, but we still get the banner
50+
return 'github';
8551
}
8652

8753
export async function determineIfGitHubWillBeUsed(

packages/create-nx-workspace/src/utils/nx/ab-testing.spec.ts

Lines changed: 25 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
isEnterpriseCloudUrl,
33
getBannerVariant,
44
shouldShowCloudPrompt,
5+
getFlowVariant,
56
} from './ab-testing';
67

78
describe('ab-testing', () => {
@@ -49,7 +50,7 @@ describe('ab-testing', () => {
4950
});
5051
});
5152

52-
describe('getBannerVariant', () => {
53+
describe('getFlowVariant', () => {
5354
const originalEnv = process.env;
5455

5556
beforeEach(() => {
@@ -63,38 +64,14 @@ describe('ab-testing', () => {
6364

6465
it('should return 0 for docs generation', () => {
6566
process.env.NX_GENERATE_DOCS_PROCESS = 'true';
66-
// Re-import to get fresh module state
67-
const { getBannerVariant: freshGetBannerVariant } = jest.requireActual(
67+
const { getFlowVariant: freshGetFlowVariant } = jest.requireActual(
6868
'./ab-testing'
6969
) as typeof import('./ab-testing');
70-
expect(freshGetBannerVariant('https://cloud.nx.app/connect/abc')).toBe(
71-
'0'
72-
);
73-
});
74-
75-
it('should return 0 for enterprise URLs', () => {
76-
expect(
77-
getBannerVariant('https://enterprise.company.com/connect/abc')
78-
).toBe('0');
79-
});
80-
81-
it('should respect NX_CNW_FLOW_VARIANT env variable', () => {
82-
process.env.NX_CNW_FLOW_VARIANT = '2';
83-
const { getBannerVariant: freshGetBannerVariant } = jest.requireActual(
84-
'./ab-testing'
85-
) as typeof import('./ab-testing');
86-
expect(freshGetBannerVariant('https://cloud.nx.app/connect/abc')).toBe(
87-
'2'
88-
);
89-
});
90-
91-
it('should return a valid variant (0, 1, or 2) for standard URLs', () => {
92-
const variant = getBannerVariant('https://cloud.nx.app/connect/abc');
93-
expect(['0', '1', '2']).toContain(variant);
70+
expect(freshGetFlowVariant()).toBe('0');
9471
});
9572
});
9673

97-
describe('shouldShowCloudPrompt', () => {
74+
describe('getBannerVariant', () => {
9875
const originalEnv = process.env;
9976

10077
beforeEach(() => {
@@ -106,25 +83,30 @@ describe('ab-testing', () => {
10683
process.env = originalEnv;
10784
});
10885

109-
it('should return true for variant 0', () => {
110-
process.env.NX_CNW_FLOW_VARIANT = '0';
111-
const { shouldShowCloudPrompt: freshShouldShowCloudPrompt } =
112-
jest.requireActual('./ab-testing') as typeof import('./ab-testing');
113-
expect(freshShouldShowCloudPrompt()).toBe(true);
86+
it('should return 0 for docs generation', () => {
87+
process.env.NX_GENERATE_DOCS_PROCESS = 'true';
88+
const { getBannerVariant: freshGetBannerVariant } = jest.requireActual(
89+
'./ab-testing'
90+
) as typeof import('./ab-testing');
91+
expect(freshGetBannerVariant('https://cloud.nx.app/connect/abc')).toBe(
92+
'0'
93+
);
11494
});
11595

116-
it('should return true for variant 1', () => {
117-
process.env.NX_CNW_FLOW_VARIANT = '1';
118-
const { shouldShowCloudPrompt: freshShouldShowCloudPrompt } =
119-
jest.requireActual('./ab-testing') as typeof import('./ab-testing');
120-
expect(freshShouldShowCloudPrompt()).toBe(true);
96+
it('should return 0 for enterprise URLs', () => {
97+
expect(
98+
getBannerVariant('https://enterprise.company.com/connect/abc')
99+
).toBe('0');
121100
});
122101

123-
it('should return false for variant 2', () => {
124-
process.env.NX_CNW_FLOW_VARIANT = '2';
125-
const { shouldShowCloudPrompt: freshShouldShowCloudPrompt } =
126-
jest.requireActual('./ab-testing') as typeof import('./ab-testing');
127-
expect(freshShouldShowCloudPrompt()).toBe(false);
102+
it('should return 2 for standard URLs (locked in CLOUD-4255)', () => {
103+
expect(getBannerVariant('https://cloud.nx.app/connect/abc')).toBe('2');
104+
});
105+
});
106+
107+
describe('shouldShowCloudPrompt', () => {
108+
it('should always return false (variant 2 locked in CLOUD-4255)', () => {
109+
expect(shouldShowCloudPrompt()).toBe(false);
128110
});
129111
});
130112
});

packages/create-nx-workspace/src/utils/nx/ab-testing.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { execSync } from 'node:child_process';
22
import {
33
existsSync,
44
readFileSync,
5-
writeFileSync,
65
statSync,
76
unlinkSync,
7+
writeFileSync,
88
} from 'node:fs';
99
import { join } from 'node:path';
1010
import { tmpdir } from 'node:os';
@@ -80,7 +80,8 @@ function getFlowVariantInternal(): string {
8080
}
8181

8282
/**
83-
* Returns the flow variant for tracking (0 = preset, 1 = template).
83+
* Returns the flow variant for tracking (0, 1, or 2).
84+
* Returns '0' for docs generation to preserve deterministic output.
8485
*/
8586
export function getFlowVariant(): string {
8687
if (process.env.NX_GENERATE_DOCS_PROCESS === 'true') {
@@ -97,12 +98,10 @@ export function getCompletionMessageKeyForVariant(): CompletionMessageKey {
9798
return 'platform-setup';
9899
}
99100

100-
/**
101-
* Returns whether the cloud prompt should be shown.
102-
* Variant 2 skips the prompt (auto-connect). (CLOUD-4235)
103-
*/
104101
export function shouldShowCloudPrompt(): boolean {
105-
return getFlowVariant() !== '2';
102+
// CLOUD-4255: Lock to variant 2 behavior (no prompt)
103+
// To re-enable A/B testing: return getFlowVariant() !== '2';
104+
return false;
106105
}
107106

108107
// ============================================================================
@@ -136,9 +135,8 @@ export function isEnterpriseCloudUrl(cloudUrl?: string): boolean {
136135

137136
/**
138137
* Get the banner variant for completion messages.
139-
* Uses NX_CNW_FLOW_VARIANT to determine which banner to show.
140-
* - Variant 0: Plain link (control) - always used for enterprise URLs
141-
* - Variant 1: "Finish your set up in 5 minutes" banner
138+
* Now locked to variant 2 (CLOUD-4255).
139+
* - Variant 0: Plain link - used for enterprise URLs and docs generation
142140
* - Variant 2: "Enable remote caching and automatic fixes" banner
143141
*
144142
* @param cloudUrl - The Nx Cloud URL. If enterprise, always returns '0'.
@@ -149,8 +147,13 @@ export function getBannerVariant(cloudUrl?: string): BannerVariant {
149147
return '0';
150148
}
151149

152-
// Use the flow variant (which handles docs generation, env var, and caching)
153-
return getFlowVariant() as BannerVariant;
150+
// Docs generation uses variant 0 for deterministic output
151+
if (process.env.NX_GENERATE_DOCS_PROCESS === 'true') {
152+
return '0';
153+
}
154+
155+
// Standard URLs get variant 2 banner
156+
return '2';
154157
}
155158

156159
export const NxCloudChoices = [

packages/create-nx-workspace/src/utils/nx/messages.spec.ts

Lines changed: 0 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -292,104 +292,6 @@ describe('Nx Cloud Messages', () => {
292292
});
293293
});
294294

295-
describe('Banner Variant Messages (CLOUD-4235)', () => {
296-
it('variant 0 should show plain link message', () => {
297-
const message = getCompletionMessage(
298-
'platform-setup',
299-
'https://cloud.nx.app/connect/abc123',
300-
VcsPushStatus.PushedToVcs,
301-
'myworkspace',
302-
'0'
303-
);
304-
expect(message).toMatchInlineSnapshot(`
305-
{
306-
"bodyLines": [
307-
"Go to Nx Cloud and finish the setup: https://cloud.nx.app/connect/abc123",
308-
],
309-
"title": "Your platform setup is almost complete.",
310-
}
311-
`);
312-
});
313-
314-
it('variant 1 should show "Finish your set up in 5 minutes" banner', () => {
315-
const message = getCompletionMessage(
316-
'platform-setup',
317-
'https://cloud.nx.app/connect/abc123',
318-
VcsPushStatus.PushedToVcs,
319-
'myworkspace',
320-
'1'
321-
);
322-
expect(message.title).toBe(
323-
'Nx Cloud configuration was successfully added.'
324-
);
325-
expect(message.bodyLines.length).toBeGreaterThan(1);
326-
expect(message.bodyLines.join('\n')).toContain(
327-
'Finish your set up in 5 minutes'
328-
);
329-
expect(message.bodyLines.join('\n')).toContain(
330-
'https://cloud.nx.app/connect/abc123'
331-
);
332-
expect(message.bodyLines.join('\n')).toContain(
333-
'Automatically fix CI failures'
334-
);
335-
});
336-
337-
it('variant 2 should show "Enable remote caching and automatic fixes when CI fails" banner', () => {
338-
const message = getCompletionMessage(
339-
'platform-setup',
340-
'https://cloud.nx.app/connect/abc123',
341-
VcsPushStatus.PushedToVcs,
342-
'myworkspace',
343-
'2'
344-
);
345-
expect(message.title).toBe(
346-
'Nx Cloud configuration was successfully added.'
347-
);
348-
expect(message.bodyLines.length).toBeGreaterThan(1);
349-
expect(message.bodyLines.join('\n')).toContain(
350-
'Enable remote caching and automatic fixes when CI fails'
351-
);
352-
expect(message.bodyLines.join('\n')).toContain(
353-
'https://cloud.nx.app/connect/abc123'
354-
);
355-
expect(message.bodyLines.join('\n')).toContain(
356-
'Set it up in less than 5 minutes'
357-
);
358-
});
359-
360-
it('should fall back to plain link when URL is null (all variants)', () => {
361-
const variants = ['0', '1', '2'] as const;
362-
variants.forEach((variant) => {
363-
const message = getCompletionMessage(
364-
'platform-setup',
365-
null,
366-
VcsPushStatus.PushedToVcs,
367-
'myworkspace',
368-
variant
369-
);
370-
// All variants fall back to plain message when URL is null
371-
expect(message.bodyLines).toHaveLength(1);
372-
expect(message.bodyLines[0]).toBe(
373-
'Return to Nx Cloud and finish the setup.'
374-
);
375-
});
376-
});
377-
378-
it('should default to variant 0 when bannerVariant is not provided', () => {
379-
const message = getCompletionMessage(
380-
'platform-setup',
381-
'https://cloud.nx.app/connect/abc123',
382-
VcsPushStatus.PushedToVcs,
383-
'myworkspace'
384-
// no bannerVariant - defaults to '0'
385-
);
386-
expect(message.bodyLines).toHaveLength(1);
387-
expect(message.bodyLines[0]).toContain(
388-
'Go to Nx Cloud and finish the setup:'
389-
);
390-
});
391-
});
392-
393295
describe('Default Messages', () => {
394296
it('should default to ci-setup when no key is provided', () => {
395297
const message = getCompletionMessage(

0 commit comments

Comments
 (0)