finalize method

Future<FinalizedReleaseSet> finalize({
  1. required List<String> targetNames,
  2. bool push = false,
})

Creates signed tags only when local HEAD equals the configured remote.

Implementation

Future<FinalizedReleaseSet> finalize({
  required List<String> targetNames,
  bool push = false,
}) async {
  if (targetNames.isEmpty) {
    throw const ShipworldException(
      'At least one release target is required',
      code: 'missing_target',
    );
  }
  if ((await _git(['status', '--porcelain'])).isNotEmpty) {
    throw const ShipworldException(
      'Git worktree must be clean before finalizing a release',
      code: 'dirty_worktree',
    );
  }
  final branch = await _git(['branch', '--show-current']);
  final branches = {
    for (final name in targetNames) config.target(name).branch,
  };
  if (branches.length != 1 || branch != branches.single) {
    throw ShipworldException(
      'Targets must be finalized together from ${branches.join(', ')}; '
      'current branch is $branch',
      code: 'wrong_branch',
    );
  }
  await _git(['fetch', '--tags', config.remote, branch]);
  final head = await _git(['rev-parse', 'HEAD']);
  final remoteHead = await _git([
    'rev-parse',
    'refs/remotes/${config.remote}/$branch',
  ]);
  if (head != remoteHead) {
    throw ShipworldException(
      'HEAD must equal ${config.remote}/$branch before finalizing',
      code: 'unmerged_head',
    );
  }

  final tags = <String>[];
  for (final name in targetNames) {
    final target = config.target(name);
    final version = await readPubspecVersion(
      target.versionPath(config.repoRoot),
    );
    await _validateChangelog(config.repoRoot, target, version);
    final tag = target.renderTag(version);
    if (await _hasLocalTag(tag) || await _hasRemoteTag(tag)) {
      throw ShipworldException(
        'Release tag already exists: $tag',
        code: 'tag_exists',
      );
    }
    tags.add(tag);
  }

  final created = <String>[];
  try {
    for (var index = 0; index < tags.length; index++) {
      final target = config.target(targetNames[index]);
      final version = await readPubspecVersion(
        target.versionPath(config.repoRoot),
      );
      final tag = tags[index];
      await _git([
        'tag',
        '-s',
        tag,
        '-m',
        '${target.name} ${version.split('+').first}',
      ]);
      created.add(tag);
      final taggedHead = await _git(['rev-list', '-n', '1', tag]);
      if (taggedHead != head) {
        throw ShipworldException(
          'Tag $tag does not point to release HEAD',
          code: 'tag_target_mismatch',
        );
      }
    }
    if (push) {
      await _git(['push', '--atomic', config.remote, ...tags]);
    }
  } catch (error) {
    for (final tag in created.reversed) {
      try {
        await _git(['tag', '-d', tag]);
      } on Object {
        // Preserve the operation error and attempt all local cleanup.
      }
    }
    rethrow;
  }
  return FinalizedReleaseSet(
    tags: List.unmodifiable(tags),
    head: head,
    pushed: push,
  );
}