prepare method
Future<PreparedReleaseSet>
prepare({
- required Map<
String, ReleaseType> bumps, - bool dryRun = false,
Preflights every target, then writes and commits the release as one unit.
Implementation
Future<PreparedReleaseSet> prepare({
required Map<String, ReleaseType> bumps,
bool dryRun = false,
}) async {
if (bumps.isEmpty) {
throw const ShipworldException(
'At least one release target is required',
code: 'missing_target',
);
}
final staged = await _git(['diff', '--cached', '--name-only']);
if (staged.isNotEmpty && !dryRun) {
throw const ShipworldException(
'Staged changes are not allowed before release preparation',
code: 'dirty_worktree',
);
}
final branch = await _git(['branch', '--show-current']);
final allowedDirtyPaths = <String>{
for (final name in bumps.keys)
if (config.target(name).changelog case final changelog?)
_repoRelative(
config.repoRoot,
config
.target(name)
.targetPath(config.repoRoot, changelog, 'changelog'),
),
};
final status = await _git(['status', '--porcelain']);
if (status.isNotEmpty && !dryRun) {
final dirtyPaths = status
.split('\n')
.where((line) => line.trim().isNotEmpty)
.map((line) => line.length > 3 ? line.substring(3).trim() : line)
.toSet();
final unexpected = dirtyPaths.difference(allowedDirtyPaths);
if (unexpected.isNotEmpty) {
throw ShipworldException(
'Git worktree contains unrelated changes: ${unexpected.join(', ')}',
code: 'dirty_worktree',
);
}
}
final prepared = <PreparedReleaseTarget>[];
final renderedFiles = <String, String>{};
final originalFiles = <String, String>{};
final stagePaths = <String>{};
for (final entry in bumps.entries) {
final target = config.target(entry.key);
if (!dryRun && branch != target.branch) {
throw ShipworldException(
'Target ${target.name} must be prepared from ${target.branch}; '
'current branch is $branch',
code: 'wrong_branch',
);
}
final pubspecPath = target.versionPath(config.repoRoot);
final pubspecContent = await File(pubspecPath).readAsString();
final previousVersion = await readPubspecVersion(pubspecPath);
final version = _bumpConfiguredVersion(
previousVersion,
entry.value,
incrementBuild: target.kind == ShipworldTargetKind.flutterApplication,
);
final tag = target.renderTag(version);
if (await _hasLocalTag(tag) || await _hasRemoteTag(tag)) {
throw ShipworldException(
'Release tag already exists: $tag',
code: 'tag_exists',
);
}
await _validateChangelog(config.repoRoot, target, version);
originalFiles[pubspecPath] = pubspecContent;
renderedFiles[pubspecPath] = renderPubspecVersion(
pubspecContent,
version,
);
stagePaths.add(_repoRelative(config.repoRoot, pubspecPath));
for (final writer in target.version.synchronized) {
final writerPath = target.targetPath(
config.repoRoot,
writer.path,
'synchronized version file',
);
final content = await File(writerPath).readAsString();
originalFiles[writerPath] = content;
renderedFiles[writerPath] = switch (writer.kind) {
VersionWriterKind.dartConstant => renderVersionConstant(
version,
constant: writer.constant,
),
};
stagePaths.add(_repoRelative(config.repoRoot, writerPath));
}
if (target.changelog case final changelog?) {
stagePaths.add(
_repoRelative(
config.repoRoot,
target.targetPath(config.repoRoot, changelog, 'changelog'),
),
);
}
prepared.add(
PreparedReleaseTarget(
name: target.name,
previousVersion: previousVersion,
version: version,
tag: tag,
),
);
}
final commitMessage = prepared.length == 1
? config
.target(prepared.single.name)
.renderCommit(prepared.single.version)
: config.renderBatchCommit([
for (final item in prepared)
(name: item.name, version: item.version),
]);
if (dryRun) {
for (final item in prepared) {
context.logger.info('Would update ${item.name} to ${item.version}');
}
return PreparedReleaseSet(
dryRun: true,
targets: List.unmodifiable(prepared),
commitMessage: commitMessage,
);
}
try {
for (final entry in renderedFiles.entries) {
await File(entry.key).writeAsString(entry.value);
}
await _git(['add', '--', ...stagePaths]);
await _git(['commit', '-m', commitMessage]);
} catch (error) {
try {
await _git(['restore', '--staged', '--', ...stagePaths]);
} on Object {
// Preserve the original error; file restoration below is authoritative.
}
for (final entry in originalFiles.entries) {
await File(entry.key).writeAsString(entry.value);
}
rethrow;
}
return PreparedReleaseSet(
dryRun: false,
targets: List.unmodifiable(prepared),
commitMessage: commitMessage,
);
}