Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 99 additions & 27 deletions script/tool/lib/src/update_release_info_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,15 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand {

@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
String nextVersionString;
final Version? version = package.parsePubspec().version;
final bool isBatchRelease =
package.parseCIConfig()?.isBatchRelease ?? false;
if (isBatchRelease && (version?.isPreRelease ?? false)) {
return PackageResult.fail(<String>[
'This command does not support batch releases packages with pre-release versions.',
'Pre-release version: $version',
]);
}

_VersionIncrementType? versionChange = _versionChange;

Expand Down Expand Up @@ -144,36 +152,14 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand {
}
}

if (versionChange != null) {
final Version? updatedVersion = _updatePubspecVersion(
if (isBatchRelease) {
return _createPendingBatchChangelog(
package,
versionChange,
versionChange: versionChange,
);
if (updatedVersion == null) {
return PackageResult.fail(<String>[
'Could not determine current version.',
]);
}
nextVersionString = updatedVersion.toString();
print('${indentation}Incremented version to $nextVersionString.');
} else {
nextVersionString = 'NEXT';
}

final _ChangelogUpdateOutcome updateOutcome = _updateChangelog(
package,
nextVersionString,
);
switch (updateOutcome) {
case _ChangelogUpdateOutcome.addedSection:
print('${indentation}Added a $nextVersionString section.');
case _ChangelogUpdateOutcome.updatedSection:
print('${indentation}Updated NEXT section.');
case _ChangelogUpdateOutcome.failed:
return PackageResult.fail(<String>['Could not update CHANGELOG.md.']);
}

return PackageResult.success();
return _updatePubspecAndChangelog(package, versionChange: versionChange);
}

_ChangelogUpdateOutcome _updateChangelog(
Expand Down Expand Up @@ -314,4 +300,90 @@ class UpdateReleaseInfoCommand extends PackageLoopingCommand {
);
}
}

/// Updates the `pubspec.yaml` and `CHANGELOG.md` files directly.
///
/// This is used for standard releases, where changes will be released
/// immediately.
Future<PackageResult> _updatePubspecAndChangelog(
RepositoryPackage package, {
_VersionIncrementType? versionChange,
}) async {
String nextVersionString;
if (versionChange != null) {
final Version? updatedVersion = _updatePubspecVersion(
package,
versionChange,
);
if (updatedVersion == null) {
return PackageResult.fail(<String>[
'Could not determine current version.',
]);
}
nextVersionString = updatedVersion.toString();
print('${indentation}Incremented version to $nextVersionString.');
} else {
nextVersionString = 'NEXT';
}

final _ChangelogUpdateOutcome updateOutcome = _updateChangelog(
package,
nextVersionString,
);
switch (updateOutcome) {
case _ChangelogUpdateOutcome.addedSection:
print('${indentation}Added a $nextVersionString section.');
case _ChangelogUpdateOutcome.updatedSection:
print('${indentation}Updated NEXT section.');
case _ChangelogUpdateOutcome.failed:
return PackageResult.fail(<String>['Could not update CHANGELOG.md.']);
}

return PackageResult.success();
}

/// Creates a pending changelog entry in the package's `pending_changelogs`
/// directory.
///
/// This is used for batch releases, where changes are accumulated in
/// individual files before being merged into the main CHANGELOG.md and
/// pubspec.yaml during the release process.
Future<PackageResult> _createPendingBatchChangelog(
RepositoryPackage package, {
_VersionIncrementType? versionChange,
}) async {
final Directory pendingDirectory = package.pendingChangelogsDirectory;
if (!pendingDirectory.existsSync()) {
return PackageResult.fail(<String>[
'Could not create pending changelog entry. Pending changelog directory does not exist.',
]);
}

final VersionChange type;
switch (versionChange) {
case _VersionIncrementType.minor:
type = VersionChange.minor;
case _VersionIncrementType.bugfix:
type = VersionChange.patch;
case _VersionIncrementType.build:
throw UnimplementedError(
'Build version changes should not happen in batch mode. Please file an issue if you see this.',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me what this instruction means. If it's not supposed to happen, why should the person file an issue? What would that issue say/request?

If you mean that it's not supported, then it should say that it's not currently supported and that if that's an issue they should file an issue requesting support for build versions in batch changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't logically happen because otherwise the tool should have throw UsageError in the earlier stage. If this line is reached, that mean something is wrong with the code

);
case null:
type = VersionChange.skip;
}

final String changelogEntry = getStringArg(_changelogFlag);
final content =
'''
changelog: |
${changelogEntry.split('\n').map((line) => ' - $line').join('\n')}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't $line need to be quoted in case it has characters that have special meaning in YAML?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is text block after a |, a quick search shows the there is no need to escape yaml special character

version: ${type.name}
''';
final filename = 'change_${DateTime.now().millisecondsSinceEpoch}.yaml';
final File file = pendingDirectory.childFile(filename);
file.writeAsStringSync(content);
print('${indentation}Created pending changelog entry: $filename');
return PackageResult.success();
}
}
227 changes: 227 additions & 0 deletions script/tool/test/update_release_info_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -779,4 +779,231 @@ packages/a_package/test/plugin_test.dart
);
});
});

group('batch release', () {
test('creates pending changelog for bugfix', () async {
final RepositoryPackage package = createFakePackage(
'a_package',
packagesDir,
version: '1.0.0',
);
package.ciConfigFile.writeAsStringSync('release:\n batch: true');
package.pendingChangelogsDirectory.createSync();
const originalChangelog = '''
## 1.0.0

* Previous changes.
''';
package.changelogFile.writeAsStringSync(originalChangelog);

final List<String> output = await runCapturingPrint(runner, <String>[
'update-release-info',
'--version=bugfix',
'--changelog',
'A change.',
]);

final String version = package.parsePubspec().version?.toString() ?? '';
expect(version, '1.0.0');
expect(package.changelogFile.readAsStringSync(), originalChangelog);
expect(
output,
containsAllInOrder(<Matcher>[
contains(' Created pending changelog entry: change_'),
]),
);

final List<File> pendingFiles = package.pendingChangelogsDirectory
.listSync()
.whereType<File>()
.toList();
expect(pendingFiles, hasLength(1));
final String content = pendingFiles.first.readAsStringSync();
expect(content, contains('changelog: |\n - A change.'));
expect(content, contains('version: patch'));
});

test('creates pending changelog for minor', () async {
final RepositoryPackage package = createFakePackage(
'a_package',
packagesDir,
version: '1.0.0',
);
package.ciConfigFile.writeAsStringSync('release:\n batch: true');
package.pendingChangelogsDirectory.createSync();

final List<String> output = await runCapturingPrint(runner, <String>[
'update-release-info',
'--version=minor',
'--changelog',
'A change.',
]);

expect(
output,
containsAllInOrder(<Matcher>[
contains(' Created pending changelog entry: change_'),
]),
);

final List<File> pendingFiles = package.pendingChangelogsDirectory
.listSync()
.whereType<File>()
.toList();
expect(pendingFiles, hasLength(1));
final String content = pendingFiles.first.readAsStringSync();
expect(content, contains('version: minor'));
});

test('creates pending changelog for next (skip)', () async {
final RepositoryPackage package = createFakePackage(
'a_package',
packagesDir,
version: '1.0.0',
);
package.ciConfigFile.writeAsStringSync('release:\n batch: true');
package.pendingChangelogsDirectory.createSync();

final List<String> output = await runCapturingPrint(runner, <String>[
'update-release-info',
'--version=next',
'--changelog',
'A change.',
]);

expect(
output,
containsAllInOrder(<Matcher>[
contains(' Created pending changelog entry: change_'),
]),
);

final List<File> pendingFiles = package.pendingChangelogsDirectory
.listSync()
.whereType<File>()
.toList();
expect(pendingFiles, hasLength(1));
final String content = pendingFiles.first.readAsStringSync();
expect(content, contains('version: skip'));
});

test(
'creates pending changelog for minimal with publish-worthy changes',
() async {
final RepositoryPackage package = createFakePackage(
'a_package',
packagesDir,
version: '1.0.0',
);
package.ciConfigFile.writeAsStringSync('release:\n batch: true');
package.pendingChangelogsDirectory.createSync();
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/a_package/lib/plugin.dart
''',
),
),
];

final List<String> output = await runCapturingPrint(runner, <String>[
'update-release-info',
'--version=minimal',
'--changelog',
'A change.',
]);

expect(
output,
containsAllInOrder(<Matcher>[
contains(' Created pending changelog entry: change_'),
]),
);

final List<File> pendingFiles = package.pendingChangelogsDirectory
.listSync()
.whereType<File>()
.toList();
expect(pendingFiles, hasLength(1));
final String content = pendingFiles.first.readAsStringSync();
expect(content, contains('version: patch'));
},
);

test('skips for minimal with no changes (batch mode)', () async {
final RepositoryPackage package = createFakePackage(
'a_package',
packagesDir,
version: '1.0.0',
);
package.ciConfigFile.writeAsStringSync('release:\n batch: true');
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(
MockProcess(
stdout: '''
packages/different_package/lib/foo.dart
''',
),
),
];

final List<String> output = await runCapturingPrint(runner, <String>[
'update-release-info',
'--version=minimal',
'--changelog',
'A change.',
]);

expect(
output,
containsAllInOrder(<Matcher>[
contains('No changes to package'),
contains('Skipped 1 package'),
]),
);
// No pending changelog should be created.
expect(package.pendingChangelogsDirectory.existsSync(), isFalse);
});

test('fails for pre-release version', () async {
final RepositoryPackage package = createFakePackage(
'a_package',
packagesDir,
version: '1.0.0-dev.1',
);
package.ciConfigFile.writeAsStringSync('release:\n batch: true');
const originalChangelog = '''
## 1.0.0-dev.1

* Previous changes.
''';
package.changelogFile.writeAsStringSync(originalChangelog);

Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>[
'update-release-info',
'--version=bugfix',
'--changelog',
'A change.',
],
errorHandler: (Error e) {
commandError = e;
},
);

expect(commandError, isA<ToolExit>());
expect(
output.join('\n'),
contains(
'This command does not support batch releases packages with pre-release versions.\n'
' Pre-release version: 1.0.0-dev.1',
),
);
});
});
}
Loading