Skip to content

Commit

Permalink
feat: Rollback on failed shared dependency resolution (#848)
Browse files Browse the repository at this point in the history
<!--
  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
  • Loading branch information
spydon authored Jan 24, 2025
1 parent a9ce186 commit 949c2f6
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 12 deletions.
37 changes: 25 additions & 12 deletions packages/melos/lib/src/commands/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,29 @@ mixin _BootstrapMixin on _CleanMixin {
..newLine();

final filteredPackages = workspace.filteredPackages.values;
final rollbackPubspecContent = <String, String>{};

try {
if (bootstrapCommandConfig.environment != null ||
bootstrapCommandConfig.dependencies != null ||
bootstrapCommandConfig.devDependencies != null) {
logger.log('Updating common dependencies in workspace packages...');

await Stream.fromIterable(filteredPackages).parallel((package) {
await Stream.fromIterable(filteredPackages)
.parallel((package) async {
final pubspecPath = utils.pubspecPathForDirectory(package.path);
final pubspecContent = await readTextFileAsync(pubspecPath);
rollbackPubspecContent[pubspecPath] = pubspecContent;

return _setSharedDependenciesForPackage(
package,
pubspecPath: pubspecPath,
pubspecContent: pubspecContent,
environment: bootstrapCommandConfig.environment,
dependencies: bootstrapCommandConfig.dependencies,
devDependencies: bootstrapCommandConfig.devDependencies,
);
}).drain<void>();

logger
..child(successLabel, prefix: '> ')
..newLine();
}

logger.log(
Expand All @@ -73,6 +77,18 @@ mixin _BootstrapMixin on _CleanMixin {
..child(successLabel, prefix: '> ')
..newLine();
} on BootstrapException catch (exception) {
if (rollbackPubspecContent.isNotEmpty) {
logger.log(
'Dependency resolution failed, rolling back changes to '
'the pubspec.yaml files...',
);

await Stream.fromIterable(rollbackPubspecContent.entries)
.parallel((entry) async {
await writeTextFileAsync(entry.key, entry.value);
}).drain<void>();
}

_logBootstrapException(exception, workspace);
rethrow;
}
Expand Down Expand Up @@ -175,13 +191,13 @@ mixin _BootstrapMixin on _CleanMixin {

Future<void> _setSharedDependenciesForPackage(
Package package, {
required String pubspecPath,
required String pubspecContent,
required Map<String, VersionConstraint?>? environment,
required Map<String, Dependency>? dependencies,
required Map<String, Dependency>? devDependencies,
}) async {
final packagePubspecFile = utils.pubspecPathForDirectory(package.path);
final packagePubspecContents = await readTextFileAsync(packagePubspecFile);
final pubspecEditor = YamlEditor(packagePubspecContents);
final pubspecEditor = YamlEditor(pubspecContent);

final updatedEnvironment = _updateEnvironment(
pubspecEditor: pubspecEditor,
Expand All @@ -204,10 +220,7 @@ mixin _BootstrapMixin on _CleanMixin {
);

if (pubspecEditor.edits.isNotEmpty) {
await writeTextFileAsync(
packagePubspecFile,
pubspecEditor.toString(),
);
await writeTextFileAsync(pubspecPath, pubspecEditor.toString());

final message = <String>[
if (updatedEnvironment) 'Updated environment',
Expand Down
133 changes: 133 additions & 0 deletions packages/melos/test/commands/bootstrap_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,139 @@ Generating IntelliJ IDE files...
});
});

test(
'rollbacks applied shared dependencies on resolution failure',
() async {
final workspaceDir = await createTemporaryWorkspace(
workspacePackages: ['a', 'b'],
configBuilder: (path) => MelosWorkspaceConfig(
name: 'Melos',
packages: [
createGlob('packages/**', currentDirectoryPath: path),
],
commands: CommandConfigs(
bootstrap: BootstrapCommandConfigs(
environment: {
'sdk': VersionConstraint.parse('>=3.6.0 <4.0.0'),
'flutter': VersionConstraint.parse('>=2.18.0 <3.0.0'),
},
dependencies: {
'flame': HostedDependency(
version: VersionConstraint.compatibleWith(
// Should fail since the version is not compatible with
// the flutter version.
Version.parse('0.1.0'),
),
),
},
devDependencies: {
'flame_lint': HostedDependency(
version: VersionConstraint.compatibleWith(
Version.parse('1.2.1'),
),
),
},
),
),
path: path,
),
);

final pkgA = await createProject(
workspaceDir,
Pubspec(
'a',
environment: {},
dependencies: {
'flame': HostedDependency(
version:
VersionConstraint.compatibleWith(Version.parse('1.23.0')),
),
},
devDependencies: {
'flame_lint': HostedDependency(
version: VersionConstraint.compatibleWith(Version.parse('1.2.0')),
),
},
),
);

final pkgB = await createProject(
workspaceDir,
Pubspec(
'b',
environment: {
'sdk': VersionConstraint.parse('>=2.12.0 <3.0.0'),
'flutter': VersionConstraint.parse('>=2.12.0 <3.0.0'),
},
dependencies: {
'flame': HostedDependency(
version:
VersionConstraint.compatibleWith(Version.parse('1.23.0')),
),
'integral_isolates': HostedDependency(
version: VersionConstraint.compatibleWith(Version.parse('0.4.1')),
),
'intl': HostedDependency(
version:
VersionConstraint.compatibleWith(Version.parse('0.17.0')),
),
'path': HostedDependency(version: VersionConstraint.any),
},
),
);

final logger = TestLogger();
final config = await MelosWorkspaceConfig.fromWorkspaceRoot(workspaceDir);
final melos = Melos(
logger: logger,
config: config,
);

final pubspecAPreBootstrap = pubspecFromYamlFile(directory: pkgA.path);
final pubspecBPreBootstrap = pubspecFromYamlFile(directory: pkgB.path);

await expectLater(
() => runMelosBootstrap(melos, logger),
throwsA(isA<BootstrapException>()),
);

final pubspecA = pubspecFromYamlFile(directory: pkgA.path);
final pubspecB = pubspecFromYamlFile(directory: pkgB.path);

expect(
pubspecAPreBootstrap.environment,
equals(defaultTestEnvironment),
);
expect(
pubspecA.environment['sdk'],
equals(pubspecAPreBootstrap.environment['sdk']),
);
expect(
pubspecA.dependencies,
equals(pubspecAPreBootstrap.dependencies),
);
expect(
pubspecA.devDependencies,
equals(pubspecAPreBootstrap.devDependencies),
);

expect(
pubspecBPreBootstrap.environment['flutter'],
equals(VersionConstraint.parse('>=2.12.0 <3.0.0')),
);
expect(
pubspecB.dependencies,
equals(pubspecBPreBootstrap.dependencies),
);
expect(
pubspecB.devDependencies,
equals(pubspecBPreBootstrap.devDependencies),
);
},
timeout: const Timeout(Duration(minutes: 20)),
);

group('melos bs --offline', () {
test('should run pub get with --offline', () async {
final workspaceDir = await createTemporaryWorkspace(
Expand Down

0 comments on commit 949c2f6

Please sign in to comment.