Skip to content

Commit 949c2f6

Browse files
authored
feat: Rollback on failed shared dependency resolution (#848)
<!-- Thanks for contributing! Provide a description of your changes below and a general summary in the title Please look at the following checklist to ensure that your PR can be accepted quickly: --> ## Description Fixed: #847 ## Type of Change <!--- Put an `x` in all the boxes that apply: --> - [x] ✨ `feat` -- New feature (non-breaking change which adds functionality) - [ ] 🛠️ `fix` -- Bug fix (non-breaking change which fixes an issue) - [ ] ❌ `!` -- Breaking change (fix or feature that would cause existing functionality to change) - [ ] 🧹 `refactor` -- Code refactor - [ ] ✅ `ci` -- Build configuration change - [ ] 📝 `docs` -- Documentation - [ ] 🗑️ `chore` -- Chore
1 parent a9ce186 commit 949c2f6

File tree

2 files changed

+158
-12
lines changed

2 files changed

+158
-12
lines changed

packages/melos/lib/src/commands/bootstrap.dart

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,29 @@ mixin _BootstrapMixin on _CleanMixin {
3737
..newLine();
3838

3939
final filteredPackages = workspace.filteredPackages.values;
40+
final rollbackPubspecContent = <String, String>{};
4041

4142
try {
4243
if (bootstrapCommandConfig.environment != null ||
4344
bootstrapCommandConfig.dependencies != null ||
4445
bootstrapCommandConfig.devDependencies != null) {
4546
logger.log('Updating common dependencies in workspace packages...');
4647

47-
await Stream.fromIterable(filteredPackages).parallel((package) {
48+
await Stream.fromIterable(filteredPackages)
49+
.parallel((package) async {
50+
final pubspecPath = utils.pubspecPathForDirectory(package.path);
51+
final pubspecContent = await readTextFileAsync(pubspecPath);
52+
rollbackPubspecContent[pubspecPath] = pubspecContent;
53+
4854
return _setSharedDependenciesForPackage(
4955
package,
56+
pubspecPath: pubspecPath,
57+
pubspecContent: pubspecContent,
5058
environment: bootstrapCommandConfig.environment,
5159
dependencies: bootstrapCommandConfig.dependencies,
5260
devDependencies: bootstrapCommandConfig.devDependencies,
5361
);
5462
}).drain<void>();
55-
56-
logger
57-
..child(successLabel, prefix: '> ')
58-
..newLine();
5963
}
6064

6165
logger.log(
@@ -73,6 +77,18 @@ mixin _BootstrapMixin on _CleanMixin {
7377
..child(successLabel, prefix: '> ')
7478
..newLine();
7579
} on BootstrapException catch (exception) {
80+
if (rollbackPubspecContent.isNotEmpty) {
81+
logger.log(
82+
'Dependency resolution failed, rolling back changes to '
83+
'the pubspec.yaml files...',
84+
);
85+
86+
await Stream.fromIterable(rollbackPubspecContent.entries)
87+
.parallel((entry) async {
88+
await writeTextFileAsync(entry.key, entry.value);
89+
}).drain<void>();
90+
}
91+
7692
_logBootstrapException(exception, workspace);
7793
rethrow;
7894
}
@@ -175,13 +191,13 @@ mixin _BootstrapMixin on _CleanMixin {
175191

176192
Future<void> _setSharedDependenciesForPackage(
177193
Package package, {
194+
required String pubspecPath,
195+
required String pubspecContent,
178196
required Map<String, VersionConstraint?>? environment,
179197
required Map<String, Dependency>? dependencies,
180198
required Map<String, Dependency>? devDependencies,
181199
}) async {
182-
final packagePubspecFile = utils.pubspecPathForDirectory(package.path);
183-
final packagePubspecContents = await readTextFileAsync(packagePubspecFile);
184-
final pubspecEditor = YamlEditor(packagePubspecContents);
200+
final pubspecEditor = YamlEditor(pubspecContent);
185201

186202
final updatedEnvironment = _updateEnvironment(
187203
pubspecEditor: pubspecEditor,
@@ -204,10 +220,7 @@ mixin _BootstrapMixin on _CleanMixin {
204220
);
205221

206222
if (pubspecEditor.edits.isNotEmpty) {
207-
await writeTextFileAsync(
208-
packagePubspecFile,
209-
pubspecEditor.toString(),
210-
);
223+
await writeTextFileAsync(pubspecPath, pubspecEditor.toString());
211224

212225
final message = <String>[
213226
if (updatedEnvironment) 'Updated environment',

packages/melos/test/commands/bootstrap_test.dart

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,139 @@ Generating IntelliJ IDE files...
946946
});
947947
});
948948

949+
test(
950+
'rollbacks applied shared dependencies on resolution failure',
951+
() async {
952+
final workspaceDir = await createTemporaryWorkspace(
953+
workspacePackages: ['a', 'b'],
954+
configBuilder: (path) => MelosWorkspaceConfig(
955+
name: 'Melos',
956+
packages: [
957+
createGlob('packages/**', currentDirectoryPath: path),
958+
],
959+
commands: CommandConfigs(
960+
bootstrap: BootstrapCommandConfigs(
961+
environment: {
962+
'sdk': VersionConstraint.parse('>=3.6.0 <4.0.0'),
963+
'flutter': VersionConstraint.parse('>=2.18.0 <3.0.0'),
964+
},
965+
dependencies: {
966+
'flame': HostedDependency(
967+
version: VersionConstraint.compatibleWith(
968+
// Should fail since the version is not compatible with
969+
// the flutter version.
970+
Version.parse('0.1.0'),
971+
),
972+
),
973+
},
974+
devDependencies: {
975+
'flame_lint': HostedDependency(
976+
version: VersionConstraint.compatibleWith(
977+
Version.parse('1.2.1'),
978+
),
979+
),
980+
},
981+
),
982+
),
983+
path: path,
984+
),
985+
);
986+
987+
final pkgA = await createProject(
988+
workspaceDir,
989+
Pubspec(
990+
'a',
991+
environment: {},
992+
dependencies: {
993+
'flame': HostedDependency(
994+
version:
995+
VersionConstraint.compatibleWith(Version.parse('1.23.0')),
996+
),
997+
},
998+
devDependencies: {
999+
'flame_lint': HostedDependency(
1000+
version: VersionConstraint.compatibleWith(Version.parse('1.2.0')),
1001+
),
1002+
},
1003+
),
1004+
);
1005+
1006+
final pkgB = await createProject(
1007+
workspaceDir,
1008+
Pubspec(
1009+
'b',
1010+
environment: {
1011+
'sdk': VersionConstraint.parse('>=2.12.0 <3.0.0'),
1012+
'flutter': VersionConstraint.parse('>=2.12.0 <3.0.0'),
1013+
},
1014+
dependencies: {
1015+
'flame': HostedDependency(
1016+
version:
1017+
VersionConstraint.compatibleWith(Version.parse('1.23.0')),
1018+
),
1019+
'integral_isolates': HostedDependency(
1020+
version: VersionConstraint.compatibleWith(Version.parse('0.4.1')),
1021+
),
1022+
'intl': HostedDependency(
1023+
version:
1024+
VersionConstraint.compatibleWith(Version.parse('0.17.0')),
1025+
),
1026+
'path': HostedDependency(version: VersionConstraint.any),
1027+
},
1028+
),
1029+
);
1030+
1031+
final logger = TestLogger();
1032+
final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir);
1033+
final melos = Melos(
1034+
logger: logger,
1035+
config: config,
1036+
);
1037+
1038+
final pubspecAPreBootstrap = pubspecFromYamlFile(directory: pkgA.path);
1039+
final pubspecBPreBootstrap = pubspecFromYamlFile(directory: pkgB.path);
1040+
1041+
await expectLater(
1042+
() => runMelosBootstrap(melos, logger),
1043+
throwsA(isA<BootstrapException>()),
1044+
);
1045+
1046+
final pubspecA = pubspecFromYamlFile(directory: pkgA.path);
1047+
final pubspecB = pubspecFromYamlFile(directory: pkgB.path);
1048+
1049+
expect(
1050+
pubspecAPreBootstrap.environment,
1051+
equals(defaultTestEnvironment),
1052+
);
1053+
expect(
1054+
pubspecA.environment['sdk'],
1055+
equals(pubspecAPreBootstrap.environment['sdk']),
1056+
);
1057+
expect(
1058+
pubspecA.dependencies,
1059+
equals(pubspecAPreBootstrap.dependencies),
1060+
);
1061+
expect(
1062+
pubspecA.devDependencies,
1063+
equals(pubspecAPreBootstrap.devDependencies),
1064+
);
1065+
1066+
expect(
1067+
pubspecBPreBootstrap.environment['flutter'],
1068+
equals(VersionConstraint.parse('>=2.12.0 <3.0.0')),
1069+
);
1070+
expect(
1071+
pubspecB.dependencies,
1072+
equals(pubspecBPreBootstrap.dependencies),
1073+
);
1074+
expect(
1075+
pubspecB.devDependencies,
1076+
equals(pubspecBPreBootstrap.devDependencies),
1077+
);
1078+
},
1079+
timeout: const Timeout(Duration(minutes: 20)),
1080+
);
1081+
9491082
group('melos bs --offline', () {
9501083
test('should run pub get with --offline', () async {
9511084
final workspaceDir = await createTemporaryWorkspace(

0 commit comments

Comments
 (0)