finalize method
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,
);
}