executeRelease method
Executes the complete release orchestration flow for context.
Implementation
Future<ReleaseResult> executeRelease(ReleaseContext context) async {
final strategy = selectStrategy(context.type);
// 0. Process Lock Acquisition
await _lockManager.acquireLock(
context.projectPath,
operation: context.type.cliName,
dryRun: context.dryRun,
);
var currentState = ReleaseStateRecord(
operation: context.type.cliName,
releaseMode: context.type.cliName,
environment: context.environmentName,
platform: context.targetPlatform,
status: ReleaseStatus.created,
);
await _stateTracker.saveState(context.projectPath, currentState,
dryRun: context.dryRun);
try {
_output.info('Release (${context.type.displayName})');
_output.detail(
'├─ Preparing: Path "${context.projectPath}" (dry-run: ${context.dryRun})');
// 1. Pre-flight Validation
final validationResult = await _validatorPipeline.validate(context);
if (!validationResult.passed) {
_output.detail('├─ Validating: ✗ Pre-flight validation failed');
for (final err in validationResult.errors) {
_output.error(' • $err');
}
currentState = currentState.copyWith(
status: ReleaseStatus.failed,
failedStage: 'validation',
errors: validationResult.errors,
);
await _stateTracker.saveState(context.projectPath, currentState,
dryRun: context.dryRun);
return ReleaseResult.failure(
type: context.type,
message: 'Pre-flight validation failed.',
errors: validationResult.errors,
warnings: validationResult.warnings,
);
}
_output.detail(
'├─ Validating: ✓ Pre-flight validation passed (${validationResult.checks.length} checks)');
currentState = currentState.copyWith(status: ReleaseStatus.validated);
await _stateTracker.saveState(context.projectPath, currentState,
dryRun: context.dryRun);
// 2. Version Management & Calculation
final ProjectVersion currentVersion =
_versionManager.getCurrentVersion(context.projectPath);
final ProjectVersion targetVersion =
await _versionManager.calculateNextVersion(
projectPath: context.projectPath,
mode: context.versionBumpMode,
releaseType: context.type,
explicitVersionName: context.versionName,
explicitBuildNumber: context.buildNumber,
);
if (context.type == ReleaseType.shorebirdPatch) {
_output.detail(
'├─ Versioning: Shorebird Patch targets existing release ($currentVersion - no version bump)');
} else if (currentVersion == targetVersion) {
_output.detail('├─ Versioning: Version unchanged ($currentVersion)');
} else {
_output.detail('├─ Versioning: $currentVersion ➔ $targetVersion');
if (!context.dryRun) {
_versionManager.updateVersion(context.projectPath, targetVersion);
}
}
if (context.dryRun) {
_output.info('\n[DRY-RUN SUMMARY]');
_output.info('Current version: $currentVersion');
_output.info('Release type: ${context.type.displayName}');
_output.info('Target version: $targetVersion');
_output.info('Mode: Dry Run');
}
// Update context with resolved version info
final activeContext = context.copyWith(
versionName: targetVersion.versionName,
buildNumber: targetVersion.buildNumber,
);
// Dispatch Start Notification Event
final isPatch = context.type == ReleaseType.shorebirdPatch;
final startEvent = ReleaseEvent(
eventType: isPatch
? ReleaseEventType.patchStarted
: ReleaseEventType.releaseStarted,
version: targetVersion.versionName,
buildNumber: targetVersion.buildNumber.toString(),
releaseMode: context.type.cliName,
platforms: [context.targetPlatform ?? 'android'],
environment: activeContext.config.environmentName,
flavor: activeContext.config.flavor,
isPatch: isPatch,
);
await _notificationManager.dispatch(startEvent, dryRun: context.dryRun);
_output.detail('├─ Executing strategy: ${strategy.type.displayName}');
final result = await strategy.execute(activeContext);
// Dispatch Completion Notification Event
final completionEvent = ReleaseEvent(
eventType: result.success
? (isPatch
? ReleaseEventType.patchSucceeded
: ReleaseEventType.releaseSucceeded)
: (isPatch
? ReleaseEventType.patchFailed
: ReleaseEventType.releaseFailed),
version: targetVersion.versionName,
buildNumber: targetVersion.buildNumber.toString(),
releaseMode: context.type.cliName,
platforms: [context.targetPlatform ?? 'android'],
deploymentStatus: result.success ? 'Successful' : 'Failed',
environment: activeContext.config.environmentName,
flavor: activeContext.config.flavor,
error: result.errors.isNotEmpty ? result.errors.first : null,
isPatch: isPatch,
);
final notifyResults = await _notificationManager.dispatch(completionEvent,
dryRun: context.dryRun);
if (notifyResults.isNotEmpty) {
for (final nRes in notifyResults) {
if (nRes.success) {
_output.detail('├─ Notification (${nRes.providerName}): SUCCESS');
} else {
_output.detail(
'├─ Notification (${nRes.providerName}): FAILED (${nRes.error})');
}
}
}
if (result.success) {
if (result.metadata.containsKey('artifactPaths')) {
final artifacts =
result.metadata['artifactPaths'] as Map<String, String>;
for (final entry in artifacts.entries) {
_output.detail('├─ Artifact (${entry.key}): ${entry.value}');
}
}
_output.detail('└─ Complete: ${result.message}');
currentState = currentState.copyWith(status: ReleaseStatus.completed);
await _stateTracker.saveState(context.projectPath, currentState,
dryRun: context.dryRun);
} else {
_output.detail('└─ Failed: ${result.message}');
currentState = currentState.copyWith(
status: ReleaseStatus.failed,
failedStage: result.errors.isNotEmpty ? 'execution' : 'deployment',
errors: result.errors,
);
await _stateTracker.saveState(context.projectPath, currentState,
dryRun: context.dryRun);
}
return result;
} finally {
await _lockManager.releaseLock(context.projectPath,
dryRun: context.dryRun);
}
}