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