Skip to content

Commit 03b300a

Browse files
authored
Fix constraints and update yarn lock at the end of release process (#145)
1 parent 3fb952e commit 03b300a

6 files changed

+411
-1
lines changed

src/functional.test.ts

+260
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ describe('create-release-branch (functional)', () => {
8383
private: true,
8484
workspaces: ['packages/*'],
8585
scripts: { foo: 'bar' },
86+
packageManager: '[email protected]',
8687
});
8788
expect(
8889
await environment.readJsonFileWithinPackage('a', 'package.json'),
@@ -195,6 +196,7 @@ describe('create-release-branch (functional)', () => {
195196
private: true,
196197
workspaces: ['packages/*'],
197198
scripts: { foo: 'bar' },
199+
packageManager: '[email protected]',
198200
});
199201
expect(
200202
await environment.readJsonFileWithinPackage('a', 'package.json'),
@@ -640,6 +642,263 @@ describe('create-release-branch (functional)', () => {
640642
);
641643
});
642644

645+
it('updates the dependency version in package "b" when package "a" version is bumped', async () => {
646+
await withMonorepoProjectEnvironment(
647+
{
648+
packages: {
649+
$root$: {
650+
name: '@scope/monorepo',
651+
version: '1.0.0',
652+
directoryPath: '.',
653+
},
654+
a: {
655+
name: '@scope/a',
656+
version: '1.0.0',
657+
directoryPath: 'packages/a',
658+
},
659+
b: {
660+
name: '@scope/b',
661+
version: '2.0.0',
662+
directoryPath: 'packages/b',
663+
},
664+
},
665+
workspaces: {
666+
'.': ['packages/*'],
667+
},
668+
},
669+
async (environment) => {
670+
await environment.updateJsonFileWithinPackage('b', 'package.json', {
671+
dependencies: {
672+
'@scope/a': '1.0.0',
673+
},
674+
});
675+
const constraintsProContent = `
676+
% All packages must have a name and version defined.
677+
\\+ gen_enforced_field(_, 'name', null).
678+
\\+ gen_enforced_field(_, 'version', null).
679+
680+
% All version ranges used to reference one workspace package in another workspace package's \`dependencies\` or \`devDependencies\` must match the current version of that package.
681+
gen_enforced_dependency(Pkg, DependencyIdent, CorrectDependencyRange, DependencyType) :-
682+
DependencyType \\= 'peerDependencies',
683+
workspace_has_dependency(Pkg, DependencyIdent, _, DependencyType),
684+
workspace_ident(DepPkg, DependencyIdent),
685+
workspace_version(DepPkg, DependencyVersion),
686+
atomic_list_concat(['^', DependencyVersion], CorrectDependencyRange),
687+
Pkg \\= DepPkg. % Ensure we do not add self-dependency
688+
689+
% Entry point to check all constraints.
690+
workspace_package(Pkg) :-
691+
package_json(Pkg, _, _).
692+
693+
enforce_all :-
694+
workspace_package(Pkg),
695+
enforce_has_name(Pkg),
696+
enforce_has_version(Pkg),
697+
(package_json(Pkg, 'dependencies', Deps) -> enforce_dependencies(Pkg, Deps) ; true).
698+
699+
enforce_has_name(Pkg) :-
700+
package_json(Pkg, 'name', _).
701+
702+
enforce_has_version(Pkg) :-
703+
package_json(Pkg, 'version', _).
704+
705+
enforce_dependencies(_, []).
706+
enforce_dependencies(Pkg, [DepPkg-DepVersion | Rest]) :-
707+
workspace_package(DepPkg),
708+
package_json(DepPkg, 'version', DepVersion),
709+
enforce_dependency_version(Pkg, DepPkg),
710+
enforce_dependencies(Pkg, Rest).
711+
712+
enforce_dependency_version(Pkg, DepPkg) :-
713+
package_json(Pkg, 'dependencies', Deps),
714+
package_json(DepPkg, 'version', DepVersion),
715+
member(DepPkg-DepVersion, Deps).
716+
717+
update_dependency_version(Pkg, DepPkg) :-
718+
package_json(Pkg, 'dependencies', Deps),
719+
package_json(DepPkg, 'version', DepVersion),
720+
\\+ member(DepPkg-DepVersion, Deps),
721+
Pkg \\= DepPkg, % Ensure we do not add self-dependency
722+
set_package_json(Pkg, 'dependencies', DepPkg, DepVersion).
723+
`;
724+
725+
await environment.writeFile('constraints.pro', constraintsProContent);
726+
await environment.runTool({
727+
releaseSpecification: {
728+
packages: {
729+
a: 'major',
730+
b: 'intentionally-skip',
731+
},
732+
},
733+
});
734+
735+
expect(
736+
await environment.readJsonFileWithinPackage('a', 'package.json'),
737+
).toStrictEqual({
738+
name: '@scope/a',
739+
version: '2.0.0',
740+
});
741+
expect(
742+
await environment.readJsonFileWithinPackage('b', 'package.json'),
743+
).toStrictEqual({
744+
name: '@scope/b',
745+
version: '2.0.0',
746+
dependencies: { '@scope/a': '^2.0.0' },
747+
});
748+
},
749+
);
750+
});
751+
752+
it('updates the yarn lock file', async () => {
753+
await withMonorepoProjectEnvironment(
754+
{
755+
packages: {
756+
$root$: {
757+
name: '@scope/monorepo',
758+
version: '1.0.0',
759+
directoryPath: '.',
760+
},
761+
a: {
762+
name: '@scope/a',
763+
version: '1.0.0',
764+
directoryPath: 'packages/a',
765+
},
766+
b: {
767+
name: '@scope/b',
768+
version: '2.0.0',
769+
directoryPath: 'packages/b',
770+
},
771+
},
772+
workspaces: {
773+
'.': ['packages/*'],
774+
},
775+
},
776+
async (environment) => {
777+
await environment.updateJsonFileWithinPackage('b', 'package.json', {
778+
dependencies: {
779+
'@scope/a': '1.0.0',
780+
},
781+
});
782+
const constraintsProContent = `
783+
% All packages must have a name and version defined.
784+
\\+ gen_enforced_field(_, 'name', null).
785+
\\+ gen_enforced_field(_, 'version', null).
786+
787+
% All version ranges used to reference one workspace package in another workspace package's \`dependencies\` or \`devDependencies\` must match the current version of that package.
788+
gen_enforced_dependency(Pkg, DependencyIdent, CorrectDependencyRange, DependencyType) :-
789+
DependencyType \\= 'peerDependencies',
790+
workspace_has_dependency(Pkg, DependencyIdent, _, DependencyType),
791+
workspace_ident(DepPkg, DependencyIdent),
792+
workspace_version(DepPkg, DependencyVersion),
793+
atomic_list_concat(['^', DependencyVersion], CorrectDependencyRange),
794+
Pkg \\= DepPkg. % Ensure we do not add self-dependency
795+
796+
% Entry point to check all constraints.
797+
workspace_package(Pkg) :-
798+
package_json(Pkg, _, _).
799+
800+
enforce_all :-
801+
workspace_package(Pkg),
802+
enforce_has_name(Pkg),
803+
enforce_has_version(Pkg),
804+
(package_json(Pkg, 'dependencies', Deps) -> enforce_dependencies(Pkg, Deps) ; true).
805+
806+
enforce_has_name(Pkg) :-
807+
package_json(Pkg, 'name', _).
808+
809+
enforce_has_version(Pkg) :-
810+
package_json(Pkg, 'version', _).
811+
812+
enforce_dependencies(_, []).
813+
enforce_dependencies(Pkg, [DepPkg-DepVersion | Rest]) :-
814+
workspace_package(DepPkg),
815+
package_json(DepPkg, 'version', DepVersion),
816+
enforce_dependency_version(Pkg, DepPkg),
817+
enforce_dependencies(Pkg, Rest).
818+
819+
enforce_dependency_version(Pkg, DepPkg) :-
820+
package_json(Pkg, 'dependencies', Deps),
821+
package_json(DepPkg, 'version', DepVersion),
822+
member(DepPkg-DepVersion, Deps).
823+
824+
update_dependency_version(Pkg, DepPkg) :-
825+
package_json(Pkg, 'dependencies', Deps),
826+
package_json(DepPkg, 'version', DepVersion),
827+
\\+ member(DepPkg-DepVersion, Deps),
828+
Pkg \\= DepPkg, % Ensure we do not add self-dependency
829+
set_package_json(Pkg, 'dependencies', DepPkg, DepVersion).
830+
`;
831+
await environment.writeFile('constraints.pro', constraintsProContent);
832+
const outdatedLockfile = `
833+
# This file is generated by running "yarn install" inside your project.
834+
# Manual changes might be lost - proceed with caution!
835+
836+
__metadata:
837+
version: 6
838+
839+
"@scope/a@^1.0.0, @scope/a@workspace:packages/a":
840+
version: 0.0.0-use.local
841+
resolution: "@scope/a@workspace:packages/a"
842+
languageName: unknown
843+
linkType: soft
844+
845+
"@scope/b@workspace:packages/b":
846+
version: 0.0.0-use.local
847+
resolution: "@scope/b@workspace:packages/b"
848+
dependencies:
849+
"@scope/a": ^1.0.0
850+
languageName: unknown
851+
linkType: soft
852+
853+
"@scope/monorepo@workspace:.":
854+
version: 0.0.0-use.local
855+
resolution: "@scope/monorepo@workspace:."
856+
languageName: unknown
857+
linkType: soft`;
858+
await environment.writeFile('yarn.lock', outdatedLockfile);
859+
await environment.runTool({
860+
releaseSpecification: {
861+
packages: {
862+
a: 'major',
863+
b: 'intentionally-skip',
864+
},
865+
},
866+
});
867+
868+
const updatedLockfile = `# This file is generated by running "yarn install" inside your project.
869+
# Manual changes might be lost - proceed with caution!
870+
871+
__metadata:
872+
version: 6
873+
874+
"@scope/a@^2.0.0, @scope/a@workspace:packages/a":
875+
version: 0.0.0-use.local
876+
resolution: "@scope/a@workspace:packages/a"
877+
languageName: unknown
878+
linkType: soft
879+
880+
"@scope/b@workspace:packages/b":
881+
version: 0.0.0-use.local
882+
resolution: "@scope/b@workspace:packages/b"
883+
dependencies:
884+
"@scope/a": ^2.0.0
885+
languageName: unknown
886+
linkType: soft
887+
888+
"@scope/monorepo@workspace:.":
889+
version: 0.0.0-use.local
890+
resolution: "@scope/monorepo@workspace:."
891+
languageName: unknown
892+
linkType: soft
893+
`;
894+
895+
expect(await environment.readFile('yarn.lock')).toStrictEqual(
896+
updatedLockfile,
897+
);
898+
},
899+
);
900+
});
901+
643902
it('does not update the versions of any packages that have been tagged with intentionally-skip', async () => {
644903
await withMonorepoProjectEnvironment(
645904
{
@@ -691,6 +950,7 @@ describe('create-release-branch (functional)', () => {
691950
version: '2.0.0',
692951
private: true,
693952
workspaces: ['packages/*'],
953+
packageManager: '[email protected]',
694954
});
695955
expect(
696956
await environment.readJsonFileWithinPackage('a', 'package.json'),

src/monorepo-workflow-operations.test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import type { ReleaseSpecification } from './release-specification.js';
1212
import * as releasePlanModule from './release-plan.js';
1313
import type { ReleasePlan } from './release-plan.js';
1414
import * as repoModule from './repo.js';
15+
import * as yarnCommands from './yarn-commands.js';
1516
import * as workflowOperations from './workflow-operations.js';
1617

1718
jest.mock('./editor');
1819
jest.mock('./release-plan');
1920
jest.mock('./release-specification');
2021
jest.mock('./repo');
22+
jest.mock('./yarn-commands.js');
2123

2224
/**
2325
* Tests the given path to determine whether it represents a file.
@@ -65,6 +67,12 @@ function getDependencySpies() {
6567
planReleaseSpy: jest.spyOn(releasePlanModule, 'planRelease'),
6668
executeReleasePlanSpy: jest.spyOn(releasePlanModule, 'executeReleasePlan'),
6769
commitAllChangesSpy: jest.spyOn(repoModule, 'commitAllChanges'),
70+
fixConstraintsSpy: jest.spyOn(yarnCommands, 'fixConstraints'),
71+
updateYarnLockfileSpy: jest.spyOn(yarnCommands, 'updateYarnLockfile'),
72+
deduplicateDependenciesSpy: jest.spyOn(
73+
yarnCommands,
74+
'deduplicateDependencies',
75+
),
6876
};
6977
}
7078

@@ -180,6 +188,9 @@ async function setupFollowMonorepoWorkflow({
180188
planReleaseSpy,
181189
executeReleasePlanSpy,
182190
commitAllChangesSpy,
191+
fixConstraintsSpy,
192+
updateYarnLockfileSpy,
193+
deduplicateDependenciesSpy,
183194
} = getDependencySpies();
184195
const editor = buildMockEditor();
185196
const releaseSpecificationPath = path.join(
@@ -273,6 +284,9 @@ async function setupFollowMonorepoWorkflow({
273284
releasePlan,
274285
releaseVersion,
275286
releaseSpecificationPath,
287+
fixConstraintsSpy,
288+
updateYarnLockfileSpy,
289+
deduplicateDependenciesSpy,
276290
};
277291
}
278292

@@ -405,6 +419,9 @@ describe('monorepo-workflow-operations', () => {
405419
createReleaseBranchSpy,
406420
commitAllChangesSpy,
407421
projectDirectoryPath,
422+
fixConstraintsSpy,
423+
updateYarnLockfileSpy,
424+
deduplicateDependenciesSpy,
408425
} = await setupFollowMonorepoWorkflow({
409426
sandbox,
410427
releaseVersion,
@@ -445,6 +462,19 @@ describe('monorepo-workflow-operations', () => {
445462
`Update Release ${releaseVersion}`,
446463
);
447464

465+
expect(fixConstraintsSpy).toHaveBeenCalledTimes(1);
466+
expect(fixConstraintsSpy).toHaveBeenCalledWith(projectDirectoryPath);
467+
468+
expect(updateYarnLockfileSpy).toHaveBeenCalledTimes(1);
469+
expect(updateYarnLockfileSpy).toHaveBeenCalledWith(
470+
projectDirectoryPath,
471+
);
472+
473+
expect(deduplicateDependenciesSpy).toHaveBeenCalledTimes(1);
474+
expect(deduplicateDependenciesSpy).toHaveBeenCalledWith(
475+
projectDirectoryPath,
476+
);
477+
448478
// Second call of followMonorepoWorkflow
449479

450480
createReleaseBranchSpy.mockResolvedValueOnce({

src/monorepo-workflow-operations.ts

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import {
2121
validateReleaseSpecification,
2222
} from './release-specification.js';
2323
import { createReleaseBranch } from './workflow-operations.js';
24+
import {
25+
deduplicateDependencies,
26+
fixConstraints,
27+
updateYarnLockfile,
28+
} from './yarn-commands.js';
2429

2530
/**
2631
* For a monorepo, the process works like this:
@@ -147,6 +152,9 @@ export async function followMonorepoWorkflow({
147152
});
148153
await executeReleasePlan(project, releasePlan, stderr);
149154
await removeFile(releaseSpecificationPath);
155+
await fixConstraints(project.directoryPath);
156+
await updateYarnLockfile(project.directoryPath);
157+
await deduplicateDependencies(project.directoryPath);
150158
await commitAllChanges(
151159
project.directoryPath,
152160
`Update Release ${newReleaseVersion}`,

0 commit comments

Comments
 (0)