executePipeline static method
Execute a named pipeline by key.
If pipelineName is null, uses the first (or only) pipeline.
Supported formats: pipelineName1,pipelineName2.
Implementation
static Future<void> executePipeline({String? pipelineName}) async {
final config = FlutterReleaseXConfig().config;
final resolvedPipelines = config.resolvedPipelines;
if (resolvedPipelines == null || resolvedPipelines.isEmpty) {
print('ā No pipelines configured.');
print(' Add "pipelines:" or "pipeline_steps:" to your config.yaml');
return;
}
// Resolve which pipelines to run
final List<PipelineModel> pipelinesToRun = [];
if (pipelineName != null) {
final names = pipelineName
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
for (final name in names) {
if (!resolvedPipelines.containsKey(name)) {
print('ā Pipeline "$name" not found.');
print(' Available pipelines: ${resolvedPipelines.keys.join(', ')}');
print(
' Use "frx pipeline list" to see all pipelines with descriptions.');
return;
}
pipelinesToRun.add(resolvedPipelines[name]!);
}
} else if (resolvedPipelines.length == 1) {
pipelinesToRun.add(resolvedPipelines.values.first);
} else {
// Multiple pipelines, no selection ā show interactive menu
print('\nš¦ Multiple pipelines available:\n');
final keys = resolvedPipelines.keys.toList();
for (int i = 0; i < keys.length; i++) {
final p = resolvedPipelines[keys[i]]!;
final desc = p.description != null ? ' ā ${p.description}' : '';
print(' ${i + 1}. ${keys[i]}$desc (${p.steps.length} steps)');
}
print('');
stdout.write(
'Enter the comma-separated numbers of the pipelines to run (e.g., 2,5,1) or just a single number: ');
final choice = stdin.readLineSync()?.trim();
if (choice == null || choice.isEmpty) {
print('ā No pipeline selected. Exiting.');
return;
}
final choices = choice
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
for (final c in choices) {
final index = int.tryParse(c);
if (index == null || index < 1 || index > keys.length) {
print('ā Invalid choice "$c". Expected 1-${keys.length}.');
return;
}
pipelinesToRun.add(resolvedPipelines[keys[index - 1]]!);
}
}
if (pipelinesToRun.isEmpty) {
print('ā No valid pipelines selected. Exiting.');
return;
}
final globalStopwatch = Stopwatch()..start();
bool globalFailed = false;
for (int pIndex = 0; pIndex < pipelinesToRun.length; pIndex++) {
final pipeline = pipelinesToRun[pIndex];
// Print pipeline header
print('');
print(
'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
print(
' š Pipeline [${pIndex + 1}/${pipelinesToRun.length}]: ${pipeline.name}');
if (pipeline.description != null) {
print(' š ${pipeline.description}');
}
print(' š ${pipeline.steps.length} steps');
print(
'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
print('');
final pipelineStopwatch = Stopwatch()..start();
final results = <_PipelineStepResult>[];
bool pipelineFailed = false;
for (int i = 0; i < pipeline.steps.length; i++) {
final stage = pipeline.steps[i];
final stepNum = i + 1;
final totalSteps = pipeline.steps.length;
print(
'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
print(' [$stepNum/$totalSteps] ${stage.name}');
if (stage.description != null) {
print(' š ${stage.description}');
}
print(
'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
final result = await _executeStep(stage);
results.add(result);
if (result.status == _StepStatus.failed) {
pipelineFailed = true;
// Check if we should continue despite failure
final shouldContinue = stage.continueOnError || stage.allowFailure;
if (!shouldContinue) {
print('');
print(
'ā Pipeline halted at step "${stage.name}". Use continue_on_error: true to skip failures.');
break;
}
print(
' ā ļø Step failed but continue_on_error is enabled ā continuing pipeline');
}
// Upload artifact if configured
if ((result.status == _StepStatus.passed ||
result.status == _StepStatus.warning) &&
stage.uploadOutput &&
stage.outputPath != null) {
// Verify output file exists before upload
final outputFile = File(stage.outputPath!);
final outputDir = Directory(stage.outputPath!);
if (outputFile.existsSync() || outputDir.existsSync()) {
await flutterReleaseXpromptUploadOption(stage.outputPath!);
await generateQrCodeAndLink();
} else {
print(
' ā ļø output_path "${stage.outputPath}" not found after step completed. Skipping upload.');
}
}
// Notify Slack if configured
if (stage.notifySlack &&
(result.status == _StepStatus.passed ||
result.status == _StepStatus.warning)) {
await notifySlack();
}
// Notify Teams if configured
if (stage.notifyTeams &&
(result.status == _StepStatus.passed ||
result.status == _StepStatus.warning)) {
await notifyTeams();
}
print('');
}
pipelineStopwatch.stop();
// Print pipeline summary table
_printPipelineSummary(
pipelineName: pipeline.name,
results: results,
totalDuration: pipelineStopwatch.elapsed,
pipelineFailed: pipelineFailed,
);
if (pipelineFailed) {
globalFailed = true;
print(
'\nā Stopping remaining pipelines due to failure in "${pipeline.name}".');
break;
}
}
globalStopwatch.stop();
if (pipelinesToRun.length > 1) {
print('');
print(
'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
if (globalFailed) {
print(' ā Global Execution FAILED');
} else {
print(' š All Pipelines Completed Successfully!');
}
print(' ā±ļø Total Time: ${_formatDuration(globalStopwatch.elapsed)}');
print(
'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
print('');
}
}