execCommandStreaming function
Implementation
Future<void> execCommandStreaming(
List<String> command,
String? vmIp,
String buildJobId,
String runId,
String token, {
required Future<bool> Function() isCancelled,
}) async {
if (vmIp == null) {
throw StateError('Cannot stream SSH command: VM IP is null.');
}
final process = await Process.start('/usr/bin/ssh', [
'-o',
'StrictHostKeyChecking=no',
'-o',
'UserKnownHostsFile=/dev/null',
'-o',
'LogLevel=ERROR',
'-o',
'RequestTTY=no',
'-o',
'BatchMode=yes',
'-o',
'ServerAliveInterval=30',
'-o',
'ServerAliveCountMax=5',
'-i',
_sshKeyPath,
'$sshUser@$vmIp',
...command,
]);
await process.stdin.close();
final stdoutCompleter = Completer<void>();
final stderrCompleter = Completer<void>();
final outputErrors = <String>[];
var hasSuccessfulStep = false;
void processLine(String line) {
final trimmed = line.trim();
if (trimmed.isEmpty || _isNoisyLine(trimmed)) return;
if (trimmed.contains('✅') || trimmed.contains('Job succeeded')) {
hasSuccessfulStep = true;
}
if (_isActError(trimmed)) {
outputErrors.add(trimmed);
}
final cleanLine = stripActPrefix(trimmed);
logInfo(buildJobId, runId, cleanLine);
}
process.stdout.transform(utf8.decoder).listen((data) {
final masked = data.replaceAll(token, '***').trim();
if (masked.isNotEmpty) {
for (final line in LineSplitter.split(masked)) {
processLine(line);
}
}
}, onDone: () => stdoutCompleter.complete());
process.stderr.transform(utf8.decoder).listen((data) {
final masked = data.replaceAll(token, '***').trim();
if (masked.isNotEmpty) {
for (final line in LineSplitter.split(masked)) {
processLine(line);
}
}
}, onDone: () => stderrCompleter.complete());
final cancelTimer = Timer.periodic(const Duration(seconds: 5), (_) async {
if (await isCancelled()) {
process.kill(ProcessSignal.sigterm);
}
});
final exitCode = await process.exitCode;
cancelTimer.cancel();
await stdoutCompleter.future;
await stderrCompleter.future;
if (exitCode != 0) {
throw Exception('act exited with code $exitCode');
}
if (outputErrors.isNotEmpty) {
throw Exception('act reported errors:\n${outputErrors.join('\n')}');
}
if (!hasSuccessfulStep) {
throw Exception(
'act exited with code 0 but no steps were executed. '
'Check that your workflow has a valid runs-on key.',
);
}
}