executeRelease method

Future<ReleaseResult> executeRelease(
  1. ReleaseContext context
)

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);
  }
}