208 lines
6.1 KiB
Dart
208 lines
6.1 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:args/args.dart';
|
|
import 'package:file/file.dart';
|
|
import 'package:file/local.dart';
|
|
import 'package:platform/platform.dart';
|
|
|
|
import 'proto/conductor_state.pb.dart' as pb;
|
|
|
|
const String kUpstreamRemote = 'https://github.com/flutter/flutter.git';
|
|
|
|
const String gsutilBinary = 'gsutil.py';
|
|
|
|
const List<String> kReleaseChannels = <String>[
|
|
'stable',
|
|
'beta',
|
|
'dev',
|
|
'master',
|
|
];
|
|
|
|
const String kReleaseDocumentationUrl = 'https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process';
|
|
|
|
const String kLuciPackagingConsoleLink = 'https://ci.chromium.org/p/flutter/g/packaging/console';
|
|
|
|
final RegExp releaseCandidateBranchRegex = RegExp(
|
|
r'flutter-(\d+)\.(\d+)-candidate\.(\d+)',
|
|
);
|
|
|
|
/// Cast a dynamic to String and trim.
|
|
String stdoutToString(dynamic input) {
|
|
final String str = input as String;
|
|
return str.trim();
|
|
}
|
|
|
|
class ConductorException implements Exception {
|
|
ConductorException(this.message);
|
|
|
|
final String message;
|
|
|
|
@override
|
|
String toString() => 'Exception: $message';
|
|
}
|
|
|
|
Directory? _flutterRoot;
|
|
Directory get localFlutterRoot {
|
|
if (_flutterRoot != null) {
|
|
return _flutterRoot!;
|
|
}
|
|
String filePath;
|
|
const FileSystem fileSystem = LocalFileSystem();
|
|
const Platform platform = LocalPlatform();
|
|
|
|
filePath = platform.script.toFilePath();
|
|
final String checkoutsDirname = fileSystem.path.normalize(
|
|
fileSystem.path.join(
|
|
fileSystem.path.dirname(filePath),
|
|
'..', // flutter/dev/tools
|
|
'..', // flutter/dev
|
|
'..', // flutter
|
|
),
|
|
);
|
|
_flutterRoot = fileSystem.directory(checkoutsDirname);
|
|
return _flutterRoot!;
|
|
}
|
|
|
|
bool assertsEnabled() {
|
|
// Verify asserts enabled
|
|
bool assertsEnabled = false;
|
|
|
|
assert(() {
|
|
assertsEnabled = true;
|
|
return true;
|
|
}());
|
|
return assertsEnabled;
|
|
}
|
|
|
|
/// Either return the value from [env] or fall back to [argResults].
|
|
///
|
|
/// If the key does not exist in either the environment or CLI args, throws a
|
|
/// [ConductorException].
|
|
///
|
|
/// The environment is favored over CLI args since the latter can have a default
|
|
/// value, which the environment should be able to override.
|
|
String? getValueFromEnvOrArgs(
|
|
String name,
|
|
ArgResults argResults,
|
|
Map<String, String> env, {
|
|
bool allowNull = false,
|
|
}) {
|
|
final String envName = fromArgToEnvName(name);
|
|
if (env[envName] != null) {
|
|
return env[envName];
|
|
}
|
|
final String? argValue = argResults[name] as String?;
|
|
if (argValue != null) {
|
|
return argValue;
|
|
}
|
|
|
|
if (allowNull) {
|
|
return null;
|
|
}
|
|
throw ConductorException(
|
|
'Expected either the CLI arg --$name or the environment variable $envName '
|
|
'to be provided!');
|
|
}
|
|
|
|
/// Return multiple values from the environment or fall back to [argResults].
|
|
///
|
|
/// Values read from an environment variable are assumed to be comma-delimited.
|
|
///
|
|
/// If the key does not exist in either the CLI args or environment, throws a
|
|
/// [ConductorException].
|
|
///
|
|
/// The environment is favored over CLI args since the latter can have a default
|
|
/// value, which the environment should be able to override.
|
|
List<String> getValuesFromEnvOrArgs(
|
|
String name,
|
|
ArgResults argResults,
|
|
Map<String, String> env,
|
|
) {
|
|
final String envName = fromArgToEnvName(name);
|
|
if (env[envName] != null && env[envName] != '') {
|
|
return env[envName]!.split(',');
|
|
}
|
|
final List<String> argValues = argResults[name] as List<String>;
|
|
if (argValues != null) {
|
|
return argValues;
|
|
}
|
|
|
|
throw ConductorException(
|
|
'Expected either the CLI arg --$name or the environment variable $envName '
|
|
'to be provided!');
|
|
}
|
|
|
|
/// Translate CLI arg names to env variable names.
|
|
///
|
|
/// For example, 'state-file' -> 'STATE_FILE'.
|
|
String fromArgToEnvName(String argName) {
|
|
return argName.toUpperCase().replaceAll(r'-', r'_');
|
|
}
|
|
|
|
/// Return a web link for the user to open a new PR.
|
|
///
|
|
/// Includes PR title and body via query params.
|
|
String getNewPrLink({
|
|
required String userName,
|
|
required String repoName,
|
|
required pb.ConductorState state,
|
|
}) {
|
|
assert(state.releaseChannel.isNotEmpty);
|
|
assert(state.releaseVersion.isNotEmpty);
|
|
late final String candidateBranch;
|
|
late final String workingBranch;
|
|
late final String repoLabel;
|
|
switch (repoName) {
|
|
case 'flutter':
|
|
candidateBranch = state.framework.candidateBranch;
|
|
workingBranch = state.framework.workingBranch;
|
|
repoLabel = 'Framework';
|
|
break;
|
|
case 'engine':
|
|
candidateBranch = state.engine.candidateBranch;
|
|
workingBranch = state.engine.workingBranch;
|
|
repoLabel = 'Engine';
|
|
break;
|
|
default:
|
|
throw ConductorException('Expected repoName to be one of flutter or engine but got $repoName.');
|
|
}
|
|
assert(candidateBranch.isNotEmpty);
|
|
assert(workingBranch.isNotEmpty);
|
|
final String title = '[flutter_releases] Flutter ${state.releaseChannel} '
|
|
'${state.releaseVersion} $repoLabel Cherrypicks';
|
|
final StringBuffer body = StringBuffer();
|
|
body.write('''
|
|
# Flutter ${state.releaseChannel} ${state.releaseVersion} $repoLabel
|
|
|
|
## Scheduled Cherrypicks
|
|
|
|
''');
|
|
if (repoName == 'engine') {
|
|
if (state.engine.dartRevision.isNotEmpty) {
|
|
// shorten hashes to make final link manageable
|
|
// prefix with github org/repo so GitHub will auto-generate a hyperlink
|
|
body.writeln('- Roll dart revision: dart-lang/sdk@${state.engine.dartRevision.substring(0, 9)}');
|
|
}
|
|
for (final pb.Cherrypick cp in state.engine.cherrypicks) {
|
|
// Only list commits that map to a commit that exists upstream.
|
|
if (cp.trunkRevision.isNotEmpty) {
|
|
body.writeln('- commit: flutter/engine@${cp.trunkRevision.substring(0, 9)}');
|
|
}
|
|
}
|
|
} else {
|
|
for (final pb.Cherrypick cp in state.framework.cherrypicks) {
|
|
// Only list commits that map to a commit that exists upstream.
|
|
if (cp.trunkRevision.isNotEmpty) {
|
|
body.writeln('- commit: ${cp.trunkRevision.substring(0, 9)}');
|
|
}
|
|
}
|
|
}
|
|
return 'https://github.com/flutter/$repoName/compare/'
|
|
'$candidateBranch...$userName:$workingBranch?'
|
|
'expand=1'
|
|
'&title=${Uri.encodeQueryComponent(title)}'
|
|
'&body=${Uri.encodeQueryComponent(body.toString())}';
|
|
}
|