blob: c8c7a8095d8e8be9a0f86afd40e5d943f12c6c9e [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'engines.dart';
import 'process.dart';
const _toolProxyAddress = '/abns/dart-eng-tool-proxy/'
'prod-dart-eng-tool-proxy-tool-proxy.annealed-tool-proxy';
const _mpaCli = '/google/bin/releases/dynamic-access-dev/reqrat/mpa';
/// How long a request may wait for MPA approval before it is abandoned.
const _approvalTimeout = Duration(hours: 48);
/// How long to wait for CIPD packages after a build was triggered.
const _buildGracePeriod = Duration(hours: 12);
/// Matches the access approval link printed by `tool-proxy-cli`, capturing
/// the approval ID.
final _approvalUrlPattern = RegExp(
r'https://(?:aa|accessapprovals)\.corp\.google\.com/(?:mpa/)?'
r'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})',
);
/// Matches a reviewer who approved a request in `mpa get --explain` output,
/// e.g. `review_state:APPROVED email:"someone@google.com"`.
final _approverPattern = RegExp(r'review_state:APPROVED email:"([^"]+)"');
/// A running or completed `tool-proxy-cli --block_on_mpa` invocation that
/// waits for MPA approval and triggers the `dart-internal/ci/<browser>`
/// packaging builder.
final class BuildRequest {
final EngineStatus status;
final Process _process;
final _lines = <String>[];
final _approvalReady = Completer<void>();
RegExpMatch? _approvalMatch;
int? _exitCode;
DateTime? _exitedAt;
BuildRequest._(this.status, this._process) {
_process.stdin.close().ignore();
_listen(_process.stdout);
_listen(_process.stderr);
_process.exitCode.then((code) {
_exitCode = code;
_exitedAt = DateTime.now();
if (!_approvalReady.isCompleted) _approvalReady.complete();
});
}
/// The MPA approval link, once `tool-proxy-cli` has printed it.
String? get approvalUrl => _approvalMatch?[0];
/// The exit code of `tool-proxy-cli`, or `null` if it is still running.
int? get exitCode => _exitCode;
/// Whether the build was triggered over [_buildGracePeriod] ago.
bool get hasTimedOut =>
_exitCode == 0 &&
DateTime.now().difference(_exitedAt!) >= _buildGracePeriod;
/// Waits until `tool-proxy-cli` prints its approval link, exits, or
/// [timeout] passes, and returns the approval link if one was printed.
Future<String?> waitForApprovalUrl({
Duration timeout = const Duration(seconds: 90),
}) async {
await _approvalReady.future.timeout(timeout, onTimeout: () {});
return approvalUrl;
}
/// Returns the emails of the reviewers who approved this request's MPA
/// prompt (go/mpa), or an empty list if none can be found.
Future<List<String>> approvers() async {
final approvalId = _approvalMatch?[1];
if (approvalId == null) return const [];
final output = await runChecked(_mpaCli, [
'get',
'--explain',
'approvals/$approvalId',
]);
return {
for (final match in _approverPattern.allMatches(output)) match[1]!,
}.toList();
}
/// Terminates the `tool-proxy-cli` process if it is still running.
void kill() => _process.kill();
/// The last [count] lines of output from `tool-proxy-cli`, for error
/// reports.
String outputTail([int count = 15]) =>
_lines.skip(_lines.length > count ? _lines.length - count : 0).join('\n');
void _listen(Stream<List<int>> stream) {
stream
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
_lines.add(line);
if (_approvalMatch == null) {
final match = _approvalUrlPattern.firstMatch(line);
if (match != null) {
_approvalMatch = match;
if (!_approvalReady.isCompleted) _approvalReady.complete();
}
}
});
}
}
/// Requests CIPD packaging builds through the dart-eng tool proxy.
///
/// See go/dart-engprod/browsers.md#triggering-cipd.
final class ToolProxy {
/// The go/dar-slash-notation justification for the MPA request.
final String reason;
ToolProxy({required this.reason});
/// Starts `tool-proxy-cli` to request MPA approval and trigger the
/// packaging build for [status].
Future<BuildRequest> startBuild(EngineStatus status) async {
final process = await startProcess('timeout', [
'${_approvalTimeout.inSeconds}s',
'tool-proxy-cli',
'--tool_proxy=$_toolProxyAddress',
'--block_on_mpa',
'--reason=$reason',
'-I',
'browser',
':browser',
status.engine.name,
':version',
status.latest.builderVersion,
]);
return BuildRequest._(status, process);
}
}