Skip to content

Commit ab69389

Browse files
leosvelperezAgentEnder
authored andcommitted
fix(core): honor pnpm minimumReleaseAge config on pnpm 11 (#36335)
## Current Behavior pnpm 11 changed `pnpm config list --json` to report configuration keys in camelCase (`minimumReleaseAge`), where pnpm 10 reported them kebab-case (`minimum-release-age`). Nx's pnpm minimum-release-age reader only looked up the kebab-case keys, so on pnpm 11 every explicitly-set value (window, exclude, strict, ignore-missing-time) was ignored. The cooldown window fell back to the built-in 1440-minute default, so `nx migrate` reported 1440 no matter what `minimumReleaseAge` was set to in `pnpm-workspace.yaml`. Setting `NX_MIGRATE_USE_REGISTRY_RESOLUTION=false` was the only workaround. ## Expected Behavior The `minimumReleaseAge` configured in `pnpm-workspace.yaml` (or any other surface pnpm resolves) is honored on pnpm 11. The reader now reads both the camelCase (pnpm 11) and kebab-case (pnpm 10) forms, so pnpm 11 config is applied while pnpm 10 keeps working. ## Related Issue(s) Fixes #36330 <!-- polygraph-session-start --> --- [View session information ↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/gh-36330-7fcd599a) <!-- polygraph-session-end -->
1 parent 6b75f1f commit ab69389

2 files changed

Lines changed: 69 additions & 23 deletions

File tree

packages/nx/src/utils/min-release-age/behavior/pnpm.spec.ts

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -534,9 +534,10 @@ describe('pnpm min-release-age behavior', () => {
534534
});
535535

536536
// readPnpmPolicy reads pnpm's resolved config via `pnpm config list --json`,
537-
// so mock that single spawn rather than any config surface. Keys use pnpm's
538-
// kebab-case form; an exclude array mirrors a yaml surface, a comma-joined
539-
// string mirrors .npmrc / env. pnpm itself decides which surface won.
537+
// so mock that single spawn rather than any config surface. pnpm 11 reports
538+
// keys camelCase, pnpm 10 kebab-case; each test mocks the form its version
539+
// emits. An exclude array mirrors a yaml surface, a comma-joined string
540+
// mirrors .npmrc / env. pnpm itself decides which surface won.
540541
function mockPnpmConfig(config: Record<string, unknown> | 'throw') {
541542
jest
542543
.spyOn(require('child_process'), 'execSync')
@@ -633,7 +634,7 @@ describe('pnpm min-release-age behavior', () => {
633634
);
634635

635636
it('v11 >=11.0.4 explicit window auto-enables strict', async () => {
636-
mockPnpmConfig({ 'minimum-release-age': 2880 });
637+
mockPnpmConfig({ minimumReleaseAge: 2880 });
637638
const result = await readPnpmPolicy('/root', '11.0.4');
638639
expect(result.outcome).toBe('active');
639640
if (result.outcome === 'active') {
@@ -645,8 +646,8 @@ describe('pnpm min-release-age behavior', () => {
645646

646647
it('v11 >=11.0.4 explicit strict:false stays loose', async () => {
647648
mockPnpmConfig({
648-
'minimum-release-age': 2880,
649-
'minimum-release-age-strict': false,
649+
minimumReleaseAge: 2880,
650+
minimumReleaseAgeStrict: false,
650651
});
651652
const result = await readPnpmPolicy('/root', '11.0.4');
652653
expect(result.outcome).toBe('active');
@@ -656,7 +657,7 @@ describe('pnpm min-release-age behavior', () => {
656657
});
657658

658659
it('v11.0.0 explicit window does NOT auto-enable strict', async () => {
659-
mockPnpmConfig({ 'minimum-release-age': 2880 });
660+
mockPnpmConfig({ minimumReleaseAge: 2880 });
660661
const result = await readPnpmPolicy('/root', '11.0.0');
661662
expect(result.outcome).toBe('active');
662663
if (result.outcome === 'active') {
@@ -665,7 +666,7 @@ describe('pnpm min-release-age behavior', () => {
665666
});
666667

667668
it('v11.1.3+ writesExcludes true', async () => {
668-
mockPnpmConfig({ 'minimum-release-age': 1440 });
669+
mockPnpmConfig({ minimumReleaseAge: 1440 });
669670
const result = await readPnpmPolicy('/root', '11.1.3');
670671
expect(result.outcome).toBe('active');
671672
if (result.outcome === 'active') {
@@ -674,7 +675,7 @@ describe('pnpm min-release-age behavior', () => {
674675
});
675676

676677
it('v11.1.2 writesExcludes false', async () => {
677-
mockPnpmConfig({ 'minimum-release-age': 1440 });
678+
mockPnpmConfig({ minimumReleaseAge: 1440 });
678679
const result = await readPnpmPolicy('/root', '11.1.2');
679680
expect(result.outcome).toBe('active');
680681
if (result.outcome === 'active') {
@@ -685,8 +686,8 @@ describe('pnpm min-release-age behavior', () => {
685686
// pnpm reports the resolved exclude as a JSON array (set in a yaml surface).
686687
it('honors an exclude array from pnpm config', async () => {
687688
mockPnpmConfig({
688-
'minimum-release-age': 1440,
689-
'minimum-release-age-exclude': ['pkg-a', 'pkg-b'],
689+
minimumReleaseAge: 1440,
690+
minimumReleaseAgeExclude: ['pkg-a', 'pkg-b'],
690691
});
691692
const result = await readPnpmPolicy('/root', '11.5.2');
692693
expect(result.outcome).toBe('active');
@@ -717,15 +718,15 @@ describe('pnpm min-release-age behavior', () => {
717718
// union) is a version-dependent landmine; nx defers rather than crash.
718719
it('invalid exclude entry -> ambiguous (defer to install)', async () => {
719720
mockPnpmConfig({
720-
'minimum-release-age': 1440,
721-
'minimum-release-age-exclude': ['pkg-a@^1.0.0'],
721+
minimumReleaseAge: 1440,
722+
minimumReleaseAgeExclude: ['pkg-a@^1.0.0'],
722723
});
723724
const result = await readPnpmPolicy('/root', '11.5.2');
724725
expect(result.outcome).toBe('ambiguous');
725726
});
726727

727728
it('v11 ignoreMissingTime defaults to skip; explicit false errors', async () => {
728-
mockPnpmConfig({ 'minimum-release-age': 1440 });
729+
mockPnpmConfig({ minimumReleaseAge: 1440 });
729730
let result = await readPnpmPolicy('/root', '11.5.2');
730731
expect(result.outcome).toBe('active');
731732
if (result.outcome === 'active') {
@@ -735,8 +736,8 @@ describe('pnpm min-release-age behavior', () => {
735736
}
736737

737738
mockPnpmConfig({
738-
'minimum-release-age': 1440,
739-
'minimum-release-age-ignore-missing-time': false,
739+
minimumReleaseAge: 1440,
740+
minimumReleaseAgeIgnoreMissingTime: false,
740741
});
741742
result = await readPnpmPolicy('/root', '11.5.2');
742743
expect(result.outcome).toBe('active');
@@ -746,6 +747,40 @@ describe('pnpm min-release-age behavior', () => {
746747
);
747748
}
748749
});
750+
751+
// pnpm 11 reports config keys camelCase via `config list --json`; pnpm 10
752+
// reported them kebab-case. Reading only the kebab form dropped every
753+
// explicitly-set value on pnpm 11, so the window fell back to the built-in
754+
// 1440 default (gh-36330).
755+
it('honors a camelCase window from pnpm 11 (auto-enables strict)', async () => {
756+
mockPnpmConfig({ minimumReleaseAge: 60 });
757+
const result = await readPnpmPolicy('/root', '11.13.0');
758+
expect(result.outcome).toBe('active');
759+
if (result.outcome === 'active') {
760+
expect(result.policy.windowMs).toBe(60 * MINUTE);
761+
const behavior = pnpmBehavior(result.policy.behavior);
762+
expect(behavior.strict).toBe(true);
763+
expect(behavior.looseFallback).toBe(false);
764+
}
765+
});
766+
767+
it('honors camelCase exclude, strict, and ignoreMissingTime from pnpm 11', async () => {
768+
mockPnpmConfig({
769+
minimumReleaseAge: 2880,
770+
minimumReleaseAgeExclude: ['pkg-a'],
771+
minimumReleaseAgeStrict: false,
772+
minimumReleaseAgeIgnoreMissingTime: false,
773+
});
774+
const result = await readPnpmPolicy('/root', '11.13.0');
775+
expect(result.outcome).toBe('active');
776+
if (result.outcome === 'active') {
777+
expect(result.policy.windowMs).toBe(2880 * MINUTE);
778+
expect(result.policy.isExcluded('pkg-a', '2.0.0')).toBe(true);
779+
const behavior = pnpmBehavior(result.policy.behavior);
780+
expect(behavior.strict).toBe(false);
781+
expect(behavior.missingTimeMap).toBe('error');
782+
}
783+
});
749784
});
750785

751786
describe('NO_MATURE release-age wording (pnpm v11 formatTimeAgo buckets)', () => {

packages/nx/src/utils/min-release-age/behavior/pnpm.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,29 @@ function readPnpmConfigList(root: string): Record<string, unknown> | null {
133133
}
134134
}
135135

136-
// Extracts the cooldown keys from pnpm's reported config. pnpm reports the
137-
// kebab-case keys; the exclude list comes back as a JSON array (set in a yaml
138-
// surface) or a comma-joined string (set via .npmrc / env).
136+
// Extracts the cooldown keys from pnpm's reported config. pnpm 11 reports them
137+
// camelCase; pnpm 10 reported them kebab-case, so read both forms. The exclude
138+
// list comes back as a JSON array (set in a yaml surface) or a comma-joined
139+
// string (set via .npmrc / env).
139140
function parseCooldownConfig(
140141
config: Record<string, unknown>
141142
): PnpmCooldownConfig {
143+
const read = (camelKey: string, kebabKey: string): unknown =>
144+
config[camelKey] ?? config[kebabKey];
142145
return {
143-
windowMinutes: toNumber(config['minimum-release-age']) ?? undefined,
144-
excludes: parseExcludeValue(config['minimum-release-age-exclude']),
145-
strictExplicit: toBoolean(config['minimum-release-age-strict']),
146+
windowMinutes:
147+
toNumber(read('minimumReleaseAge', 'minimum-release-age')) ?? undefined,
148+
excludes: parseExcludeValue(
149+
read('minimumReleaseAgeExclude', 'minimum-release-age-exclude')
150+
),
151+
strictExplicit: toBoolean(
152+
read('minimumReleaseAgeStrict', 'minimum-release-age-strict')
153+
),
146154
ignoreMissingTimeExplicit: toBoolean(
147-
config['minimum-release-age-ignore-missing-time']
155+
read(
156+
'minimumReleaseAgeIgnoreMissingTime',
157+
'minimum-release-age-ignore-missing-time'
158+
)
148159
),
149160
};
150161
}

0 commit comments

Comments
 (0)