From 81c3f1568cac6edad0f7f22cf796ea454ef80e27 Mon Sep 17 00:00:00 2001 From: Wayne Duran Date: Mon, 3 May 2021 13:56:12 +0800 Subject: [PATCH] feat: prepare_release command --- README.md | 84 ++++++-- lib/changelog_command.dart | 48 +++-- lib/current_version_command.dart | 11 +- lib/next_version_command.dart | 29 ++- lib/prepare_release_command.dart | 88 ++++++++ lib/project.dart | 39 ++++ lib/release_tools_command.dart | 16 +- lib/release_tools_runner.dart | 73 ++++--- lib/remote_tag_id_command.dart | 12 +- lib/update_version_command.dart | 22 +- lib/update_year_command.dart | 11 +- test/helpers.dart | 8 + test/prepare_release_command_test.dart | 266 +++++++++++++++++++++++++ test/runner_setup.dart | 1 + 14 files changed, 595 insertions(+), 113 deletions(-) create mode 100644 lib/prepare_release_command.dart create mode 100644 lib/project.dart create mode 100644 test/helpers.dart create mode 100644 test/prepare_release_command_test.dart diff --git a/README.md b/README.md index 4bb858e..a96d460 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ A collection of scripts to help with creating releases for publishing libraries - `release_tools next_version` - Get the next version based on commits. - `release_tools should_release` - Check if we can create a release based on commits that follow the [Conventional Commit](https://www.conventionalcommits.org/) spec. - `release_tools changelog` - Update changelog based on commits that follow the Conventional Commit spec. -- `release_tools update_year` - For syncing years on license files -- `release_tools remote_tag_id` - Get the commit id of a remote tag -- `release_tools current_version` - Get the current version of this package +- `release_tools update_year` - For syncing years on license files. +- `release_tools remote_tag_id` - Get the commit id of a remote tag. +- `release_tools current_version` - Get the current version of this package. +- `release_tools prepare_release` - Complete release prep logic using the tools previously mentioned. ## Notes Before Using @@ -23,16 +24,19 @@ To be effective, `release_tools` makes a few assumptions about a project: - Commits follow the [Conventional Commit](https://www.conventionalcommits.org/) spec - Versions are tagged +If your project needs are typical, you probably only need `prepare_release`. However, if you need more fine-grained control, use the other scripts as you see fit. ## Installation -I recommend installing `release_tools` globally so that it won't interfere with your project's own dependecies: +I recommend installing `release_tools` globally so that it won't interfere with your project's own dependecies. Constrain it to a specific version to limit supply-chain exploits. ```sh -$ pub global activate release_tools +$ pub global activate release_tools 0.2.5 ``` -## update_version +## Scripts + +### update_version The following command will update the version on `pubspec.yaml` to version 1.0.1 @@ -40,7 +44,7 @@ The following command will update the version on `pubspec.yaml` to version 1.0.1 $ release_tools update_version 1.0.1 ``` -## next_version +### next_version If you leave out the version to increment from, it will attempt to obtain the version from pubspec.yaml @@ -76,7 +80,7 @@ $ release_tools next_version --from=abcde1234 1.0.1 ...where `--from` should point to a commit id. -## should_release +### should_release The following will print 'yes' to stdout if there are releasable commits, or 'no' if there are none. @@ -87,9 +91,9 @@ $ release_tools should_release --from=abcde1234 no ``` -## changelog +### changelog -The following will update changelog based on the commit logs that follow the Conventional Commit spec. +The following will update the changelog based on the commit logs that follow the Conventional Commit spec. ```sh $ release_tools changelog 2.0.1 @@ -117,7 +121,7 @@ A sample changelog would be the following: - null-safety ([#6](issues/6)) ([43cf9b7](commit/43cf9b7)) ``` -## update_year +### update_year A simple tool for updating the year on LICENSE files. Note that the logic is really simple. It simply updates the first 4-digit number to the current year which may or may not be enough for your needs. @@ -126,7 +130,7 @@ $ release_tools update_year $ release_tools update_year --license=MY_LICENSE_FILE ``` -## remote_tag_id +### remote_tag_id Use this to retrieve the commit id of a tag on the git repository's remote. @@ -147,11 +151,63 @@ You can specify the remote repository instead of the default 'origin' if needed: $ release_tools remote_tag_id --remote=source 0.2.2 ``` -## current_version +### current_version Use this if you need to retrieve the current version on pubspec.yaml ```sh $ release_tools current_version # 1.0.2 -``` \ No newline at end of file +``` + +### prepare_release + +Complete release preparation logic with the following steps: + +1. Get the current version +2. Get the commits from the last tag or, if a tag is not available for the last release, from the beginning of commit history +3. Check if a release is appropriate and if so... +4. Update version on pubspec +5. Create summary changelog from the commits + +```sh +$ release_tools prepare_release +``` + +If there are no releasable commits, it will print the following: + +```sh +There are no releasable commits +``` + +Otherwise, it will print something like the following: + +```sh +Version bumped to: 0.2.5 + +SUMMARY: + +# 0.2.5 (2021-05-03) + +## Bug Fixes + +- **changelog:** performance section in changelogs ([063e07d](commit/063e07d)) + +## Features + +- prepare_release command ([877d63e](commit/877d63e)) +``` + + +If you need a summary of the result of the script run, you can pass `-w` to write some summary files like in the following: + +```sh +$ release_tools prepare_release -w +``` + +This will create two files, `VERSION.txt` and `RELEASE_SUMMARY.txt` which will contain just the version for release and the summary of changes, respectively. + +## Similar Tools + +- [pub_release](https://pub.dev/packages/pub_release) - A much more mature release tool. I wanted to use this tool on my projects but found I didn't need a lot of its features. Still awesome! +- [melos](https://pub.dev/packages/melos) - This one is geared towards monorepos. \ No newline at end of file diff --git a/lib/changelog_command.dart b/lib/changelog_command.dart index e8ae5dc..6850ab3 100644 --- a/lib/changelog_command.dart +++ b/lib/changelog_command.dart @@ -1,15 +1,13 @@ -import 'package:args/args.dart'; import 'package:conventional/conventional.dart'; -import 'package:file/file.dart'; import 'git_exec.dart'; import 'help_footer.dart'; import 'printer.dart'; +import 'project.dart'; import 'release_tools_command.dart'; class ChangelogCommand extends ReleaseToolsCommand with GitCommand { - final FileSystem fs; - final String workingDir; + final Project project; final DateTime now; @override @@ -37,9 +35,8 @@ release_tools changelog --from=3682c64 2.0.1 '''); ChangelogCommand({ - required this.fs, + required this.project, required this.git, - required this.workingDir, required this.printer, required this.now, }) { @@ -48,22 +45,29 @@ release_tools changelog --from=3682c64 2.0.1 @override Future run() async { - if (argResults is ArgResults) { - final args = argResults!; - if (args.rest.isEmpty) { - throw ArgumentError('Please provide a version to mark the changes.'); - } - final version = args.rest.first; - final commits = await getCommits(); - final summary = await writeChangelogToFile( - commits: commits, - version: version, - now: now, - file: fs.directory(workingDir).childFile('CHANGELOG.md'), - ); - if (summary is ChangeSummary) { - printer.println(summary.toMarkdown()); - } + final args = ensureArgResults(); + if (args.rest.isEmpty) { + throw ArgumentError('Please provide a version to mark the changes.'); } + final version = args.rest.first; + final summary = await writeChangelog( + commits: await getCommits(), + version: version, + ); + if (summary is ChangeSummary) { + printer.println(summary.toMarkdown()); + } + } + + Future writeChangelog({ + required List commits, + required String version, + }) { + return writeChangelogToFile( + commits: commits, + version: version, + now: now, + file: project.changelog(), + ); } } diff --git a/lib/current_version_command.dart b/lib/current_version_command.dart index b407c23..698a320 100644 --- a/lib/current_version_command.dart +++ b/lib/current_version_command.dart @@ -1,15 +1,11 @@ -import 'package:file/file.dart'; - import 'help_footer.dart'; import 'printer.dart'; +import 'project.dart'; import 'release_tools_command.dart'; class CurrentVersionCommand extends ReleaseToolsCommand with VersionCommand { @override - final FileSystem fs; - - @override - final String workingDir; + final Project project; @override final Printer printer; @@ -32,8 +28,7 @@ release_tools current_version '''); CurrentVersionCommand({ - required this.fs, - required this.workingDir, + required this.project, required this.printer, }); diff --git a/lib/next_version_command.dart b/lib/next_version_command.dart index 9a11110..5e5129e 100644 --- a/lib/next_version_command.dart +++ b/lib/next_version_command.dart @@ -1,22 +1,19 @@ import 'package:conventional/conventional.dart'; -import 'package:file/file.dart'; import 'package:pub_semver/pub_semver.dart'; import 'git_exec.dart'; import 'help_footer.dart'; import 'printer.dart'; +import 'project.dart'; import 'release_tools_command.dart'; class NextVersionCommand extends ReleaseToolsCommand with GitCommand, VersionCommand { @override - final FileSystem fs; - - @override - final String workingDir; + final GitExec git; @override - final GitExec git; + final Project project; @override final Printer printer; @@ -43,8 +40,7 @@ release_tools next_version --from=3682c64 2.0.1 NextVersionCommand({ required this.git, - required this.fs, - required this.workingDir, + required this.project, required this.printer, }) { gitFromOption(); @@ -53,8 +49,19 @@ release_tools next_version --from=3682c64 2.0.1 @override Future run() async { final currentVersion = await getVersionFromArgsOrPubspec(); - final newVersion = - nextVersion(Version.parse(currentVersion), await getCommits()); - printer.println(newVersion.toString()); + printer.println( + await getNextVersionFromString( + await getCommits(), + currentVersion, + ), + ); + } + + Future getNextVersionFromString( + List commits, + String currentVersion, + ) async { + final newVersion = nextVersion(Version.parse(currentVersion), commits); + return newVersion.toString(); } } diff --git a/lib/prepare_release_command.dart b/lib/prepare_release_command.dart new file mode 100644 index 0000000..b3b0543 --- /dev/null +++ b/lib/prepare_release_command.dart @@ -0,0 +1,88 @@ +import 'package:conventional/conventional.dart'; +import 'package:release_tools/changelog_command.dart'; +import 'package:release_tools/printer.dart'; +import 'package:release_tools/remote_tag_id_command.dart'; +import 'package:release_tools/update_version_command.dart'; + +import 'next_version_command.dart'; +import 'release_tools_command.dart'; + +class PrepareReleaseCommand extends ReleaseToolsCommand { + @override + final String description = 'Prepares the project for release.'; + + @override + final String name = 'prepare_release'; + + @override + final Printer printer; + + final NextVersionCommand nextVersionCommand; + final RemoteTagIdCommand remoteTagIdCommand; + final ChangelogCommand changelogCommand; + final UpdateVersionCommand updateVersionCommand; + + PrepareReleaseCommand({ + required this.printer, + required this.nextVersionCommand, + required this.remoteTagIdCommand, + required this.changelogCommand, + required this.updateVersionCommand, + }) { + argParser.addFlag( + 'writeSummary', + abbr: 'w', + help: + 'Writes release information to files (VERSION.txt and RELEASE_SUMMARY.txt)', + ); + } + + @override + Future run() async { + final currentVersion = await nextVersionCommand.getVersionFromPubspec(); + final tagId = await remoteTagIdCommand.getRemoteTagId( + currentVersion, + 'origin', + ); + final commits = + await changelogCommand.getCommitsFromId(tagId.isEmpty ? null : tagId); + final nextVersion = await nextVersionCommand.getNextVersionFromString( + commits, + currentVersion, + ); + if (nextVersion != currentVersion) { + await _createRelease( + commits: commits, + nextVersion: nextVersion, + ); + } else { + printer.println('There are no releasable commits'); + } + } + + Future _createRelease({ + required List commits, + required String nextVersion, + }) async { + final args = ensureArgResults(); + final summary = await changelogCommand.writeChangelog( + commits: commits, + version: nextVersion, + ); + + await updateVersionCommand.updateVersionOnPubspecFile(nextVersion); + if (summary is ChangeSummary) { + if (args['writeSummary'] as bool) { + final project = changelogCommand.project; + final versionFile = project.getFile('VERSION.txt'); + await versionFile.writeAsString(nextVersion); + + final summaryFile = project.getFile('RELEASE_SUMMARY.txt'); + await summaryFile.writeAsString(summary.toMarkdown()); + } + + printer.printSuccess('Version bumped to: $nextVersion\n'); + printer.println('SUMMARY:\n\n${summary.toMarkdown()}'); + } + } +} diff --git a/lib/project.dart b/lib/project.dart new file mode 100644 index 0000000..4b3feb0 --- /dev/null +++ b/lib/project.dart @@ -0,0 +1,39 @@ +import 'package:file/file.dart'; + +class Project { + final FileSystem fs; + final String workingDir; + + Project({ + required this.fs, + required this.workingDir, + }); + + File getFile(String fileName) { + return fs.directory(workingDir).childFile(fileName); + } + + File changelog() { + return getFile('CHANGELOG.md'); + } + + File pubspec() { + return getFile('pubspec.yaml'); + } + + Future pubspecExists() { + return pubspec().exists(); + } + + Future writeToChangelog(String contents) async { + await changelog().writeAsString(contents); + } + + Future writeToPubspec(String contents) async { + await pubspec().writeAsString(contents); + } + + Future getPubspecContents() { + return pubspec().readAsString(); + } +} diff --git a/lib/release_tools_command.dart b/lib/release_tools_command.dart index 8a9fcab..3a4d884 100644 --- a/lib/release_tools_command.dart +++ b/lib/release_tools_command.dart @@ -1,11 +1,11 @@ import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:conventional/conventional.dart'; -import 'package:file/file.dart'; import 'package:release_tools/printer.dart'; import 'package:yaml/yaml.dart'; import 'git_exec.dart'; +import 'project.dart'; abstract class ReleaseToolsCommand extends Command { Printer get printer; @@ -22,8 +22,7 @@ abstract class ReleaseToolsCommand extends Command { } mixin VersionCommand on ReleaseToolsCommand { - FileSystem get fs; - String get workingDir; + Project get project; Future getVersionFromArgsOrPubspec() async { final args = ensureArgResults(); @@ -41,13 +40,12 @@ mixin VersionCommand on ReleaseToolsCommand { } Future getVersionFromPubspec() async { - final file = fs.directory(workingDir).childFile('pubspec.yaml'); - if (!await file.exists()) { + if (!await project.pubspecExists()) { throw NoPubspecFileFound( 'No pubspec.yaml found.', ); } - final contents = await file.readAsString(); + final contents = await project.getPubspecContents(); Map yaml; try { yaml = await loadYaml(contents) as Map; @@ -80,6 +78,10 @@ mixin GitCommand on ReleaseToolsCommand { Future> getCommits() async { final args = ensureArgResults(); final from = args['from'] is String ? args['from'] as String : null; - return git.commits(from: from); + return getCommitsFromId(from); + } + + Future> getCommitsFromId(String? id) { + return git.commits(from: id); } } diff --git a/lib/release_tools_runner.dart b/lib/release_tools_runner.dart index 4ebabe4..986159f 100644 --- a/lib/release_tools_runner.dart +++ b/lib/release_tools_runner.dart @@ -1,14 +1,17 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; -import 'package:release_tools/changelog_command.dart'; -import 'package:release_tools/remote_tag_id_command.dart'; -import 'package:release_tools/should_release_command.dart'; -import 'package:release_tools/update_year_command.dart'; +import 'package:release_tools/prepare_release_command.dart'; +import 'package:release_tools/project.dart'; + +import 'changelog_command.dart'; import 'current_version_command.dart'; import 'git_exec.dart'; import 'next_version_command.dart'; import 'printer.dart'; +import 'remote_tag_id_command.dart'; +import 'should_release_command.dart'; import 'update_version_command.dart'; +import 'update_year_command.dart'; class ReleaseToolsRunner { final GitExec git; @@ -26,42 +29,50 @@ class ReleaseToolsRunner { }); Future run(List arguments) async { + final project = Project(fs: fs, workingDir: workingDir); + final nextVersionCommand = NextVersionCommand( + project: project, + printer: printer, + git: git, + ); + final remoteTagIdCommand = RemoteTagIdCommand( + printer: printer, + git: git, + ); + final changelogCommand = ChangelogCommand( + printer: printer, + project: project, + git: git, + now: now, + ); + final updateVersionCommand = UpdateVersionCommand( + printer: printer, + project: project, + ); + final cmd = CommandRunner("release_tools", "A collection of tools to help with creating releases and publishing libraries.") - ..addCommand(UpdateVersionCommand( - fs: fs, - printer: printer, - workingDir: workingDir, - )) - ..addCommand(ChangelogCommand( - fs: fs, - printer: printer, - workingDir: workingDir, - git: git, - now: now, - )) - ..addCommand(NextVersionCommand( - fs: fs, - printer: printer, - workingDir: workingDir, - git: git, - )) + ..addCommand(nextVersionCommand) + ..addCommand(changelogCommand) + ..addCommand(updateVersionCommand) ..addCommand(UpdateYearCommand( - fs: fs, printer: printer, - workingDir: workingDir, + project: project, now: now, )) - ..addCommand(RemoteTagIdCommand( - printer: printer, - git: git, - )) + ..addCommand(remoteTagIdCommand) ..addCommand(CurrentVersionCommand( - fs: fs, printer: printer, - workingDir: workingDir, + project: project, )) - ..addCommand(ShouldReleaseCommand(printer: printer, git: git)); + ..addCommand(ShouldReleaseCommand(printer: printer, git: git)) + ..addCommand(PrepareReleaseCommand( + printer: printer, + nextVersionCommand: nextVersionCommand, + remoteTagIdCommand: remoteTagIdCommand, + changelogCommand: changelogCommand, + updateVersionCommand: updateVersionCommand, + )); await cmd.run(arguments); } diff --git a/lib/remote_tag_id_command.dart b/lib/remote_tag_id_command.dart index a7c138b..de3f5e4 100644 --- a/lib/remote_tag_id_command.dart +++ b/lib/remote_tag_id_command.dart @@ -44,11 +44,15 @@ release_tools remote_tag_id 3.2.8 --remote=source Future run() async { final args = ensureArgResults(); final version = args.rest.first; + final commitId = await getRemoteTagId(version, args['remote'] as String); + printer.printSuccess(commitId); + } + + Future getRemoteTagId(String tag, String remote) async { final result = await git.lsRemoteTag( - tag: version, - remote: args['remote'] as String, + tag: tag, + remote: remote, ); - final commitId = result.split(RegExp(r'\s+')).first; - printer.printSuccess(commitId); + return result.split(RegExp(r'\s+')).first; } } diff --git a/lib/update_version_command.dart b/lib/update_version_command.dart index 12ab841..38faf06 100644 --- a/lib/update_version_command.dart +++ b/lib/update_version_command.dart @@ -4,11 +4,11 @@ import 'package:args/args.dart'; import 'help_footer.dart'; import 'printer.dart'; +import 'project.dart'; import 'release_tools_command.dart'; class UpdateVersionCommand extends ReleaseToolsCommand { - final FileSystem fs; - final String workingDir; + final Project project; @override final Printer printer; @@ -29,21 +29,24 @@ class UpdateVersionCommand extends ReleaseToolsCommand { final usageFooter = helpFooter('release_tools update_version 2.0.1'); UpdateVersionCommand({ - required this.fs, - required this.workingDir, + required this.project, required this.printer, }) : super(); @override Future run() async { if (argResults is ArgResults) { - final pubspecFile = await _getPubspecFile(); final newVersion = _getNewVersion(argResults!.rest); - await _updateVersionOnFile(pubspecFile, newVersion); + await updateVersionOnPubspecFile(newVersion); printer.printSuccess('Updated version to "$newVersion".'); } } + Future updateVersionOnPubspecFile(String newVersion) async { + final pubspecFile = await _getPubspecFile(); + await _updateVersionOnFile(pubspecFile, newVersion); + } + String _getNewVersion(List arguments) { if (arguments.isEmpty) { throw ArgumentError( @@ -53,11 +56,10 @@ class UpdateVersionCommand extends ReleaseToolsCommand { } Future _getPubspecFile() async { - final pubspecFile = fs.directory(workingDir).childFile('pubspec.yaml'); - if (!await pubspecFile.exists()) { - throw MissingPubspecError(workingDir); + if (!await project.pubspecExists()) { + throw MissingPubspecError(project.workingDir); } - return pubspecFile; + return project.pubspec(); } Future _updateVersionOnFile(File pubspecFile, String newVersion) async { diff --git a/lib/update_year_command.dart b/lib/update_year_command.dart index 165e035..3a1dde7 100644 --- a/lib/update_year_command.dart +++ b/lib/update_year_command.dart @@ -2,14 +2,14 @@ import 'package:file/file.dart'; import 'help_footer.dart'; import 'printer.dart'; +import 'project.dart'; import 'release_tools_command.dart'; final _yearRegexp = RegExp(r'\d{4}'); const _defaultLicenseFiles = ['LICENSE', 'LICENSE.txt']; class UpdateYearCommand extends ReleaseToolsCommand { - final FileSystem fs; - final String workingDir; + final Project project; final DateTime now; @override @@ -34,8 +34,7 @@ release_tools update_year --license=MY_LICENSE_FILE '''); UpdateYearCommand({ - required this.fs, - required this.workingDir, + required this.project, required this.printer, required this.now, }) { @@ -79,7 +78,7 @@ release_tools update_year --license=MY_LICENSE_FILE late File file; bool found = false; for (final fileName in _defaultLicenseFiles) { - file = fs.directory(workingDir).childFile(fileName); + file = project.getFile(fileName); if (await file.exists()) { found = true; break; @@ -94,7 +93,7 @@ release_tools update_year --license=MY_LICENSE_FILE } Future _findLicenseFile(String name) async { - final file = fs.directory(workingDir).childFile(name); + final file = project.getFile(name); if (!await file.exists()) { throw StateError('Unable to find a license file "$name".'); } diff --git a/test/helpers.dart b/test/helpers.dart new file mode 100644 index 0000000..2fe3887 --- /dev/null +++ b/test/helpers.dart @@ -0,0 +1,8 @@ +import 'package:conventional/conventional.dart'; + +List parseCommits(List commitList) { + if (commitList.isEmpty) { + return []; + } + return Commit.parseCommits(commitList.join('\r\n\r\n')); +} diff --git a/test/prepare_release_command_test.dart b/test/prepare_release_command_test.dart new file mode 100644 index 0000000..e83b089 --- /dev/null +++ b/test/prepare_release_command_test.dart @@ -0,0 +1,266 @@ +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:release_tools/git_exec.dart'; +import 'package:release_tools/prepare_release_command.dart'; +import 'package:release_tools/printer.dart'; +import 'package:release_tools/release_tools_runner.dart'; +import 'package:test/test.dart'; + +import 'fixtures.dart'; +import 'helpers.dart'; +import 'runner_setup.dart'; + +void main() { + group(PrepareReleaseCommand, () { + final now = DateTime.parse('2021-02-10'); + runnerSetup((getContext) { + late ReleaseToolsRunner runner; + late MemoryFileSystem fs; + late String workingDir; + late StubPrinter printer; + late StubGitExec git; + const command = 'prepare_release'; + const pubspecContents = ''' +name: foo_bar +description: A sample pubspec file._file +version: 1.0.0 + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + equatable: ^2.0.0 + +dev_dependencies: + test: ^1.14.4 +'''; + const originalChangelogContent = ''' +# 1.0.0 (2021-02-09) + +## Features + +- eat healthy ([#3](issues/3)) ([cf60800](commit/cf60800)) +'''; + + setUp(() async { + final context = getContext(now: now); + runner = context.runner; + fs = context.fs; + git = context.git; + workingDir = context.workingDir; + printer = context.printer; + + // prepare changelog + final file = fs.directory(workingDir).childFile('CHANGELOG.md'); + await file.writeAsString(originalChangelogContent); + }); + + File getFile(String fileName) { + return fs.directory(workingDir).childFile(fileName); + } + + Future getChangelogFileContents() async { + final file = getFile('CHANGELOG.md'); + return file.readAsString(); + } + + Future createPubspec(String contents) async { + final pubspecFile = getFile('pubspec.yaml'); + await pubspecFile.writeAsString(contents); + } + + Future readPubspec() { + final pubspecFile = getFile('pubspec.yaml'); + return pubspecFile.readAsString(); + } + + group('errors', () { + test('throws StateError when there is no pubspec.yaml is available', + () { + expect(() => runner.run([command]), throwsStateError); + }); + + test( + 'throws StateError when version key is not present on pubspec.yaml', + () async { + await createPubspec('foo: bar'); + expect(() => runner.run([command]), throwsStateError); + }); + + test('throws StateError when pubspec.yaml is not a valid yaml file', + () async { + await createPubspec(' 113241#'); + expect(() => runner.run([command]), throwsStateError); + }); + }); + + group('happy paths', () { + const expectedSummary = ''' +# 2.0.0 (2021-02-10) + +## Bug Fixes + +- plug holes ([cf60800](commit/cf60800)) + +## Features + +- it jumps ([925fcd3](commit/925fcd3)) + +## BREAKING CHANGES + +- null-safety ([43cf9b7](commit/43cf9b7)) +'''; + setUp(() => createPubspec(pubspecContents)); + + group('when there is a tag from last release', () { + setUp(() async { + git.lsRemoteTagResponse = ''' +3ed81541a61c7502b658c027f6d5ec87c129c1a9 refs/tags/1.0.0'''; + }); + + void logicTest() { + test('it gets remote tag for the version', () async { + expect(git.lsRemoteTagArgs['tag'], equals('1.0.0')); + }); + + test('it logs from that tag commit id', () async { + expect( + git.commitsFrom, + equals('3ed81541a61c7502b658c027f6d5ec87c129c1a9'), + ); + }); + } + + void successTest() { + logicTest(); + + test('it updates pubspec version', () async { + expect(await readPubspec(), contains('version: 2.0.0')); + }); + + test('it updates changelog', () async { + expect( + await getChangelogFileContents(), + contains(expectedSummary), + ); + }); + + test('it prints the version', () { + expect( + printer.prints.join('\n'), + contains('Version bumped to: 2.0.0'), + ); + }); + + test('it prints the summary', () { + expect( + printer.prints.join('\n'), + contains('SUMMARY:\n\n$expectedSummary'), + ); + }); + } + + group('with no releasable commits', () { + setUp(() { + git.commitsResponse = parseCommits([chore]); + }); + + group('by default', () { + late String changeLogContents; + late String pubspecContents; + + setUp(() async { + changeLogContents = + await getFile('CHANGELOG.md').readAsString(); + pubspecContents = await getFile('pubspec.yaml').readAsString(); + await runner.run([command]); + }); + + logicTest(); + + test('it does not update the changelog file', () async { + expect( + await getFile('CHANGELOG.md').readAsString(), + changeLogContents, + ); + }); + + test('it does not update the pubspec file', () async { + expect( + await getFile('pubspec.yaml').readAsString(), + pubspecContents, + ); + }); + + test('it does not print any version text', () { + expect( + printer.prints.join('\n'), + isNot(contains('Version bumped to')), + ); + }); + + test('it prints that there are no releasable commits', () { + expect( + printer.prints.join('\n'), + contains('There are no releasable commits'), + ); + }); + }); + }); + + group('with releasable commits', () { + setUp(() { + git.commitsResponse = parseCommits([feat, chore, breaking, fix]); + }); + + group('by default', () { + setUp(() async { + await runner.run([command]); + }); + + successTest(); + test('it does not write a change summary file', () async { + final summaryFile = getFile('RELEASE_SUMMARY.txt'); + expect(await summaryFile.exists(), false); + }); + + test('it does not write a version file', () async { + final versionFile = getFile('VERSION.txt'); + expect(await versionFile.exists(), false); + }); + }); + + group('when passed with -w', () { + setUp(() async { + await runner.run([command, '-w']); + }); + + successTest(); + test('it writes a change summary file', () async { + final summaryFile = getFile('RELEASE_SUMMARY.txt'); + expect( + await summaryFile.readAsString(), equals(expectedSummary)); + }); + + test('it does not write a version file', () async { + final versionFile = getFile('VERSION.txt'); + expect(await versionFile.readAsString(), equals('2.0.0')); + }); + }); + }); + }); + + group('when there is no tag/release from before', () { + setUp(() async { + git.lsRemoteTagResponse = ''; + await runner.run([command]); + }); + + test('it gets commits from earliest commit', () { + expect(git.commitsFrom, equals(null)); + }); + }); + }); + }); + }); +} diff --git a/test/runner_setup.dart b/test/runner_setup.dart index 0d9d962..9d7072c 100644 --- a/test/runner_setup.dart +++ b/test/runner_setup.dart @@ -23,6 +23,7 @@ typedef GetContext = RunnerTestContext Function({DateTime? now}); typedef SetupCallback = void Function(GetContext); +/// Setup the test context for running tests with the command void runnerSetup(SetupCallback callback) { RunnerTestContext ctxCaller({DateTime? now}) { final fs = MemoryFileSystem();