From f0eca8dbd06831256733d5bf38cfe171ac1ca30d Mon Sep 17 00:00:00 2001 From: Andrei Lesnitsky Date: Wed, 30 Nov 2022 11:19:07 +0100 Subject: [PATCH] feat: implement scoped changelogs (#421) Co-authored-by: Gabriel Terwesten --- docs/configuration/overview.mdx | 47 ++++- packages/melos/lib/src/commands/runner.dart | 6 +- packages/melos/lib/src/commands/version.dart | 61 +++++-- ...hangelog.dart => aggregate_changelog.dart} | 28 +-- packages/melos/lib/src/common/utils.dart | 9 +- packages/melos/lib/src/common/versioning.dart | 2 +- packages/melos/lib/src/package.dart | 107 ++++++++++++ packages/melos/lib/src/scripts.dart | 119 +------------ packages/melos/lib/src/workspace_configs.dart | 163 +++++++++++++++--- .../melos/test/workspace_config_test.dart | 102 ++++++++--- 10 files changed, 440 insertions(+), 204 deletions(-) rename packages/melos/lib/src/common/{workspace_changelog.dart => aggregate_changelog.dart} (91%) diff --git a/docs/configuration/overview.mdx b/docs/configuration/overview.mdx index 0e362db08..4ed8c377c 100644 --- a/docs/configuration/overview.mdx +++ b/docs/configuration/overview.mdx @@ -31,7 +31,8 @@ Supported hosts: repository: https://github.com/invertase/melos ``` -When using a self-hosted GitHub or GitLab instance, you can specify the repository location like this: +When using a self-hosted GitHub or GitLab instance, you can specify the +repository location like this: ```yaml repository: @@ -184,7 +185,7 @@ A template for the commit message, that is generated by `melos version`. Templates must use mustache syntax and have the following variables available: - `new_package_versions`: A list of the versioned packages and their new -versions. + versions. The default is: @@ -240,7 +241,8 @@ command: Whether to add links to commits in the CHANGELOG.md that is generated by `melos version`. -Enabling this option, requires [`repository`](/configuration/overview#repository) to be specified. +Enabling this option, requires +[`repository`](/configuration/overview#repository) to be specified. ```yaml command: @@ -259,6 +261,41 @@ command: workspaceChangelog: true ``` +### `command/version/changelogs` + +Configure aggregate changelogs which document the changes made to multiple +packages. + +```yaml +command: + version: + changelogs: + - path: FOO_CHANGELOG.md + description: | + All notable changes to foo packages will be documented in this file. + packageFilters: + scope: foo_* +``` + +### `command/version/changelogs/path` + +The path to the changelog file relative to the workspace root. + +### `command/version/changelogs/packageFilters` + +Package filters to match packages that should be included in the changelog. + +See [Filtering Packages](/filters) for all available filters. + +### `command/version/changelogs/description` + +> optional + +A description to include at the top of the changelog. + +If you change this value, you will need to manually update the changelog file to +reflect the new description. + ### `command/version/updateGitTagRefs` Whether to update package version tags in git dependencies of dependents when @@ -276,7 +313,7 @@ command: ### `command/version/releaseUrl` -Whether to generate and print a link to the prefilled release creation page for each package after -versioning. Defaults to `false`. +Whether to generate and print a link to the prefilled release creation page for +each package after versioning. Defaults to `false`. [glob]: https://docs.python.org/3/library/glob.html diff --git a/packages/melos/lib/src/commands/runner.dart b/packages/melos/lib/src/commands/runner.dart index fc7cf654d..34595fe6f 100644 --- a/packages/melos/lib/src/commands/runner.dart +++ b/packages/melos/lib/src/commands/runner.dart @@ -18,6 +18,7 @@ import 'package:yaml/yaml.dart'; import 'package:yaml_edit/yaml_edit.dart'; import '../command_runner/version.dart'; +import '../common/aggregate_changelog.dart'; import '../common/exception.dart'; import '../common/git.dart'; import '../common/git_commit.dart'; @@ -27,11 +28,10 @@ import '../common/intellij_project.dart'; import '../common/io.dart'; import '../common/pending_package_update.dart'; import '../common/platform.dart'; -import '../common/utils.dart'; import '../common/utils.dart' as utils; -import '../common/versioning.dart' as versioning; +import '../common/utils.dart'; import '../common/versioning.dart'; -import '../common/workspace_changelog.dart'; +import '../common/versioning.dart' as versioning; import '../global_options.dart'; import '../logging.dart'; import '../package.dart'; diff --git a/packages/melos/lib/src/commands/version.dart b/packages/melos/lib/src/commands/version.dart index a90a2a28b..f026bf7f2 100644 --- a/packages/melos/lib/src/commands/version.dart +++ b/packages/melos/lib/src/commands/version.dart @@ -650,24 +650,51 @@ mixin _VersionMixin on _RunMixin { } }); - // Build a workspace root changelog if enabled. - if (updateChangelog && - workspace.config.commands.version.workspaceChangelog) { - final today = DateTime.now(); - final dateSlug = - "${today.year.toString()}-${today.month.toString().padLeft(2, '0')}-" - "${today.day.toString().padLeft(2, '0')}"; - final workspaceChangelog = WorkspaceChangelog( - workspace, - dateSlug, - pendingPackageUpdates, - logger, + if (updateChangelog) { + await Future.wait( + workspace.config.commands.version.aggregateChangelogs + .map((changelogConfig) { + return writeAggregateChangelog( + workspace, + changelogConfig, + pendingPackageUpdates, + ); + }), ); - - await workspaceChangelog.write(); } } + Future writeAggregateChangelog( + MelosWorkspace workspace, + AggregateChangelogConfig config, + List pendingPackageUpdates, + ) async { + final today = DateTime.now(); + final dateSlug = [ + today.year.toString(), + today.month.toString().padLeft(2, '0'), + today.day.toString().padLeft(2, '0') + ].join('-'); + + final packages = + await workspace.allPackages.applyFilter(config.packageFilter); + // ignore: parameter_assignments + pendingPackageUpdates = pendingPackageUpdates + .where((update) => packages[update.package.name] != null) + .toList(); + + final changelog = AggregateChangelog( + workspace, + config.description, + dateSlug, + pendingPackageUpdates, + logger, + config.path, + ); + + await changelog.write(); + } + Set _getPackagesWithVersionableCommits( Map> packageCommits, ) { @@ -772,13 +799,15 @@ mixin _VersionMixin on _RunMixin { List pendingPackageUpdates, MelosWorkspace workspace, ) async { - if (workspace.config.commands.version.workspaceChangelog) { + for (final changelog + in workspace.config.commands.version.aggregateChangelogs) { await gitAdd( - 'CHANGELOG.md', + changelog.path, workingDirectory: workspace.path, logger: logger, ); } + await Future.forEach(pendingPackageUpdates, (MelosPendingPackageUpdate pendingPackageUpdate) async { await gitAdd( diff --git a/packages/melos/lib/src/common/workspace_changelog.dart b/packages/melos/lib/src/common/aggregate_changelog.dart similarity index 91% rename from packages/melos/lib/src/common/workspace_changelog.dart rename to packages/melos/lib/src/common/aggregate_changelog.dart index c9f018220..954ee5b0d 100644 --- a/packages/melos/lib/src/common/workspace_changelog.dart +++ b/packages/melos/lib/src/common/aggregate_changelog.dart @@ -22,25 +22,29 @@ import '../workspace.dart'; import 'changelog.dart'; import 'io.dart'; import 'pending_package_update.dart'; +import 'utils.dart'; -class WorkspaceChangelog { - WorkspaceChangelog( +class AggregateChangelog { + AggregateChangelog( this.workspace, - this.title, + this.description, + this.newEntryTitle, this.pendingPackageUpdates, this.logger, + this.path, ); final MelosWorkspace workspace; - final String title; + final String? description; + final String newEntryTitle; final MelosLogger logger; final List pendingPackageUpdates; + final String path; String get _changelogFileHeader => ''' # Change Log -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +${description?.withoutTrailing('\n') ?? ''} '''; String _packageVersionTitle(MelosPendingPackageUpdate update) { @@ -67,7 +71,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline pendingPackageUpdates.where((update) => !update.hasBreakingChanges); body.writeln(_changelogFileHeader); - body.writeln('## $title'); + body.writeln('## $newEntryTitle'); body.writeln(); body.writeln('### Changes'); body.writeln(); @@ -153,8 +157,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline return body.toString(); } - String get path { - return joinAll([workspace.path, 'CHANGELOG.md']); + String get absolutePath { + return joinAll([workspace.path, path]); } @override @@ -163,8 +167,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline } Future read() async { - if (fileExists(path)) { - final contents = await readTextFileAsync(path); + if (fileExists(absolutePath)) { + final contents = await readTextFileAsync(absolutePath); return contents.replaceFirst(_changelogFileHeader, ''); } return ''; @@ -181,6 +185,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline } contents = '$markdown$contents'; - await writeTextFileAsync(path, contents); + await writeTextFileAsync(absolutePath, contents); } } diff --git a/packages/melos/lib/src/common/utils.dart b/packages/melos/lib/src/common/utils.dart index 2aafef8f1..0ca23ecf7 100644 --- a/packages/melos/lib/src/common/utils.dart +++ b/packages/melos/lib/src/common/utils.dart @@ -77,7 +77,7 @@ const envKeyMelosTerminalWidth = 'MELOS_TERMINAL_WIDTH'; final melosPackageUri = Uri.parse('package:melos/melos.dart'); -extension Indent on String { +extension StringUtils on String { String indent(String indent) { final split = this.split('\n'); @@ -97,6 +97,13 @@ extension Indent on String { return buffer.toString(); } + + String withoutTrailing(String trailer) { + if (endsWith(trailer)) { + return substring(0, length - trailer.length); + } + return this; + } } int get terminalWidth { diff --git a/packages/melos/lib/src/common/versioning.dart b/packages/melos/lib/src/common/versioning.dart index bc7d6d2e7..dfbbb564a 100644 --- a/packages/melos/lib/src/common/versioning.dart +++ b/packages/melos/lib/src/common/versioning.dart @@ -330,7 +330,7 @@ Version incrementBuildNumber(Version currentVersion) { if (build.isEmpty) { nextBuildNumber = 0; } else if (build.length == 1) { - final Object? buildNumber = build.first; + final buildNumber = build.first; if (buildNumber is int) { nextBuildNumber = buildNumber + 1; } diff --git a/packages/melos/lib/src/package.dart b/packages/melos/lib/src/package.dart index c933a10d8..a633ebec1 100644 --- a/packages/melos/lib/src/package.dart +++ b/packages/melos/lib/src/package.dart @@ -154,6 +154,113 @@ class PackageFilter { _validate(); } + factory PackageFilter.fromYaml( + Map yaml, { + required String path, + required String workspacePath, + }) { + final scope = assertListOrString( + key: filterOptionScope, + map: yaml, + path: path, + ); + final ignore = assertListOrString( + key: filterOptionIgnore, + map: yaml, + path: path, + ); + final dirExists = assertListOrString( + key: filterOptionDirExists, + map: yaml, + path: path, + ); + final fileExists = assertListOrString( + key: filterOptionFileExists, + map: yaml, + path: path, + ); + final dependsOn = assertListOrString( + key: filterOptionDependsOn, + map: yaml, + path: path, + ); + final noDependsOn = assertListOrString( + key: filterOptionNoDependsOn, + map: yaml, + path: path, + ); + + final updatedSince = assertIsA( + value: yaml[filterOptionSince], + key: filterOptionSince, + path: path, + ); + + final diff = assertIsA( + value: yaml[filterOptionDiff], + key: filterOptionDiff, + path: path, + ); + + final excludePrivatePackagesTmp = assertIsA( + value: yaml[filterOptionNoPrivate], + key: filterOptionNoPrivate, + path: path, + ); + final includePrivatePackagesTmp = assertIsA( + value: yaml[filterOptionPrivate], + key: filterOptionNoPrivate, + path: path, + ); + if (includePrivatePackagesTmp != null && + excludePrivatePackagesTmp != null) { + throw MelosConfigException( + 'Cannot specify both --private and --no-private at the same time', + ); + } + bool? includePrivatePackages; + if (includePrivatePackagesTmp != null) { + includePrivatePackages = includePrivatePackagesTmp; + } + if (excludePrivatePackagesTmp != null) { + includePrivatePackages = !excludePrivatePackagesTmp; + } + + final published = assertIsA( + value: yaml[filterOptionPublished], + key: filterOptionPublished, + path: path, + ); + final nullSafe = assertIsA( + value: yaml[filterOptionNullsafety], + key: filterOptionNullsafety, + path: path, + ); + final flutter = assertIsA( + value: yaml[filterOptionFlutter], + key: filterOptionFlutter, + path: path, + ); + + Glob createPackageGlob(String pattern) => + createGlob(pattern, currentDirectoryPath: workspacePath); + + return PackageFilter( + scope: scope.map(createPackageGlob).toList(), + ignore: ignore.map(createPackageGlob).toList(), + dirExists: dirExists, + fileExists: fileExists, + dependsOn: dependsOn, + noDependsOn: noDependsOn, + updatedSince: updatedSince, + diff: diff, + includePrivatePackages: includePrivatePackages, + published: published, + nullSafe: nullSafe, + flutter: flutter, + ); + } + /// A default constructor with **all** properties as requires, to ensure that /// copyWith functions properly copy all properties. PackageFilter._({ diff --git a/packages/melos/lib/src/scripts.dart b/packages/melos/lib/src/scripts.dart index 894e9f256..5d6e4c0c9 100644 --- a/packages/melos/lib/src/scripts.dart +++ b/packages/melos/lib/src/scripts.dart @@ -20,7 +20,6 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; -import 'common/glob.dart'; import 'common/utils.dart'; import 'common/validation.dart'; import 'package.dart'; @@ -197,9 +196,9 @@ class Script { packageFilter = packageFilterMap == null ? null - : packageFilterFromYaml( + : PackageFilter.fromYaml( packageFilterMap, - scriptName: name, + path: 'scripts/$name/select-package', workspacePath: workspacePath, ); @@ -230,120 +229,6 @@ class Script { ); } - /// Do not use - // this method is not inside PackageFilter as it is specific to Scripts - @visibleForTesting - static PackageFilter packageFilterFromYaml( - Map yaml, { - required String scriptName, - // necessary for the glob workaround - required String workspacePath, - }) { - final filtersPath = 'scripts/$scriptName/select-package'; - - final scope = assertListOrString( - key: filterOptionScope, - map: yaml, - path: filtersPath, - ); - final ignore = assertListOrString( - key: filterOptionIgnore, - map: yaml, - path: filtersPath, - ); - final dirExists = assertListOrString( - key: filterOptionDirExists, - map: yaml, - path: filtersPath, - ); - final fileExists = assertListOrString( - key: filterOptionFileExists, - map: yaml, - path: filtersPath, - ); - final dependsOn = assertListOrString( - key: filterOptionDependsOn, - map: yaml, - path: filtersPath, - ); - final noDependsOn = assertListOrString( - key: filterOptionNoDependsOn, - map: yaml, - path: filtersPath, - ); - - final updatedSince = assertIsA( - value: yaml[filterOptionSince], - key: filterOptionSince, - path: filtersPath, - ); - - final diff = assertIsA( - value: yaml[filterOptionDiff], - key: filterOptionDiff, - path: filtersPath, - ); - - final excludePrivatePackagesTmp = assertIsA( - value: yaml[filterOptionNoPrivate], - key: filterOptionNoPrivate, - path: filtersPath, - ); - final includePrivatePackagesTmp = assertIsA( - value: yaml[filterOptionPrivate], - key: filterOptionNoPrivate, - path: filtersPath, - ); - if (includePrivatePackagesTmp != null && - excludePrivatePackagesTmp != null) { - throw MelosConfigException( - 'Cannot specify both --private and --no-private at the same time', - ); - } - bool? includePrivatePackages; - if (includePrivatePackagesTmp != null) { - includePrivatePackages = includePrivatePackagesTmp; - } - if (excludePrivatePackagesTmp != null) { - includePrivatePackages = !excludePrivatePackagesTmp; - } - - final published = assertIsA( - value: yaml[filterOptionPublished], - key: filterOptionPublished, - path: filtersPath, - ); - final nullSafe = assertIsA( - value: yaml[filterOptionNullsafety], - key: filterOptionNullsafety, - path: filtersPath, - ); - final flutter = assertIsA( - value: yaml[filterOptionFlutter], - key: filterOptionFlutter, - path: filtersPath, - ); - - return PackageFilter( - scope: scope - .map((e) => createGlob(e, currentDirectoryPath: workspacePath)) - .toList(), - ignore: ignore - .map((e) => createGlob(e, currentDirectoryPath: workspacePath)) - .toList(), - dirExists: dirExists, - fileExists: fileExists, - dependsOn: dependsOn, - noDependsOn: noDependsOn, - updatedSince: updatedSince, - diff: diff, - includePrivatePackages: includePrivatePackages, - published: published, - nullSafe: nullSafe, - flutter: flutter, - ); - } - @visibleForTesting static ExecOptions execOptionsFromYaml( Map yaml, { diff --git a/packages/melos/lib/src/workspace_configs.dart b/packages/melos/lib/src/workspace_configs.dart index 85461676b..495398dd7 100644 --- a/packages/melos/lib/src/workspace_configs.dart +++ b/packages/melos/lib/src/workspace_configs.dart @@ -22,6 +22,7 @@ import 'package:glob/glob.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart'; +import '../melos.dart'; import 'common/git_repository.dart'; import 'common/glob.dart'; import 'common/io.dart'; @@ -150,7 +151,10 @@ class CommandConfigs { this.version = VersionCommandConfigs.empty, }); - factory CommandConfigs.fromYaml(Map yaml) { + factory CommandConfigs.fromYaml( + Map yaml, { + required String workspacePath, + }) { final bootstrapMap = assertKeyIsA?>( key: 'bootstrap', map: yaml, @@ -165,7 +169,10 @@ class CommandConfigs { return CommandConfigs( bootstrap: BootstrapCommandConfigs.fromYaml(bootstrapMap ?? const {}), - version: VersionCommandConfigs.fromYaml(versionMap ?? const {}), + version: VersionCommandConfigs.fromYaml( + versionMap ?? const {}, + workspacePath: workspacePath, + ), ); } @@ -292,6 +299,69 @@ BootstrapCommandConfigs( } } +@immutable +class AggregateChangelogConfig { + AggregateChangelogConfig({ + this.isWorkspaceChangelog = false, + required this.path, + required this.packageFilter, + this.description, + }); + + AggregateChangelogConfig.workspace() + : this( + isWorkspaceChangelog: true, + path: 'CHANGELOG.md', + packageFilter: PackageFilter(), + description: ''' +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +''', + ); + + final bool isWorkspaceChangelog; + final String path; + final PackageFilter packageFilter; + final String? description; + + Map toJson() { + return { + 'isWorkspaceChangelog': isWorkspaceChangelog, + 'path': path, + 'packageFilter': packageFilter, + 'description': description, + }; + } + + @override + bool operator ==(Object other) => + other is AggregateChangelogConfig && + runtimeType == other.runtimeType && + other.isWorkspaceChangelog == isWorkspaceChangelog && + other.path == path && + other.packageFilter == packageFilter && + other.description == description; + + @override + int get hashCode => + runtimeType.hashCode ^ + isWorkspaceChangelog.hashCode ^ + path.hashCode ^ + packageFilter.hashCode ^ + description.hashCode; + + @override + String toString() { + return ''' +AggregateChangelogConfig( + isWorkspaceChangelog: $isWorkspaceChangelog, + path: $path, + packageFilter: $packageFilter, + description: $description, +)'''; + } +} + /// Configurations for `melos version`. @immutable class VersionCommandConfigs { @@ -301,12 +371,15 @@ class VersionCommandConfigs { this.includeScopes = false, this.linkToCommits, this.includeCommitId, - this.workspaceChangelog = false, this.updateGitTagRefs = false, this.releaseUrl = false, + this.aggregateChangelogs = const [], }); - factory VersionCommandConfigs.fromYaml(Map yaml) { + factory VersionCommandConfigs.fromYaml( + Map yaml, { + required String workspacePath, + }) { final branch = assertKeyIsA( key: 'branch', map: yaml, @@ -332,11 +405,6 @@ class VersionCommandConfigs { map: yaml, path: 'command/version', ); - final workspaceChangelog = assertKeyIsA( - key: 'workspaceChangelog', - map: yaml, - path: 'command/version', - ); final updateGitTagRefs = assertKeyIsA( key: 'updateGitTagRefs', map: yaml, @@ -348,15 +416,66 @@ class VersionCommandConfigs { path: 'command/version', ); + final workspaceChangelog = assertKeyIsA( + key: 'workspaceChangelog', + map: yaml, + path: 'command/version', + ); + + final aggregateChangelogs = []; + if (workspaceChangelog ?? false) { + aggregateChangelogs.add(AggregateChangelogConfig.workspace()); + } + + final changelogsYaml = assertKeyIsA?>( + key: 'changelogs', + map: yaml, + path: 'command/version', + ); + + for (var i = 0; i < (changelogsYaml?.length ?? 0); i++) { + final entry = changelogsYaml?[i] as Map; + + final path = assertKeyIsA( + map: entry, + path: 'command/version/changelogs[$i]', + key: 'path', + ); + + final packageFilterMap = assertKeyIsA>( + map: entry, + key: 'packageFilters', + path: 'command/version/changelogs[$i]', + ); + final packageFilter = PackageFilter.fromYaml( + packageFilterMap, + path: 'command/version/changelogs[$i]', + workspacePath: workspacePath, + ); + + final description = assertKeyIsA( + map: entry, + path: 'command/version/changelogs[$i]', + key: 'description', + ); + final changelogConfig = AggregateChangelogConfig( + path: path, + packageFilter: packageFilter, + description: description, + ); + + aggregateChangelogs.add(changelogConfig); + } + return VersionCommandConfigs( branch: branch, message: message, includeScopes: includeScopes ?? false, includeCommitId: includeCommitId, linkToCommits: linkToCommits, - workspaceChangelog: workspaceChangelog ?? false, updateGitTagRefs: updateGitTagRefs ?? false, releaseUrl: releaseUrl ?? false, + aggregateChangelogs: aggregateChangelogs, ); } @@ -379,10 +498,6 @@ class VersionCommandConfigs { /// Whether to add links to commits in the generated CHANGELOG.md. final bool? linkToCommits; - /// Whether to also generate a CHANGELOG.md for the entire workspace at the - /// root. - final bool workspaceChangelog; - /// Whether to also update pubspec with git referenced packages. final bool updateGitTagRefs; @@ -390,6 +505,10 @@ class VersionCommandConfigs { /// page for each package after versioning. final bool releaseUrl; + /// A list of changelogs configurations that will be used to generate + /// changelogs which describe the changes in multiple packages. + final List aggregateChangelogs; + Map toJson() { return { if (branch != null) 'branch': branch, @@ -397,8 +516,9 @@ class VersionCommandConfigs { 'includeScopes': includeScopes, if (includeCommitId != null) 'includeCommitId': includeCommitId, if (linkToCommits != null) 'linkToCommits': linkToCommits, - 'workspaceChangelog': workspaceChangelog, 'updateGitTagRefs': updateGitTagRefs, + 'aggregateChangelogs': + aggregateChangelogs.map((config) => config.toJson()).toList(), }; } @@ -411,9 +531,10 @@ class VersionCommandConfigs { other.includeScopes == includeScopes && other.includeCommitId == includeCommitId && other.linkToCommits == linkToCommits && - other.workspaceChangelog == workspaceChangelog && other.updateGitTagRefs == updateGitTagRefs && - other.releaseUrl == releaseUrl; + other.releaseUrl == releaseUrl && + const DeepCollectionEquality() + .equals(other.aggregateChangelogs, aggregateChangelogs); @override int get hashCode => @@ -423,9 +544,9 @@ class VersionCommandConfigs { includeScopes.hashCode ^ includeCommitId.hashCode ^ linkToCommits.hashCode ^ - workspaceChangelog.hashCode ^ updateGitTagRefs.hashCode ^ - releaseUrl.hashCode; + releaseUrl.hashCode ^ + const DeepCollectionEquality().hash(aggregateChangelogs); @override String toString() { @@ -436,9 +557,9 @@ VersionCommandConfigs( includeScopes: $includeScopes, includeCommitId: $includeCommitId, linkToCommits: $linkToCommits, - workspaceChangelog: $workspaceChangelog, updateGitTagRefs: $updateGitTagRefs, releaseUrl: $releaseUrl, + aggregateChangelogs: $aggregateChangelogs, )'''; } } @@ -583,7 +704,7 @@ class MelosWorkspaceConfig { ide: ideMap == null ? IDEConfigs.empty : IDEConfigs.fromYaml(ideMap), commands: commandMap == null ? CommandConfigs.empty - : CommandConfigs.fromYaml(commandMap), + : CommandConfigs.fromYaml(commandMap, workspacePath: path), ); } diff --git a/packages/melos/test/workspace_config_test.dart b/packages/melos/test/workspace_config_test.dart index 8dd32bf22..a58ac3b83 100644 --- a/packages/melos/test/workspace_config_test.dart +++ b/packages/melos/test/workspace_config_test.dart @@ -18,6 +18,7 @@ import 'package:melos/melos.dart'; import 'package:melos/src/common/git_repository.dart'; import 'package:melos/src/common/platform.dart'; import 'package:melos/src/scripts.dart'; +import 'package:melos/src/workspace_configs.dart'; import 'package:test/test.dart'; import 'matchers.dart'; @@ -91,41 +92,53 @@ void main() { expect(value.includeCommitId, null); expect(value.linkToCommits, null); expect(value.updateGitTagRefs, false); - expect(value.workspaceChangelog, false); + expect(value.aggregateChangelogs, isEmpty); }); group('fromYaml', () { test('accepts empty object', () { expect( - VersionCommandConfigs.fromYaml(const {}), + VersionCommandConfigs.fromYaml(const {}, workspacePath: '.'), VersionCommandConfigs.empty, ); }); test('throws if branch is not a string', () { expect( - () => VersionCommandConfigs.fromYaml(const {'branch': 42}), + () => VersionCommandConfigs.fromYaml( + const {'branch': 42}, + workspacePath: '.', + ), throwsMelosConfigException(), ); }); test('throws if message is not a string', () { expect( - () => VersionCommandConfigs.fromYaml(const {'message': 42}), + () => VersionCommandConfigs.fromYaml( + const {'message': 42}, + workspacePath: '.', + ), throwsMelosConfigException(), ); }); test('throws if includeScopes is not a bool', () { expect( - () => VersionCommandConfigs.fromYaml(const {'includeScopes': 42}), + () => VersionCommandConfigs.fromYaml( + const {'includeScopes': 42}, + workspacePath: '.', + ), throwsMelosConfigException(), ); }); test('throws if linkToCommits is not a bool', () { expect( - () => VersionCommandConfigs.fromYaml(const {'linkToCommits': 42}), + () => VersionCommandConfigs.fromYaml( + const {'linkToCommits': 42}, + workspacePath: '.', + ), throwsMelosConfigException(), ); }); @@ -141,16 +154,31 @@ void main() { 'linkToCommits': true, 'updateGitTagRefs': true, 'workspaceChangelog': true, + 'changelogs': [ + { + 'path': 'FOO_CHANGELOG.md', + 'packageFilters': {'flutter': true}, + 'description': 'Changelog for all foo packages.', + } + ] }, + workspacePath: '.', ), - const VersionCommandConfigs( + VersionCommandConfigs( branch: 'branch', message: 'message', includeScopes: true, includeCommitId: true, linkToCommits: true, updateGitTagRefs: true, - workspaceChangelog: true, + aggregateChangelogs: [ + AggregateChangelogConfig.workspace(), + AggregateChangelogConfig( + path: 'FOO_CHANGELOG.md', + packageFilter: PackageFilter(flutter: true), + description: 'Changelog for all foo packages.', + ), + ], ), ); }); @@ -179,18 +207,24 @@ void main() { group('fromYaml', () { test('supports `bootstrap` and `version` missing', () { expect( - CommandConfigs.fromYaml(const {}), + CommandConfigs.fromYaml( + const {}, + workspacePath: '.', + ), CommandConfigs.empty, ); }); test('can decode `bootstrap`', () { expect( - CommandConfigs.fromYaml(const { - 'bootstrap': { - 'usePubspecOverrides': true, - } - }), + CommandConfigs.fromYaml( + const { + 'bootstrap': { + 'usePubspecOverrides': true, + } + }, + workspacePath: '.', + ), const CommandConfigs( bootstrap: BootstrapCommandConfigs( usePubspecOverrides: true, @@ -201,11 +235,14 @@ void main() { test('can decode `bootstrap` with pub get offline', () { expect( - CommandConfigs.fromYaml(const { - 'bootstrap': { - 'runPubGetOffline': true, - } - }), + CommandConfigs.fromYaml( + const { + 'bootstrap': { + 'runPubGetOffline': true, + } + }, + workspacePath: '.', + ), const CommandConfigs( bootstrap: BootstrapCommandConfigs( runPubGetOffline: true, @@ -216,13 +253,16 @@ void main() { test('can decode `version`', () { expect( - CommandConfigs.fromYaml(const { - 'version': { - 'message': 'Hello world', - 'branch': 'main', - 'linkToCommits': true, - } - }), + CommandConfigs.fromYaml( + const { + 'version': { + 'message': 'Hello world', + 'branch': 'main', + 'linkToCommits': true, + } + }, + workspacePath: '.', + ), const CommandConfigs( version: VersionCommandConfigs( branch: 'main', @@ -235,14 +275,20 @@ void main() { test('throws if `bootstrap` is not a map', () { expect( - () => CommandConfigs.fromYaml(const {'bootstrap': 42}), + () => CommandConfigs.fromYaml( + const {'bootstrap': 42}, + workspacePath: '.', + ), throwsMelosConfigException(), ); }); test('throws if `version` is not a map', () { expect( - () => CommandConfigs.fromYaml(const {'version': 42}), + () => CommandConfigs.fromYaml( + const {'version': 42}, + workspacePath: '.', + ), throwsMelosConfigException(), ); });