version method

Future<void> version({
  1. GlobalOptions? global,
  2. PackageFilter? filter,
  3. bool asPrerelease = false,
  4. bool asStableRelease = false,
  5. bool updateChangelog = true,
  6. bool updateDependentsConstraints = true,
  7. bool updateDependentsVersions = true,
  8. bool gitTag = true,
  9. bool? releaseUrl,
  10. String? message,
  11. bool force = false,
  12. bool showPrivatePackages = false,
  13. String? preid,
  14. String? dependentPreid,
  15. bool versionPrivatePackages = false,
  16. Map<String, ManualVersionChange> manualVersions = const {},
})
inherited

Version packages automatically based on the git history or with manually specified versions.

Implementation

Future<void> version({
  GlobalOptions? global,
  PackageFilter? filter,
  bool asPrerelease = false,
  bool asStableRelease = false,
  bool updateChangelog = true,
  bool updateDependentsConstraints = true,
  bool updateDependentsVersions = true,
  bool gitTag = true,
  bool? releaseUrl,
  String? message,
  bool force = false,
  // all
  bool showPrivatePackages = false,
  String? preid,
  String? dependentPreid,
  bool versionPrivatePackages = false,
  Map<String, versioning.ManualVersionChange> manualVersions = const {},
}) async {
  if (asPrerelease && asStableRelease) {
    throw ArgumentError('Cannot use both asPrerelease and asStableRelease.');
  }

  if (updateDependentsVersions && !updateDependentsConstraints) {
    throw ArgumentError(
      'Cannot use updateDependentsVersions without '
      'updateDependentsConstraints.',
    );
  }

  if ((asPrerelease || asStableRelease) && manualVersions.isNotEmpty) {
    throw ArgumentError(
      'Cannot use manualVersions with asPrerelease or asStableRelease.',
    );
  }

  final workspace = await createWorkspace(
    global: global,
    // We ignore `since` package list filtering on the 'version' command as it
    // already filters it itself, filtering here would map dependant version
    // fail as it won't be aware of any packages that have been filtered out
    // here because of the 'since' filter.
    filter: filter?.copyWithUpdatedSince(null),
  );

  if (workspace.config.commands.version.branch != null) {
    final currentBranchName = await gitGetCurrentBranchName(
      workingDirectory: workspace.path,
      logger: logger,
    );
    if (currentBranchName != workspace.config.commands.version.branch) {
      throw RestrictedBranchException(
        workspace.config.commands.version.branch!,
        currentBranchName,
      );
    }
  }

  message ??=
      workspace.config.commands.version.message ?? defaultCommitMessage;

  logger
    ..command('melos version')
    ..child(targetStyle(workspace.path))
    ..newLine();

  final commitMessageTemplate = Template(message, delimiters: '{ }');

  final packageCommits = await _getPackageCommits(
    workspace,
    versionPrivatePackages: versionPrivatePackages,
    since: filter?.updatedSince,
  );

  final packagesWithVersionableCommits =
      _getPackagesWithVersionableCommits(packageCommits);

  for (final packageName in manualVersions.keys) {
    if (!workspace.allPackages.keys.contains(packageName)) {
      exitCode = 1;
      logger
          .error('package "$packageName" does not exist in this workspace.');
      return;
    }
  }

  final packagesToManuallyVersion = manualVersions.keys
      .map((packageName) => workspace.allPackages[packageName]!)
      .toSet();
  final packagesToAutoVersion = {
    for (final package in workspace.filteredPackages.values)
      if (!packagesToManuallyVersion.contains(package))
        if (packagesWithVersionableCommits.contains(package.name))
          if (!asStableRelease || !package.version.isPreRelease) package
  };
  final packagesToVersion = {
    ...packagesToManuallyVersion,
    ...packagesToAutoVersion,
  };
  final dependentPackagesToVersion = <Package>{};
  final pendingPackageUpdates = <MelosPendingPackageUpdate>[];

  if (workspace.config.scripts.containsKey('preversion')) {
    logger
      ..log('Running "preversion" lifecycle script...')
      ..newLine();
    await run(scriptName: 'preversion');
  }

  if (asStableRelease) {
    for (final package in workspace.filteredPackages.values) {
      if (!package.version.isPreRelease) continue;

      pendingPackageUpdates.add(
        MelosPendingPackageUpdate(
          workspace,
          package,
          const [],
          PackageUpdateReason.graduate,
          graduate: asStableRelease,
          prerelease: asPrerelease,
          preid: preid,
          logger: logger,
        ),
      );

      final packageUnscoped = workspace.allPackages[package.name]!;
      dependentPackagesToVersion
          .addAll(packageUnscoped.dependentsInWorkspace.values);
    }
  }

  for (final package in packagesToVersion) {
    final packageUnscoped = workspace.allPackages[package.name]!;
    dependentPackagesToVersion
        .addAll(packageUnscoped.dependentsInWorkspace.values);

    // Add dependentsInWorkspace dependents in the workspace until no more are
    // added.
    var packagesAdded = 1;
    while (packagesAdded != 0) {
      final packagesCountBefore = dependentPackagesToVersion.length;
      final packages = <Package>{...dependentPackagesToVersion};
      for (final dependentPackage in packages) {
        dependentPackagesToVersion
            .addAll(dependentPackage.dependentsInWorkspace.values);
      }
      packagesAdded = dependentPackagesToVersion.length - packagesCountBefore;
    }
  }

  pendingPackageUpdates.addAll(
    packagesToManuallyVersion.map(
      (package) {
        final name = package.name;
        final version = manualVersions[name]!(package.version);
        final commits = packageCommits[name] ?? [];

        String? userChangelogMessage;
        if (updateChangelog) {
          final bool promptForMessage;
          String? defaultUserChangelogMessage;

          if (commits.isEmpty) {
            logger.log(
              'Could not find any commits for manually versioned package '
              '"$name".',
            );

            promptForMessage = true;
            defaultUserChangelogMessage = 'Bump "$name" to `$version`.';
          } else {
            logger.log(
              'Found commits for manually versioned package "$name".',
            );

            promptForMessage = promptBool(
              message: 'Do you want to provide an additional changelog entry '
                  'message?',
              defaultsToWithoutPrompt: false,
            );
          }

          if (promptForMessage) {
            userChangelogMessage = promptInput(
              'Provide a changelog entry message',
              defaultsTo: defaultUserChangelogMessage,
            );
          }
        }

        return MelosPendingPackageUpdate.manual(
          workspace,
          package,
          commits,
          version,
          userChangelogMessage: userChangelogMessage,
          logger: logger,
        );
      },
    ),
  );

  pendingPackageUpdates.addAll(
    packagesToAutoVersion.map(
      (package) => MelosPendingPackageUpdate(
        workspace,
        package,
        packageCommits[package.name]!,
        PackageUpdateReason.commit,
        graduate: asStableRelease,
        prerelease: asPrerelease,
        preid: preid,
        logger: logger,
      ),
    ),
  );

  for (final package in dependentPackagesToVersion) {
    final packageHasPendingUpdate = pendingPackageUpdates.any(
      (packageToVersion) => packageToVersion.package.name == package.name,
    );

    if (!packagesToVersion.contains(package) && !packageHasPendingUpdate) {
      pendingPackageUpdates.add(
        MelosPendingPackageUpdate(
          workspace,
          package,
          const [],
          PackageUpdateReason.dependency,
          // Dependent packages that should have graduated would have already
          // gone through graduation logic above. So graduate should use the
          // default of 'false' here so as not to graduate anything that was
          // specifically excluded.
          // graduate: false,
          prerelease: asPrerelease,
          preid: dependentPreid ?? preid,
          logger: logger,
        ),
      );
    }
  }

  // Filter out private packages.
  if (!versionPrivatePackages) {
    pendingPackageUpdates.removeWhere((update) => update.package.isPrivate);
  }

  if (pendingPackageUpdates.isEmpty) {
    logger.warning(
      'No packages were found that required versioning.',
      label: false,
    );
    logger.hint(
      'Try running "melos list" with the same filtering options to see a '
      'list of packages that were included.',
    );
    logger.hint(
      'Try running "melos version --all" to include private packages',
    );
    return;
  }

  logger.log(
    AnsiStyles.magentaBright(
      'The following '
      '${packageNameStyle(pendingPackageUpdates.length.toString())} '
      'packages will be updated:\n',
    ),
  );

  _logNewVersionTable(
    pendingPackageUpdates,
    updateDependentsVersions: updateDependentsVersions,
    updateDependentsConstraints: updateDependentsConstraints,
  );

  // show commit message
  for (final element in pendingPackageUpdates) {
    logger.trace(AnsiStyles.yellow.bold(element.package.name));
    final commitLogger = logger.childWithoutMessage();
    for (final commit in element.commits) {
      commitLogger.trace(commit.message);
    }
  }

  final shouldContinue = force || promptBool();
  if (!shouldContinue) {
    logger.error('Operation was canceled.', label: false);
    exitCode = 1;
    return;
  }

  await _performPackageUpdates(
    pendingPackageUpdates,
    updateDependentsVersions: updateDependentsVersions,
    updateDependentsConstraints: updateDependentsConstraints,
    updateChangelog: updateChangelog,
    workspace: workspace,
  );

  // TODO allow support for individual package lifecycle version scripts
  if (workspace.config.scripts.containsKey('version')) {
    logger.log('Running "version" lifecycle script...\n');
    await run(scriptName: 'version');
  }

  if (gitTag) {
    await _gitStageChanges(pendingPackageUpdates, workspace);
    await _gitCommitChanges(
      workspace,
      pendingPackageUpdates,
      commitMessageTemplate,
      updateDependentsVersions: updateDependentsVersions,
    );
    await _gitTagChanges(
      pendingPackageUpdates,
      updateDependentsVersions,
    );
  }

  // TODO allow support for individual package lifecycle postversion scripts
  if (workspace.config.scripts.containsKey('postversion')) {
    logger.log('Running "postversion" lifecycle script...\n');
    await run(scriptName: 'postversion');
  }

  if (gitTag) {
    // TODO automatic push support
    logger.success(
      'Versioning successful. '
      'Ensure you push your git changes and tags (if applicable) via '
      '${AnsiStyles.bgBlack.gray('git push --follow-tags')}',
    );
  } else {
    logger.success(
      'Versioning successful. '
      'Ensure you commit and push your changes (if applicable).',
    );
  }

  // TODO Support for automatically creating a release,
  // e.g. when GITHUB_TOKEN is present in CI or using `gh release create`
  // from GitHub CLI.

  if (releaseUrl ?? config.commands.version.releaseUrl) {
    final repository = workspace.config.repository;

    if (repository == null) {
      logger.warning(
        'No repository configured in melos.yaml to generate a '
        'release for.',
      );
    } else if (repository is! SupportsManualRelease) {
      logger.warning('Repository does not support releases urls');
    } else {
      final pendingPackageReleases = pendingPackageUpdates.map((update) {
        return link(
          repository.releaseUrlForUpdate(update),
          update.package.name,
        );
      }).join(ansiStylesDisabled ? '\n' : ', ');

      logger.success(
        'Make sure you create a release for each new package version:'
        '${ansiStylesDisabled ? '\n' : ' '}'
        '${AnsiStyles.bgBlack.gray(pendingPackageReleases)}',
      );
    }
  }
}