[flutter_conductor] Add "start", "status", "clean" commands to conductor release tool (#80528)

This commit is contained in:
Christopher Fujino 2021-04-19 15:04:04 -07:00 committed by GitHub
parent dc40e23893
commit 4a7f280687
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 2362 additions and 121 deletions

View file

@ -40,6 +40,8 @@ analyzer:
unnecessary_null_comparison: ignore
exclude:
- "bin/cache/**"
# Ignore protoc generated files
- "dev/tools/lib/proto/*"
linter:
rules:

View file

@ -2,18 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Rolls the dev channel.
// Only tested on Linux.
//
// See: https://github.com/flutter/flutter/wiki/Release-process
import 'dart:io' as io;
import 'package:args/command_runner.dart';
import 'package:dev_tools/clean.dart';
import 'package:dev_tools/codesign.dart';
import 'package:dev_tools/globals.dart';
import 'package:dev_tools/roll_dev.dart';
import 'package:dev_tools/repository.dart';
import 'package:dev_tools/start.dart';
import 'package:dev_tools/status.dart';
import 'package:dev_tools/stdio.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
@ -54,6 +54,16 @@ Future<void> main(List<String> args) async {
checkouts: checkouts,
flutterRoot: localFlutterRoot,
),
StatusCommand(
checkouts: checkouts,
),
StartCommand(
checkouts: checkouts,
flutterRoot: localFlutterRoot,
),
CleanCommand(
checkouts: checkouts,
),
].forEach(runner.addCommand);
if (!assertsEnabled()) {
@ -63,8 +73,8 @@ Future<void> main(List<String> args) async {
try {
await runner.run(args);
} on Exception catch (e) {
stdio.printError(e.toString());
} on Exception catch (e, stacktrace) {
stdio.printError('$e\n\n$stacktrace');
io.exit(1);
}
}

75
dev/tools/lib/clean.dart Normal file
View file

@ -0,0 +1,75 @@
// 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/command_runner.dart';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import './globals.dart';
import './repository.dart';
import './state.dart';
import './stdio.dart';
const String kYesFlag = 'yes';
const String kStateOption = 'state-file';
/// Command to clean up persistent state file.
///
/// If the release was not completed, this command will abort the release.
class CleanCommand extends Command<void> {
CleanCommand({
@required this.checkouts,
}) : platform = checkouts.platform,
fileSystem = checkouts.fileSystem,
stdio = checkouts.stdio {
final String defaultPath = defaultStateFilePath(platform);
argParser.addFlag(
kYesFlag,
help: 'Override confirmation checks.',
);
argParser.addOption(
kStateOption,
defaultsTo: defaultPath,
help: 'Path to persistent state file. Defaults to $defaultPath',
);
}
final Checkouts checkouts;
final FileSystem fileSystem;
final Platform platform;
final Stdio stdio;
@override
String get name => 'clean';
@override
String get description => 'Cleanup persistent state file. '
'This will abort a work in progress release.';
@override
void run() {
final File stateFile = checkouts.fileSystem.file(argResults[kStateOption]);
if (!stateFile.existsSync()) {
throw ConductorException(
'No persistent state file found at ${stateFile.path}!');
}
if (!(argResults[kYesFlag] as bool)) {
stdio.printStatus(
'Are you sure you want to clean up the persistent state file at\n'
'${stateFile.path} (y/n)?',
);
final String response = stdio.readLineSync();
// Only proceed if the first character of stdin is 'y' or 'Y'
if (response.isEmpty || response[0].toLowerCase() != 'y') {
stdio.printStatus('Aborting clean operation.');
return;
}
}
stdio.printStatus('Deleting persistent state file ${stateFile.path}...');
stateFile.deleteSync();
}
}

View file

@ -33,11 +33,15 @@ class CodesignCommand extends Command<void> {
CodesignCommand({
@required this.checkouts,
@required this.flutterRoot,
FrameworkRepository framework,
}) : assert(flutterRoot != null),
fileSystem = checkouts.fileSystem,
platform = checkouts.platform,
stdio = checkouts.stdio,
processManager = checkouts.processManager {
if (framework != null) {
_framework = framework;
}
argParser.addFlag(
kVerify,
help:
@ -71,13 +75,12 @@ class CodesignCommand extends Command<void> {
final Directory flutterRoot;
FrameworkRepository _framework;
FrameworkRepository get framework => _framework ??= FrameworkRepository.localRepoAsUpstream(
checkouts,
upstreamPath: flutterRoot.path,
);
@visibleForTesting
set framework(FrameworkRepository framework) => _framework = framework;
FrameworkRepository get framework {
return _framework ??= FrameworkRepository.localRepoAsUpstream(
checkouts,
upstreamPath: flutterRoot.path,
);
}
@override
String get name => 'codesign';
@ -102,15 +105,19 @@ class CodesignCommand extends Command<void> {
String revision;
if (argResults.wasParsed(kRevision)) {
stdio.printError('Warning! When providing an arbitrary revision, the contents of the cache may not');
stdio.printError('match the expected binaries in the conductor tool. It is preferred to check out');
stdio.printError('the desired revision and run that version of the conductor.\n');
stdio.printError(
'Warning! When providing an arbitrary revision, the contents of the cache may not');
stdio.printError(
'match the expected binaries in the conductor tool. It is preferred to check out');
stdio.printError(
'the desired revision and run that version of the conductor.\n');
revision = argResults[kRevision] as String;
} else {
revision = (processManager.runSync(
<String>['git', 'rev-parse', 'HEAD'],
workingDirectory: framework.checkoutDirectory.path,
).stdout as String).trim();
).stdout as String)
.trim();
assert(revision.isNotEmpty);
}
@ -158,7 +165,10 @@ class CodesignCommand extends Command<void> {
'dart-sdk/bin/dart',
'dart-sdk/bin/dartaotruntime',
'dart-sdk/bin/utils/gen_snapshot',
].map((String relativePath) => fileSystem.path.join(framework.cacheDirectory, relativePath)).toList();
]
.map((String relativePath) =>
fileSystem.path.join(framework.cacheDirectory, relativePath))
.toList();
}
/// Binaries that are only expected to be codesigned.
@ -178,7 +188,10 @@ class CodesignCommand extends Command<void> {
'artifacts/engine/ios/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/Flutter',
'artifacts/engine/ios/Flutter.xcframework/ios-x86_64-simulator/Flutter.framework/Flutter',
'artifacts/ios-deploy/ios-deploy',
].map((String relativePath) => fileSystem.path.join(framework.cacheDirectory, relativePath)).toList();
]
.map((String relativePath) =>
fileSystem.path.join(framework.cacheDirectory, relativePath))
.toList();
}
/// Verify the existence of all expected binaries in cache.
@ -197,19 +210,27 @@ class CodesignCommand extends Command<void> {
} else if (binariesWithoutEntitlements.contains(binaryPath)) {
foundFiles.add(binaryPath);
} else {
throw ConductorException('Found unexpected binary in cache: $binaryPath');
throw ConductorException(
'Found unexpected binary in cache: $binaryPath');
}
}
final List<String> allExpectedFiles = binariesWithEntitlements + binariesWithoutEntitlements;
final List<String> allExpectedFiles =
binariesWithEntitlements + binariesWithoutEntitlements;
if (foundFiles.length < allExpectedFiles.length) {
final List<String> unfoundFiles = allExpectedFiles.where(
(String file) => !foundFiles.contains(file),
).toList();
stdio.printError('Expected binaries not found in cache:\n\n${unfoundFiles.join('\n')}\n');
stdio.printError('If this commit is removing binaries from the cache, this test should be fixed by');
stdio.printError('removing the relevant entry from either the `binariesWithEntitlements` or');
stdio.printError('`binariesWithoutEntitlements` getters in dev/tools/lib/codesign.dart.');
final List<String> unfoundFiles = allExpectedFiles
.where(
(String file) => !foundFiles.contains(file),
)
.toList();
stdio.printError(
'Expected binaries not found in cache:\n\n${unfoundFiles.join('\n')}\n');
stdio.printError(
'If this commit is removing binaries from the cache, this test should be fixed by');
stdio.printError(
'removing the relevant entry from either the `binariesWithEntitlements` or');
stdio.printError(
'`binariesWithoutEntitlements` getters in dev/tools/lib/codesign.dart.');
throw ConductorException('Did not find all expected binaries!');
}
@ -275,13 +296,15 @@ class CodesignCommand extends Command<void> {
}
if (unexpectedBinaries.isNotEmpty) {
stdio.printError('Found ${unexpectedBinaries.length} unexpected binaries in the cache:');
stdio.printError(
'Found ${unexpectedBinaries.length} unexpected binaries in the cache:');
unexpectedBinaries.forEach(print);
}
// Finally, exit on any invalid state
if (unsignedBinaries.isNotEmpty) {
throw ConductorException('Test failed because unsigned binaries detected.');
throw ConductorException(
'Test failed because unsigned binaries detected.');
}
if (wrongEntitlementBinaries.isNotEmpty) {
@ -291,7 +314,8 @@ class CodesignCommand extends Command<void> {
}
if (unexpectedBinaries.isNotEmpty) {
throw ConductorException('Test failed because unexpected binaries found in the cache.');
throw ConductorException(
'Test failed because unexpected binaries found in the cache.');
}
stdio.printStatus(
@ -300,6 +324,7 @@ class CodesignCommand extends Command<void> {
}
List<String> _allBinaryPaths;
/// Find every binary file in the given [rootDirectory].
List<String> findBinaryPaths(String rootDirectory) {
if (_allBinaryPaths != null) {
@ -356,7 +381,8 @@ class CodesignCommand extends Command<void> {
bool passes = true;
final String output = entitlementResult.stdout as String;
for (final String entitlement in expectedEntitlements) {
final bool entitlementExpected = binariesWithEntitlements.contains(binaryPath);
final bool entitlementExpected =
binariesWithEntitlements.contains(binaryPath);
if (output.contains(entitlement) != entitlementExpected) {
stdio.printError(
'File "$binaryPath" ${entitlementExpected ? 'does not have expected' : 'has unexpected'} '

View file

@ -19,6 +19,7 @@ class Git {
List<String> args,
String explanation, {
@required String workingDirectory,
bool allowFailures = false,
}) {
final ProcessResult result = _run(args, workingDirectory);
if (result.exitCode == 0) {
@ -68,6 +69,15 @@ class Git {
message.writeln('stdout from git:\n${result.stdout}\n');
if ((result.stderr as String).isNotEmpty)
message.writeln('stderr from git:\n${result.stderr}\n');
throw Exception(message);
throw GitException(message.toString());
}
}
class GitException implements Exception {
GitException(this.message);
final String message;
@override
String toString() => 'Exception: $message';
}

View file

@ -2,20 +2,15 @@
// 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';
const String kIncrement = 'increment';
const String kCommit = 'commit';
const String kRemoteName = 'remote';
const String kJustPrint = 'just-print';
const String kYes = 'yes';
const String kForce = 'force';
const String kSkipTagging = 'skip-tagging';
const String kUpstreamRemote = 'https://github.com/flutter/flutter.git';
const String gsutilBinary = 'gsutil.py';
const List<String> kReleaseChannels = <String>[
'stable',
'beta',
@ -23,6 +18,12 @@ const List<String> kReleaseChannels = <String>[
'master',
];
const String kReleaseDocumentationUrl = 'https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process';
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;
@ -86,3 +87,64 @@ bool assertsEnabled() {
}());
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,
) {
final String envName = fromArgToEnvName(name);
if (env[envName] != null ) {
return env[envName];
}
final String argValue = argResults[name] as String;
if (argValue != null) {
return argValue;
}
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'_');
}

View file

@ -0,0 +1,8 @@
## Flutter Conductor Protocol Buffers
This directory contains [conductor_state.proto](./conductor_state.proto), which
defines the persistent state file the conductor creates. After changes to this
file, you must run the [compile_proto.sh](./compile_proto.sh) script in this
directory, which will re-generate the rest of the Dart files in this directory,
format them, and prepend the license comment from
[license_header.txt](./license_header.txt).

View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# 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.
# //flutter/dev/tools/lib/proto
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
DARTFMT="$DIR/../../../../bin/cache/dart-sdk/bin/dartfmt"
# Ensure dart-sdk is cached
"$DIR/../../../../bin/dart" --version
if ! type protoc >/dev/null 2>&1; then
PROTOC_LINK='https://grpc.io/docs/protoc-installation/'
echo "Error! \"protoc\" binary required on path."
echo "See $PROTOC_LINK for more information."
exit 1
fi
if ! type dart >/dev/null 2>&1; then
echo "Error! \"dart\" binary required on path."
exit 1
fi
# Pin protoc-gen-dart to pre-nullsafe version.
dart pub global activate protoc_plugin 19.3.1
protoc --dart_out="$DIR" --proto_path="$DIR" "$DIR/conductor_state.proto"
for SOURCE_FILE in $(ls "$DIR"/*.pb*.dart); do
# Format in place file
"$DARTFMT" --overwrite --line-length 120 "$SOURCE_FILE"
# Create temp copy with the license header prepended
cp license_header.txt "${SOURCE_FILE}.tmp"
# Add an extra newline required by analysis (analysis also prevents
# license_header.txt from having the trailing newline)
echo '' >> "${SOURCE_FILE}.tmp"
cat "$SOURCE_FILE" >> "${SOURCE_FILE}.tmp"
# Move temp version (with license) over the original
mv "${SOURCE_FILE}.tmp" "$SOURCE_FILE"
done

View file

@ -0,0 +1,538 @@
// 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.
///
// Generated code. Do not modify.
// source: conductor_state.proto
//
// @dart = 2.7
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb;
import 'conductor_state.pbenum.dart';
export 'conductor_state.pbenum.dart';
class Remote extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Remote',
package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'conductor_state'),
createEmptyInstance: create)
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'name')
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'url')
..hasRequiredFields = false;
Remote._() : super();
factory Remote({
$core.String name,
$core.String url,
}) {
final _result = create();
if (name != null) {
_result.name = name;
}
if (url != null) {
_result.url = url;
}
return _result;
}
factory Remote.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r);
factory Remote.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(i, r);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
Remote clone() => Remote()..mergeFromMessage(this);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
Remote copyWith(void Function(Remote) updates) =>
super.copyWith((message) => updates(message as Remote)) as Remote; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Remote create() => Remote._();
Remote createEmptyInstance() => create();
static $pb.PbList<Remote> createRepeated() => $pb.PbList<Remote>();
@$core.pragma('dart2js:noInline')
static Remote getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Remote>(create);
static Remote _defaultInstance;
@$pb.TagNumber(1)
$core.String get name => $_getSZ(0);
@$pb.TagNumber(1)
set name($core.String v) {
$_setString(0, v);
}
@$pb.TagNumber(1)
$core.bool hasName() => $_has(0);
@$pb.TagNumber(1)
void clearName() => clearField(1);
@$pb.TagNumber(2)
$core.String get url => $_getSZ(1);
@$pb.TagNumber(2)
set url($core.String v) {
$_setString(1, v);
}
@$pb.TagNumber(2)
$core.bool hasUrl() => $_has(1);
@$pb.TagNumber(2)
void clearUrl() => clearField(2);
}
class Cherrypick extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Cherrypick',
package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'conductor_state'),
createEmptyInstance: create)
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'trunkRevision',
protoName: 'trunkRevision')
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'appliedRevision',
protoName: 'appliedRevision')
..e<CherrypickState>(
3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'state', $pb.PbFieldType.OE,
defaultOrMaker: CherrypickState.PENDING, valueOf: CherrypickState.valueOf, enumValues: CherrypickState.values)
..hasRequiredFields = false;
Cherrypick._() : super();
factory Cherrypick({
$core.String trunkRevision,
$core.String appliedRevision,
CherrypickState state,
}) {
final _result = create();
if (trunkRevision != null) {
_result.trunkRevision = trunkRevision;
}
if (appliedRevision != null) {
_result.appliedRevision = appliedRevision;
}
if (state != null) {
_result.state = state;
}
return _result;
}
factory Cherrypick.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r);
factory Cherrypick.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(i, r);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
Cherrypick clone() => Cherrypick()..mergeFromMessage(this);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
Cherrypick copyWith(void Function(Cherrypick) updates) =>
super.copyWith((message) => updates(message as Cherrypick)) as Cherrypick; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Cherrypick create() => Cherrypick._();
Cherrypick createEmptyInstance() => create();
static $pb.PbList<Cherrypick> createRepeated() => $pb.PbList<Cherrypick>();
@$core.pragma('dart2js:noInline')
static Cherrypick getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Cherrypick>(create);
static Cherrypick _defaultInstance;
@$pb.TagNumber(1)
$core.String get trunkRevision => $_getSZ(0);
@$pb.TagNumber(1)
set trunkRevision($core.String v) {
$_setString(0, v);
}
@$pb.TagNumber(1)
$core.bool hasTrunkRevision() => $_has(0);
@$pb.TagNumber(1)
void clearTrunkRevision() => clearField(1);
@$pb.TagNumber(2)
$core.String get appliedRevision => $_getSZ(1);
@$pb.TagNumber(2)
set appliedRevision($core.String v) {
$_setString(1, v);
}
@$pb.TagNumber(2)
$core.bool hasAppliedRevision() => $_has(1);
@$pb.TagNumber(2)
void clearAppliedRevision() => clearField(2);
@$pb.TagNumber(3)
CherrypickState get state => $_getN(2);
@$pb.TagNumber(3)
set state(CherrypickState v) {
setField(3, v);
}
@$pb.TagNumber(3)
$core.bool hasState() => $_has(2);
@$pb.TagNumber(3)
void clearState() => clearField(3);
}
class Repository extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Repository',
package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'conductor_state'),
createEmptyInstance: create)
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'candidateBranch',
protoName: 'candidateBranch')
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'startingGitHead',
protoName: 'startingGitHead')
..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'currentGitHead',
protoName: 'currentGitHead')
..aOS(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'checkoutPath',
protoName: 'checkoutPath')
..aOM<Remote>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'upstream',
subBuilder: Remote.create)
..aOM<Remote>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mirror',
subBuilder: Remote.create)
..pc<Cherrypick>(
7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'cherrypicks', $pb.PbFieldType.PM,
subBuilder: Cherrypick.create)
..hasRequiredFields = false;
Repository._() : super();
factory Repository({
$core.String candidateBranch,
$core.String startingGitHead,
$core.String currentGitHead,
$core.String checkoutPath,
Remote upstream,
Remote mirror,
$core.Iterable<Cherrypick> cherrypicks,
}) {
final _result = create();
if (candidateBranch != null) {
_result.candidateBranch = candidateBranch;
}
if (startingGitHead != null) {
_result.startingGitHead = startingGitHead;
}
if (currentGitHead != null) {
_result.currentGitHead = currentGitHead;
}
if (checkoutPath != null) {
_result.checkoutPath = checkoutPath;
}
if (upstream != null) {
_result.upstream = upstream;
}
if (mirror != null) {
_result.mirror = mirror;
}
if (cherrypicks != null) {
_result.cherrypicks.addAll(cherrypicks);
}
return _result;
}
factory Repository.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r);
factory Repository.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(i, r);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
Repository clone() => Repository()..mergeFromMessage(this);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
Repository copyWith(void Function(Repository) updates) =>
super.copyWith((message) => updates(message as Repository)) as Repository; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Repository create() => Repository._();
Repository createEmptyInstance() => create();
static $pb.PbList<Repository> createRepeated() => $pb.PbList<Repository>();
@$core.pragma('dart2js:noInline')
static Repository getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Repository>(create);
static Repository _defaultInstance;
@$pb.TagNumber(1)
$core.String get candidateBranch => $_getSZ(0);
@$pb.TagNumber(1)
set candidateBranch($core.String v) {
$_setString(0, v);
}
@$pb.TagNumber(1)
$core.bool hasCandidateBranch() => $_has(0);
@$pb.TagNumber(1)
void clearCandidateBranch() => clearField(1);
@$pb.TagNumber(2)
$core.String get startingGitHead => $_getSZ(1);
@$pb.TagNumber(2)
set startingGitHead($core.String v) {
$_setString(1, v);
}
@$pb.TagNumber(2)
$core.bool hasStartingGitHead() => $_has(1);
@$pb.TagNumber(2)
void clearStartingGitHead() => clearField(2);
@$pb.TagNumber(3)
$core.String get currentGitHead => $_getSZ(2);
@$pb.TagNumber(3)
set currentGitHead($core.String v) {
$_setString(2, v);
}
@$pb.TagNumber(3)
$core.bool hasCurrentGitHead() => $_has(2);
@$pb.TagNumber(3)
void clearCurrentGitHead() => clearField(3);
@$pb.TagNumber(4)
$core.String get checkoutPath => $_getSZ(3);
@$pb.TagNumber(4)
set checkoutPath($core.String v) {
$_setString(3, v);
}
@$pb.TagNumber(4)
$core.bool hasCheckoutPath() => $_has(3);
@$pb.TagNumber(4)
void clearCheckoutPath() => clearField(4);
@$pb.TagNumber(5)
Remote get upstream => $_getN(4);
@$pb.TagNumber(5)
set upstream(Remote v) {
setField(5, v);
}
@$pb.TagNumber(5)
$core.bool hasUpstream() => $_has(4);
@$pb.TagNumber(5)
void clearUpstream() => clearField(5);
@$pb.TagNumber(5)
Remote ensureUpstream() => $_ensure(4);
@$pb.TagNumber(6)
Remote get mirror => $_getN(5);
@$pb.TagNumber(6)
set mirror(Remote v) {
setField(6, v);
}
@$pb.TagNumber(6)
$core.bool hasMirror() => $_has(5);
@$pb.TagNumber(6)
void clearMirror() => clearField(6);
@$pb.TagNumber(6)
Remote ensureMirror() => $_ensure(5);
@$pb.TagNumber(7)
$core.List<Cherrypick> get cherrypicks => $_getList(6);
}
class ConductorState extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ConductorState',
package: const $pb.PackageName(
const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'conductor_state'),
createEmptyInstance: create)
..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'releaseChannel',
protoName: 'releaseChannel')
..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'releaseVersion',
protoName: 'releaseVersion')
..aOM<Repository>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'engine',
subBuilder: Repository.create)
..aOM<Repository>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'framework',
subBuilder: Repository.create)
..aInt64(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'createdDate',
protoName: 'createdDate')
..aInt64(7, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'lastUpdatedDate',
protoName: 'lastUpdatedDate')
..pPS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'logs')
..e<ReleasePhase>(
9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'lastPhase', $pb.PbFieldType.OE,
protoName: 'lastPhase',
defaultOrMaker: ReleasePhase.INITIALIZE,
valueOf: ReleasePhase.valueOf,
enumValues: ReleasePhase.values)
..aOS(10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'conductorVersion')
..hasRequiredFields = false;
ConductorState._() : super();
factory ConductorState({
$core.String releaseChannel,
$core.String releaseVersion,
Repository engine,
Repository framework,
$fixnum.Int64 createdDate,
$fixnum.Int64 lastUpdatedDate,
$core.Iterable<$core.String> logs,
ReleasePhase lastPhase,
$core.String conductorVersion,
}) {
final _result = create();
if (releaseChannel != null) {
_result.releaseChannel = releaseChannel;
}
if (releaseVersion != null) {
_result.releaseVersion = releaseVersion;
}
if (engine != null) {
_result.engine = engine;
}
if (framework != null) {
_result.framework = framework;
}
if (createdDate != null) {
_result.createdDate = createdDate;
}
if (lastUpdatedDate != null) {
_result.lastUpdatedDate = lastUpdatedDate;
}
if (logs != null) {
_result.logs.addAll(logs);
}
if (lastPhase != null) {
_result.lastPhase = lastPhase;
}
if (conductorVersion != null) {
_result.conductorVersion = conductorVersion;
}
return _result;
}
factory ConductorState.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(i, r);
factory ConductorState.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(i, r);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
ConductorState clone() => ConductorState()..mergeFromMessage(this);
@$core.Deprecated('Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
ConductorState copyWith(void Function(ConductorState) updates) =>
super.copyWith((message) => updates(message as ConductorState))
as ConductorState; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ConductorState create() => ConductorState._();
ConductorState createEmptyInstance() => create();
static $pb.PbList<ConductorState> createRepeated() => $pb.PbList<ConductorState>();
@$core.pragma('dart2js:noInline')
static ConductorState getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ConductorState>(create);
static ConductorState _defaultInstance;
@$pb.TagNumber(1)
$core.String get releaseChannel => $_getSZ(0);
@$pb.TagNumber(1)
set releaseChannel($core.String v) {
$_setString(0, v);
}
@$pb.TagNumber(1)
$core.bool hasReleaseChannel() => $_has(0);
@$pb.TagNumber(1)
void clearReleaseChannel() => clearField(1);
@$pb.TagNumber(2)
$core.String get releaseVersion => $_getSZ(1);
@$pb.TagNumber(2)
set releaseVersion($core.String v) {
$_setString(1, v);
}
@$pb.TagNumber(2)
$core.bool hasReleaseVersion() => $_has(1);
@$pb.TagNumber(2)
void clearReleaseVersion() => clearField(2);
@$pb.TagNumber(4)
Repository get engine => $_getN(2);
@$pb.TagNumber(4)
set engine(Repository v) {
setField(4, v);
}
@$pb.TagNumber(4)
$core.bool hasEngine() => $_has(2);
@$pb.TagNumber(4)
void clearEngine() => clearField(4);
@$pb.TagNumber(4)
Repository ensureEngine() => $_ensure(2);
@$pb.TagNumber(5)
Repository get framework => $_getN(3);
@$pb.TagNumber(5)
set framework(Repository v) {
setField(5, v);
}
@$pb.TagNumber(5)
$core.bool hasFramework() => $_has(3);
@$pb.TagNumber(5)
void clearFramework() => clearField(5);
@$pb.TagNumber(5)
Repository ensureFramework() => $_ensure(3);
@$pb.TagNumber(6)
$fixnum.Int64 get createdDate => $_getI64(4);
@$pb.TagNumber(6)
set createdDate($fixnum.Int64 v) {
$_setInt64(4, v);
}
@$pb.TagNumber(6)
$core.bool hasCreatedDate() => $_has(4);
@$pb.TagNumber(6)
void clearCreatedDate() => clearField(6);
@$pb.TagNumber(7)
$fixnum.Int64 get lastUpdatedDate => $_getI64(5);
@$pb.TagNumber(7)
set lastUpdatedDate($fixnum.Int64 v) {
$_setInt64(5, v);
}
@$pb.TagNumber(7)
$core.bool hasLastUpdatedDate() => $_has(5);
@$pb.TagNumber(7)
void clearLastUpdatedDate() => clearField(7);
@$pb.TagNumber(8)
$core.List<$core.String> get logs => $_getList(6);
@$pb.TagNumber(9)
ReleasePhase get lastPhase => $_getN(7);
@$pb.TagNumber(9)
set lastPhase(ReleasePhase v) {
setField(9, v);
}
@$pb.TagNumber(9)
$core.bool hasLastPhase() => $_has(7);
@$pb.TagNumber(9)
void clearLastPhase() => clearField(9);
@$pb.TagNumber(10)
$core.String get conductorVersion => $_getSZ(8);
@$pb.TagNumber(10)
set conductorVersion($core.String v) {
$_setString(8, v);
}
@$pb.TagNumber(10)
$core.bool hasConductorVersion() => $_has(8);
@$pb.TagNumber(10)
void clearConductorVersion() => clearField(10);
}

View file

@ -0,0 +1,69 @@
// 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.
///
// Generated code. Do not modify.
// source: conductor_state.proto
//
// @dart = 2.7
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields
// ignore_for_file: UNDEFINED_SHOWN_NAME
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class ReleasePhase extends $pb.ProtobufEnum {
static const ReleasePhase INITIALIZE =
ReleasePhase._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'INITIALIZE');
static const ReleasePhase APPLY_ENGINE_CHERRYPICKS =
ReleasePhase._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'APPLY_ENGINE_CHERRYPICKS');
static const ReleasePhase CODESIGN_ENGINE_BINARIES =
ReleasePhase._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CODESIGN_ENGINE_BINARIES');
static const ReleasePhase APPLY_FRAMEWORK_CHERRYPICKS = ReleasePhase._(
3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'APPLY_FRAMEWORK_CHERRYPICKS');
static const ReleasePhase PUBLISH_VERSION =
ReleasePhase._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PUBLISH_VERSION');
static const ReleasePhase PUBLISH_CHANNEL =
ReleasePhase._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PUBLISH_CHANNEL');
static const ReleasePhase VERIFY_RELEASE =
ReleasePhase._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'VERIFY_RELEASE');
static const $core.List<ReleasePhase> values = <ReleasePhase>[
INITIALIZE,
APPLY_ENGINE_CHERRYPICKS,
CODESIGN_ENGINE_BINARIES,
APPLY_FRAMEWORK_CHERRYPICKS,
PUBLISH_VERSION,
PUBLISH_CHANNEL,
VERIFY_RELEASE,
];
static final $core.Map<$core.int, ReleasePhase> _byValue = $pb.ProtobufEnum.initByValue(values);
static ReleasePhase valueOf($core.int value) => _byValue[value];
const ReleasePhase._($core.int v, $core.String n) : super(v, n);
}
class CherrypickState extends $pb.ProtobufEnum {
static const CherrypickState PENDING =
CherrypickState._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PENDING');
static const CherrypickState PENDING_WITH_CONFLICT =
CherrypickState._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PENDING_WITH_CONFLICT');
static const CherrypickState COMPLETED =
CherrypickState._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'COMPLETED');
static const CherrypickState ABANDONED =
CherrypickState._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ABANDONED');
static const $core.List<CherrypickState> values = <CherrypickState>[
PENDING,
PENDING_WITH_CONFLICT,
COMPLETED,
ABANDONED,
];
static final $core.Map<$core.int, CherrypickState> _byValue = $pb.ProtobufEnum.initByValue(values);
static CherrypickState valueOf($core.int value) => _byValue[value];
const CherrypickState._($core.int v, $core.String n) : super(v, n);
}

View file

@ -0,0 +1,107 @@
// 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.
///
// Generated code. Do not modify.
// source: conductor_state.proto
//
// @dart = 2.7
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
import 'dart:core' as $core;
import 'dart:convert' as $convert;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use releasePhaseDescriptor instead')
const ReleasePhase$json = const {
'1': 'ReleasePhase',
'2': const [
const {'1': 'INITIALIZE', '2': 0},
const {'1': 'APPLY_ENGINE_CHERRYPICKS', '2': 1},
const {'1': 'CODESIGN_ENGINE_BINARIES', '2': 2},
const {'1': 'APPLY_FRAMEWORK_CHERRYPICKS', '2': 3},
const {'1': 'PUBLISH_VERSION', '2': 4},
const {'1': 'PUBLISH_CHANNEL', '2': 5},
const {'1': 'VERIFY_RELEASE', '2': 6},
],
};
/// Descriptor for `ReleasePhase`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List releasePhaseDescriptor = $convert.base64Decode(
'CgxSZWxlYXNlUGhhc2USDgoKSU5JVElBTElaRRAAEhwKGEFQUExZX0VOR0lORV9DSEVSUllQSUNLUxABEhwKGENPREVTSUdOX0VOR0lORV9CSU5BUklFUxACEh8KG0FQUExZX0ZSQU1FV09SS19DSEVSUllQSUNLUxADEhMKD1BVQkxJU0hfVkVSU0lPThAEEhMKD1BVQkxJU0hfQ0hBTk5FTBAFEhIKDlZFUklGWV9SRUxFQVNFEAY=');
@$core.Deprecated('Use cherrypickStateDescriptor instead')
const CherrypickState$json = const {
'1': 'CherrypickState',
'2': const [
const {'1': 'PENDING', '2': 0},
const {'1': 'PENDING_WITH_CONFLICT', '2': 1},
const {'1': 'COMPLETED', '2': 2},
const {'1': 'ABANDONED', '2': 3},
],
};
/// Descriptor for `CherrypickState`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List cherrypickStateDescriptor = $convert.base64Decode(
'Cg9DaGVycnlwaWNrU3RhdGUSCwoHUEVORElORxAAEhkKFVBFTkRJTkdfV0lUSF9DT05GTElDVBABEg0KCUNPTVBMRVRFRBACEg0KCUFCQU5ET05FRBAD');
@$core.Deprecated('Use remoteDescriptor instead')
const Remote$json = const {
'1': 'Remote',
'2': const [
const {'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
const {'1': 'url', '3': 2, '4': 1, '5': 9, '10': 'url'},
],
};
/// Descriptor for `Remote`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List remoteDescriptor =
$convert.base64Decode('CgZSZW1vdGUSEgoEbmFtZRgBIAEoCVIEbmFtZRIQCgN1cmwYAiABKAlSA3VybA==');
@$core.Deprecated('Use cherrypickDescriptor instead')
const Cherrypick$json = const {
'1': 'Cherrypick',
'2': const [
const {'1': 'trunkRevision', '3': 1, '4': 1, '5': 9, '10': 'trunkRevision'},
const {'1': 'appliedRevision', '3': 2, '4': 1, '5': 9, '10': 'appliedRevision'},
const {'1': 'state', '3': 3, '4': 1, '5': 14, '6': '.conductor_state.CherrypickState', '10': 'state'},
],
};
/// Descriptor for `Cherrypick`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List cherrypickDescriptor = $convert.base64Decode(
'CgpDaGVycnlwaWNrEiQKDXRydW5rUmV2aXNpb24YASABKAlSDXRydW5rUmV2aXNpb24SKAoPYXBwbGllZFJldmlzaW9uGAIgASgJUg9hcHBsaWVkUmV2aXNpb24SNgoFc3RhdGUYAyABKA4yIC5jb25kdWN0b3Jfc3RhdGUuQ2hlcnJ5cGlja1N0YXRlUgVzdGF0ZQ==');
@$core.Deprecated('Use repositoryDescriptor instead')
const Repository$json = const {
'1': 'Repository',
'2': const [
const {'1': 'candidateBranch', '3': 1, '4': 1, '5': 9, '10': 'candidateBranch'},
const {'1': 'startingGitHead', '3': 2, '4': 1, '5': 9, '10': 'startingGitHead'},
const {'1': 'currentGitHead', '3': 3, '4': 1, '5': 9, '10': 'currentGitHead'},
const {'1': 'checkoutPath', '3': 4, '4': 1, '5': 9, '10': 'checkoutPath'},
const {'1': 'upstream', '3': 5, '4': 1, '5': 11, '6': '.conductor_state.Remote', '10': 'upstream'},
const {'1': 'mirror', '3': 6, '4': 1, '5': 11, '6': '.conductor_state.Remote', '10': 'mirror'},
const {'1': 'cherrypicks', '3': 7, '4': 3, '5': 11, '6': '.conductor_state.Cherrypick', '10': 'cherrypicks'},
],
};
/// Descriptor for `Repository`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List repositoryDescriptor = $convert.base64Decode(
'CgpSZXBvc2l0b3J5EigKD2NhbmRpZGF0ZUJyYW5jaBgBIAEoCVIPY2FuZGlkYXRlQnJhbmNoEigKD3N0YXJ0aW5nR2l0SGVhZBgCIAEoCVIPc3RhcnRpbmdHaXRIZWFkEiYKDmN1cnJlbnRHaXRIZWFkGAMgASgJUg5jdXJyZW50R2l0SGVhZBIiCgxjaGVja291dFBhdGgYBCABKAlSDGNoZWNrb3V0UGF0aBIzCgh1cHN0cmVhbRgFIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSCHVwc3RyZWFtEi8KBm1pcnJvchgGIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSBm1pcnJvchI9CgtjaGVycnlwaWNrcxgHIAMoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5DaGVycnlwaWNrUgtjaGVycnlwaWNrcw==');
@$core.Deprecated('Use conductorStateDescriptor instead')
const ConductorState$json = const {
'1': 'ConductorState',
'2': const [
const {'1': 'releaseChannel', '3': 1, '4': 1, '5': 9, '10': 'releaseChannel'},
const {'1': 'releaseVersion', '3': 2, '4': 1, '5': 9, '10': 'releaseVersion'},
const {'1': 'engine', '3': 4, '4': 1, '5': 11, '6': '.conductor_state.Repository', '10': 'engine'},
const {'1': 'framework', '3': 5, '4': 1, '5': 11, '6': '.conductor_state.Repository', '10': 'framework'},
const {'1': 'createdDate', '3': 6, '4': 1, '5': 3, '10': 'createdDate'},
const {'1': 'lastUpdatedDate', '3': 7, '4': 1, '5': 3, '10': 'lastUpdatedDate'},
const {'1': 'logs', '3': 8, '4': 3, '5': 9, '10': 'logs'},
const {'1': 'lastPhase', '3': 9, '4': 1, '5': 14, '6': '.conductor_state.ReleasePhase', '10': 'lastPhase'},
const {'1': 'conductor_version', '3': 10, '4': 1, '5': 9, '10': 'conductorVersion'},
],
};
/// Descriptor for `ConductorState`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List conductorStateDescriptor = $convert.base64Decode(
'Cg5Db25kdWN0b3JTdGF0ZRImCg5yZWxlYXNlQ2hhbm5lbBgBIAEoCVIOcmVsZWFzZUNoYW5uZWwSJgoOcmVsZWFzZVZlcnNpb24YAiABKAlSDnJlbGVhc2VWZXJzaW9uEjMKBmVuZ2luZRgEIAEoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5SZXBvc2l0b3J5UgZlbmdpbmUSOQoJZnJhbWV3b3JrGAUgASgLMhsuY29uZHVjdG9yX3N0YXRlLlJlcG9zaXRvcnlSCWZyYW1ld29yaxIgCgtjcmVhdGVkRGF0ZRgGIAEoA1ILY3JlYXRlZERhdGUSKAoPbGFzdFVwZGF0ZWREYXRlGAcgASgDUg9sYXN0VXBkYXRlZERhdGUSEgoEbG9ncxgIIAMoCVIEbG9ncxI7CglsYXN0UGhhc2UYCSABKA4yHS5jb25kdWN0b3Jfc3RhdGUuUmVsZWFzZVBoYXNlUglsYXN0UGhhc2USKwoRY29uZHVjdG9yX3ZlcnNpb24YCiABKAlSEGNvbmR1Y3RvclZlcnNpb24=');

View file

@ -0,0 +1,12 @@
// 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.
///
// Generated code. Do not modify.
// source: conductor_state.proto
//
// @dart = 2.7
// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package
export 'conductor_state.pb.dart';

View file

@ -0,0 +1,103 @@
syntax = "proto3";
package conductor_state;
// A git remote
message Remote {
string name = 1;
string url = 2;
}
enum ReleasePhase {
// Release was started with `conductor start` and repositories cloned.
INITIALIZE = 0;
APPLY_ENGINE_CHERRYPICKS = 1;
CODESIGN_ENGINE_BINARIES = 2;
APPLY_FRAMEWORK_CHERRYPICKS = 3;
// Git tag applied to framework RC branch HEAD and pushed upstream.
PUBLISH_VERSION = 4;
// RC branch HEAD pushed to upstream release branch.
//
// For example, flutter-1.2-candidate.3 -> upstream/beta
PUBLISH_CHANNEL = 5;
// Package artifacts verified to exist on cloud storage.
VERIFY_RELEASE = 6;
}
enum CherrypickState {
// The cherrypick has not yet been applied.
PENDING = 0;
// The cherrypick has not been applied and will require manual resolution.
PENDING_WITH_CONFLICT = 1;
// The cherrypick has been successfully applied to the local checkout.
//
// This state requires Cherrypick.appliedRevision to also be set.
COMPLETED = 2;
// The cherrypick will NOT be applied in this release.
ABANDONED = 3;
}
message Cherrypick {
// The revision on trunk to cherrypick.
string trunkRevision = 1;
// Once applied, the actual commit revision of the cherrypick.
string appliedRevision = 2;
CherrypickState state = 3;
}
message Repository {
// The development git branch the release is based on.
//
// Must be of the form /flutter-(\d+)\.(\d+)-candidate\.(\d+)/
string candidateBranch = 1;
// The commit hash at the tip of the branch before cherrypicks were applied.
string startingGitHead = 2;
// The difference in commits between this and [startingGitHead] is the number
// of cherrypicks that have been currently applied.
string currentGitHead = 3;
// Path to the git checkout on local disk.
string checkoutPath = 4;
// The remote commits will be fetched from.
Remote upstream = 5;
// The remote cherrypicks will be pushed to to create a Pull Request.
//
// This should be a mirror owned by the user conducting the release.
Remote mirror = 6;
// Desired cherrypicks.
repeated Cherrypick cherrypicks = 7;
}
message ConductorState {
// One of 'stable', 'beta', or 'dev'
string releaseChannel = 1;
// The name of the release.
string releaseVersion = 2;
Repository engine = 4;
Repository framework = 5;
int64 createdDate = 6;
int64 lastUpdatedDate = 7;
repeated string logs = 8;
// The last [ReleasePhase] that was successfully completed.
ReleasePhase lastPhase = 9;
// Commit hash of the Conductor tool.
string conductor_version = 10;
}

View file

@ -0,0 +1,3 @@
// 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.

View file

@ -11,28 +11,68 @@ import 'package:process/process.dart';
import 'package:platform/platform.dart';
import './git.dart';
import './globals.dart' as globals;
import './globals.dart';
import './stdio.dart';
import './version.dart';
/// Allowed git remote names.
enum RemoteName {
upstream,
mirror,
}
class Remote {
const Remote({
@required RemoteName name,
@required this.url,
}) : _name = name;
final RemoteName _name;
/// The name of the remote.
String get name {
switch (_name) {
case RemoteName.upstream:
return 'upstream';
case RemoteName.mirror:
return 'mirror';
}
throw ConductorException('Invalid value of _name: $_name'); // For analyzer
}
/// The URL of the remote.
final String url;
}
/// A source code repository.
abstract class Repository {
Repository({
@required this.name,
@required this.upstream,
@required this.fetchRemote,
@required this.processManager,
@required this.stdio,
@required this.platform,
@required this.fileSystem,
@required this.parentDirectory,
this.initialRef,
this.localUpstream = false,
this.useExistingCheckout = false,
this.pushRemote,
}) : git = Git(processManager),
assert(localUpstream != null),
assert(useExistingCheckout != null);
final String name;
final String upstream;
final Remote fetchRemote;
/// Remote to publish tags and commits to.
///
/// This value can be null, in which case attempting to publish will lead to
/// a [ConductorException].
final Remote pushRemote;
/// The initial ref (branch or commit name) to check out.
final String initialRef;
final Git git;
final ProcessManager processManager;
final Stdio stdio;
@ -55,33 +95,49 @@ abstract class Repository {
return _checkoutDirectory;
}
_checkoutDirectory = parentDirectory.childDirectory(name);
lazilyInitialize();
return _checkoutDirectory;
}
/// Ensure the repository is cloned to disk and initialized with proper state.
void lazilyInitialize() {
if (!useExistingCheckout && _checkoutDirectory.existsSync()) {
stdio.printTrace('Deleting $name from ${_checkoutDirectory.path}...');
_checkoutDirectory.deleteSync(recursive: true);
} else if (useExistingCheckout && _checkoutDirectory.existsSync()) {
git.run(
<String>['checkout', 'master'],
'Checkout to master branch',
workingDirectory: _checkoutDirectory.path,
);
git.run(
<String>['pull', '--ff-only'],
'Updating $name repo',
workingDirectory: _checkoutDirectory.path,
);
}
if (!_checkoutDirectory.existsSync()) {
stdio.printTrace(
'Cloning $name from $upstream to ${_checkoutDirectory.path}...');
'Cloning $name from ${fetchRemote.url} to ${_checkoutDirectory.path}...',
);
git.run(
<String>['clone', '--', upstream, _checkoutDirectory.path],
<String>[
'clone',
'--origin',
fetchRemote.name,
'--',
fetchRemote.url,
_checkoutDirectory.path
],
'Cloning $name repo',
workingDirectory: parentDirectory.path,
);
if (pushRemote != null) {
git.run(
<String>['remote', 'add', pushRemote.name, pushRemote.url],
'Adding remote ${pushRemote.url} as ${pushRemote.name}',
workingDirectory: _checkoutDirectory.path,
);
git.run(
<String>['fetch', pushRemote.name],
'Fetching git remote ${pushRemote.name}',
workingDirectory: _checkoutDirectory.path,
);
}
if (localUpstream) {
// These branches must exist locally for the repo that depends on it
// to fetch and push to.
for (final String channel in globals.kReleaseChannels) {
for (final String channel in kReleaseChannels) {
git.run(
<String>['checkout', channel, '--'],
'check out branch $channel locally',
@ -91,10 +147,17 @@ abstract class Repository {
}
}
if (initialRef != null) {
git.run(
<String>['checkout', '${fetchRemote.name}/$initialRef'],
'Checking out initialRef $initialRef',
workingDirectory: _checkoutDirectory.path,
);
}
final String revision = reverseParse('HEAD');
stdio
.printTrace('Repository $name is checked out at revision "$revision".');
return _checkoutDirectory;
stdio.printTrace(
'Repository $name is checked out at revision "$revision".',
);
}
/// The URL of the remote named [remoteName].
@ -117,6 +180,15 @@ abstract class Repository {
return output == '';
}
/// Return the revision for the branch point between two refs.
String branchPoint(String firstRef, String secondRef) {
return git.getOutput(
<String>['merge-base', firstRef, secondRef],
'determine the merge base between $firstRef and $secondRef',
workingDirectory: checkoutDirectory.path,
).trim();
}
/// Fetch all branches and associated commits and tags from [remoteName].
void fetch(String remoteName) {
git.run(
@ -126,10 +198,22 @@ abstract class Repository {
);
}
void checkout(String revision) {
/// Create (and checkout) a new branch based on the current HEAD.
///
/// Runs `git checkout -b $branchName`.
void newBranch(String branchName) {
git.run(
<String>['checkout', revision],
'checkout $revision',
<String>['checkout', '-b', branchName],
'create & checkout new branch $branchName',
workingDirectory: checkoutDirectory.path,
);
}
/// Check out the given ref.
void checkout(String ref) {
git.run(
<String>['checkout', ref],
'checkout ref',
workingDirectory: checkoutDirectory.path,
);
}
@ -146,13 +230,25 @@ abstract class Repository {
);
}
/// List commits in reverse chronological order.
List<String> revList(List<String> args) {
return git
.getOutput(
<String>['rev-list', ...args],
'rev-list with args ${args.join(' ')}',
workingDirectory: checkoutDirectory.path,
)
.trim()
.split('\n');
}
/// Look up the commit for [ref].
String reverseParse(String ref) {
final String revisionHash = git.getOutput(
<String>['rev-parse', ref],
'look up the commit for the ref $ref',
workingDirectory: checkoutDirectory.path,
).trim();
);
assert(revisionHash.isNotEmpty);
return revisionHash;
}
@ -184,11 +280,55 @@ abstract class Repository {
return exitcode == 0;
}
/// Resets repository HEAD to [commit].
void reset(String commit) {
/// Determines if a commit will cherry-pick to current HEAD without conflict.
bool canCherryPick(String commit) {
assert(
gitCheckoutClean(),
'cannot cherry-pick because git checkout ${checkoutDirectory.path} is not clean',
);
final int exitcode = git.run(
<String>['cherry-pick', '--no-commit', commit],
'attempt to cherry-pick $commit without committing',
allowNonZeroExitCode: true,
workingDirectory: checkoutDirectory.path,
);
final bool result = exitcode == 0;
if (result == false) {
stdio.printError(git.getOutput(
<String>['diff'],
'get diff of failed cherry-pick',
workingDirectory: checkoutDirectory.path,
));
}
reset('HEAD');
return result;
}
/// Cherry-pick a [commit] to the current HEAD.
///
/// This method will throw a [GitException] if the command fails.
void cherryPick(String commit) {
assert(
gitCheckoutClean(),
'cannot cherry-pick because git checkout ${checkoutDirectory.path} is not clean',
);
git.run(
<String>['reset', commit, '--hard'],
'reset to the release commit',
<String>['cherry-pick', '--no-commit', commit],
'attempt to cherry-pick $commit without committing',
workingDirectory: checkoutDirectory.path,
);
}
/// Resets repository HEAD to [ref].
void reset(String ref) {
git.run(
<String>['reset', ref, '--hard'],
'reset to $ref',
workingDirectory: checkoutDirectory.path,
);
}
@ -260,12 +400,17 @@ class FrameworkRepository extends Repository {
FrameworkRepository(
this.checkouts, {
String name = 'framework',
String upstream = FrameworkRepository.defaultUpstream,
Remote fetchRemote = const Remote(
name: RemoteName.upstream, url: FrameworkRepository.defaultUpstream),
bool localUpstream = false,
bool useExistingCheckout = false,
String initialRef,
Remote pushRemote,
}) : super(
name: name,
upstream: upstream,
fetchRemote: fetchRemote,
pushRemote: pushRemote,
initialRef: initialRef,
fileSystem: checkouts.fileSystem,
localUpstream: localUpstream,
parentDirectory: checkouts.directory,
@ -288,7 +433,10 @@ class FrameworkRepository extends Repository {
return FrameworkRepository(
checkouts,
name: name,
upstream: 'file://$upstreamPath/',
fetchRemote: Remote(
name: RemoteName.upstream,
url: 'file://$upstreamPath/',
),
localUpstream: false,
useExistingCheckout: useExistingCheckout,
);
@ -298,6 +446,8 @@ class FrameworkRepository extends Repository {
static const String defaultUpstream =
'https://github.com/flutter/flutter.git';
static const String defaultBranch = 'master';
String get cacheDirectory => fileSystem.path.join(
checkoutDirectory.path,
'bin',
@ -311,7 +461,8 @@ class FrameworkRepository extends Repository {
return FrameworkRepository(
checkouts,
name: cloneName,
upstream: 'file://${checkoutDirectory.path}/',
fetchRemote: Remote(
name: RemoteName.upstream, url: 'file://${checkoutDirectory.path}/'),
useExistingCheckout: useExistingCheckout,
);
}
@ -345,8 +496,8 @@ class FrameworkRepository extends Repository {
}
@override
void checkout(String revision) {
super.checkout(revision);
void checkout(String ref) {
super.checkout(ref);
// The tool will overwrite old cached artifacts, but not delete unused
// artifacts from a previous version. Thus, delete the entire cache and
// re-populate.
@ -363,12 +514,55 @@ class FrameworkRepository extends Repository {
final io.ProcessResult result =
runFlutter(<String>['--version', '--machine']);
final Map<String, dynamic> versionJson = jsonDecode(
globals.stdoutToString(result.stdout),
stdoutToString(result.stdout),
) as Map<String, dynamic>;
return Version.fromString(versionJson['frameworkVersion'] as String);
}
}
class EngineRepository extends Repository {
EngineRepository(
this.checkouts, {
String name = 'engine',
String initialRef = EngineRepository.defaultBranch,
Remote fetchRemote = const Remote(
name: RemoteName.upstream, url: EngineRepository.defaultUpstream),
bool localUpstream = false,
bool useExistingCheckout = false,
Remote pushRemote,
}) : super(
name: name,
fetchRemote: fetchRemote,
pushRemote: pushRemote,
initialRef: initialRef,
fileSystem: checkouts.fileSystem,
localUpstream: localUpstream,
parentDirectory: checkouts.directory,
platform: checkouts.platform,
processManager: checkouts.processManager,
stdio: checkouts.stdio,
useExistingCheckout: useExistingCheckout,
);
final Checkouts checkouts;
static const String defaultUpstream = 'https://github.com/flutter/engine.git';
static const String defaultBranch = 'master';
@override
Repository cloneRepository(String cloneName) {
assert(localUpstream);
cloneName ??= 'clone-of-$name';
return EngineRepository(
checkouts,
name: cloneName,
fetchRemote: Remote(
name: RemoteName.upstream, url: 'file://${checkoutDirectory.path}/'),
useExistingCheckout: useExistingCheckout,
);
}
}
/// An enum of all the repositories that the Conductor supports.
enum RepositoryType {
framework,

View file

@ -8,11 +8,18 @@ import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import './globals.dart';
import './repository.dart';
import './stdio.dart';
import './version.dart';
const String kIncrement = 'increment';
const String kCommit = 'commit';
const String kRemoteName = 'remote';
const String kJustPrint = 'just-print';
const String kYes = 'yes';
const String kForce = 'force';
const String kSkipTagging = 'skip-tagging';
/// Create a new dev release without cherry picks.
class RollDevCommand extends Command<void> {
RollDevCommand({
@ -57,7 +64,17 @@ class RollDevCommand extends Command<void> {
help: 'Do not create tag and push to remote, only update release branch. '
'For recovering when the script fails trying to git push to the release branch.'
);
argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.');
argParser.addFlag(
kYes,
negatable: false,
abbr: 'y',
help: 'Skip the confirmation prompt.',
);
argParser.addOption(
kRemoteName,
help: 'Specifies which git remote to fetch from.',
defaultsTo: 'upstream',
);
}
final Checkouts checkouts;
@ -92,8 +109,8 @@ bool rollDev({
@required ArgResults argResults,
@required Stdio stdio,
@required FrameworkRepository repository,
String remoteName = 'origin',
}) {
final String remoteName = argResults[kRemoteName] as String;
final String level = argResults[kIncrement] as String;
final String commit = argResults[kCommit] as String;
final bool justPrint = argResults[kJustPrint] as bool;

348
dev/tools/lib/start.dart Normal file
View file

@ -0,0 +1,348 @@
// 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 'dart:convert' show jsonEncode;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:fixnum/fixnum.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import './git.dart';
import './globals.dart';
import './proto/conductor_state.pb.dart' as pb;
import './proto/conductor_state.pbenum.dart' show ReleasePhase;
import './repository.dart';
import './state.dart';
import './stdio.dart';
const String kCandidateOption = 'candidate-branch';
const String kReleaseOption = 'release-channel';
const String kStateOption = 'state-file';
const String kFrameworkMirrorOption = 'framework-mirror';
const String kEngineMirrorOption = 'engine-mirror';
const String kFrameworkUpstreamOption = 'framework-upstream';
const String kEngineUpstreamOption = 'engine-upstream';
const String kFrameworkCherrypicksOption = 'framework-cherrypicks';
const String kEngineCherrypicksOption = 'engine-cherrypicks';
/// Command to print the status of the current Flutter release.
class StartCommand extends Command<void> {
StartCommand({
@required this.checkouts,
@required this.flutterRoot,
}) : platform = checkouts.platform,
processManager = checkouts.processManager,
fileSystem = checkouts.fileSystem,
stdio = checkouts.stdio {
final String defaultPath = defaultStateFilePath(platform);
argParser.addOption(
kCandidateOption,
help: 'The candidate branch the release will be based on.',
);
argParser.addOption(
kReleaseOption,
help: 'The target release channel for the release.',
allowed: <String>['stable', 'beta', 'dev'],
);
argParser.addOption(
kFrameworkUpstreamOption,
defaultsTo: FrameworkRepository.defaultUpstream,
help:
'Configurable Framework repo upstream remote. Primarily for testing.',
hide: true,
);
argParser.addOption(
kEngineUpstreamOption,
defaultsTo: EngineRepository.defaultUpstream,
help: 'Configurable Engine repo upstream remote. Primarily for testing.',
hide: true,
);
argParser.addOption(
kFrameworkMirrorOption,
help: 'Framework repo mirror remote.',
);
argParser.addOption(
kEngineMirrorOption,
help: 'Engine repo mirror remote.',
);
argParser.addOption(
kStateOption,
defaultsTo: defaultPath,
help: 'Path to persistent state file. Defaults to $defaultPath',
);
argParser.addMultiOption(
kEngineCherrypicksOption,
help: 'Engine cherrypick hashes to be applied.',
defaultsTo: <String>[],
);
argParser.addMultiOption(
kFrameworkCherrypicksOption,
help: 'Framework cherrypick hashes to be applied.',
defaultsTo: <String>[],
);
final Git git = Git(processManager);
conductorVersion = git.getOutput(
<String>['rev-parse', 'HEAD'],
'look up the current revision.',
workingDirectory: flutterRoot.path,
).trim();
assert(conductorVersion.isNotEmpty);
}
final Checkouts checkouts;
/// The root directory of the Flutter repository that houses the Conductor.
///
/// This directory is used to check the git revision of the Conductor.
final Directory flutterRoot;
final FileSystem fileSystem;
final Platform platform;
final ProcessManager processManager;
final Stdio stdio;
/// Git revision for the currently running Conductor.
String conductorVersion;
@override
String get name => 'start';
@override
String get description => 'Initialize a new Flutter release.';
@override
void run() {
if (!platform.isMacOS && !platform.isLinux) {
throw ConductorException(
'Error! This tool is only supported on macOS and Linux',
);
}
final File stateFile = checkouts.fileSystem.file(
getValueFromEnvOrArgs(kStateOption, argResults, platform.environment),
);
if (stateFile.existsSync()) {
throw ConductorException(
'Error! A persistent state file already found at ${argResults[kStateOption]}.\n\n'
'Run `conductor clean` to cancel a previous release.');
}
final String frameworkUpstream = getValueFromEnvOrArgs(
kFrameworkUpstreamOption,
argResults,
platform.environment,
);
final String frameworkMirror = getValueFromEnvOrArgs(
kFrameworkMirrorOption,
argResults,
platform.environment,
);
final String engineUpstream = getValueFromEnvOrArgs(
kEngineUpstreamOption,
argResults,
platform.environment,
);
final String engineMirror = getValueFromEnvOrArgs(
kEngineMirrorOption,
argResults,
platform.environment,
);
final String candidateBranch = getValueFromEnvOrArgs(
kCandidateOption,
argResults,
platform.environment,
);
final String releaseChannel = getValueFromEnvOrArgs(
kReleaseOption,
argResults,
platform.environment,
);
final List<String> frameworkCherrypickRevisions = getValuesFromEnvOrArgs(
kFrameworkCherrypicksOption,
argResults,
platform.environment,
);
final List<String> engineCherrypickRevisions = getValuesFromEnvOrArgs(
kEngineCherrypicksOption,
argResults,
platform.environment,
);
if (!releaseCandidateBranchRegex.hasMatch(candidateBranch)) {
throw ConductorException(
'Invalid release candidate branch "$candidateBranch". '
'Text should match the regex pattern /${releaseCandidateBranchRegex.pattern}/.');
}
final Int64 unixDate = Int64(DateTime.now().millisecondsSinceEpoch);
final pb.ConductorState state = pb.ConductorState();
state.releaseChannel = releaseChannel;
state.createdDate = unixDate;
state.lastUpdatedDate = unixDate;
final EngineRepository engine = EngineRepository(
checkouts,
initialRef: candidateBranch,
fetchRemote: Remote(
name: RemoteName.upstream,
url: engineUpstream,
),
pushRemote: Remote(
name: RemoteName.mirror,
url: engineMirror,
),
);
// Create a new branch so that we don't accidentally push to upstream
// candidateBranch.
engine.newBranch('cherrypicks-$candidateBranch');
final List<pb.Cherrypick> engineCherrypicks = _sortCherrypicks(
repository: engine,
cherrypicks: engineCherrypickRevisions,
upstreamRef: EngineRepository.defaultBranch,
releaseRef: candidateBranch,
).map((String revision) => pb.Cherrypick(
trunkRevision: revision,
state: pb.CherrypickState.PENDING,
)).toList();
for (final pb.Cherrypick cherrypick in engineCherrypicks) {
final String revision = cherrypick.trunkRevision;
final bool success = engine.canCherryPick(revision);
stdio.printTrace(
'Attempt to cherrypick $revision ${success ? 'succeeded' : 'failed'}',
);
if (!success) {
cherrypick.state = pb.CherrypickState.PENDING_WITH_CONFLICT;
}
}
final String engineHead = engine.reverseParse('HEAD');
state.engine = pb.Repository(
candidateBranch: candidateBranch,
startingGitHead: engineHead,
currentGitHead: engineHead,
checkoutPath: engine.checkoutDirectory.path,
cherrypicks: engineCherrypicks,
);
final FrameworkRepository framework = FrameworkRepository(
checkouts,
initialRef: candidateBranch,
fetchRemote: Remote(
name: RemoteName.upstream,
url: frameworkUpstream,
),
pushRemote: Remote(
name: RemoteName.mirror,
url: frameworkMirror,
),
);
framework.newBranch('cherrypicks-$candidateBranch');
final List<pb.Cherrypick> frameworkCherrypicks = _sortCherrypicks(
repository: framework,
cherrypicks: frameworkCherrypickRevisions,
upstreamRef: FrameworkRepository.defaultBranch,
releaseRef: candidateBranch,
).map((String revision) => pb.Cherrypick(
trunkRevision: revision,
state: pb.CherrypickState.PENDING,
)).toList();
for (final pb.Cherrypick cherrypick in frameworkCherrypicks) {
final String revision = cherrypick.trunkRevision;
final bool result = framework.canCherryPick(revision);
stdio.printTrace(
'Attempt to cherrypick $cherrypick ${result ? 'succeeded' : 'failed'}',
);
}
final String frameworkHead = framework.reverseParse('HEAD');
state.framework = pb.Repository(
candidateBranch: candidateBranch,
startingGitHead: frameworkHead,
currentGitHead: frameworkHead,
checkoutPath: framework.checkoutDirectory.path,
cherrypicks: frameworkCherrypicks,
);
state.lastPhase = ReleasePhase.INITIALIZE;
state.conductorVersion = conductorVersion;
stdio.printTrace('Writing state to file ${stateFile.path}...');
state.logs.addAll(stdio.logs);
stateFile.writeAsStringSync(
jsonEncode(state.toProto3Json()),
flush: true,
);
stdio.printStatus(presentState(state));
}
// To minimize merge conflicts, sort the commits by rev-list order.
List<String> _sortCherrypicks({
@required Repository repository,
@required List<String> cherrypicks,
@required String upstreamRef,
@required String releaseRef,
}) {
if (cherrypicks.isEmpty) {
return cherrypicks;
}
// Input cherrypick hashes that failed to be parsed by git.
final List<String> unknownCherrypicks = <String>[];
// Full 40-char hashes parsed by git.
final List<String> validatedCherrypicks = <String>[];
// Final, validated, sorted list of cherrypicks to be applied.
final List<String> sortedCherrypicks = <String>[];
for (final String cherrypick in cherrypicks) {
try {
final String fullRef = repository.reverseParse(cherrypick);
validatedCherrypicks.add(fullRef);
} on GitException {
// Catch this exception so that we can validate the rest.
unknownCherrypicks.add(cherrypick);
}
}
final String branchPoint = repository.branchPoint(
'${repository.fetchRemote.name}/$upstreamRef',
'${repository.fetchRemote.name}/$releaseRef',
);
// `git rev-list` returns newest first, so reverse this list
final List<String> upstreamRevlist = repository.revList(<String>[
'--ancestry-path',
'$branchPoint..$upstreamRef',
]).reversed.toList();
stdio.printStatus('upstreamRevList:\n${upstreamRevlist.join('\n')}\n');
stdio.printStatus('validatedCherrypicks:\n${validatedCherrypicks.join('\n')}\n');
for (final String upstreamRevision in upstreamRevlist) {
if (validatedCherrypicks.contains(upstreamRevision)) {
validatedCherrypicks.remove(upstreamRevision);
sortedCherrypicks.add(upstreamRevision);
if (unknownCherrypicks.isEmpty && validatedCherrypicks.isEmpty) {
return sortedCherrypicks;
}
}
}
// We were given input cherrypicks that were not present in the upstream
// rev-list
stdio.printError(
'The following ${repository.name} cherrypicks were not found in the '
'upstream $upstreamRef branch:',
);
for (final String cp in <String>[...validatedCherrypicks, ...unknownCherrypicks]) {
stdio.printError('\t$cp');
}
throw ConductorException(
'${validatedCherrypicks.length + unknownCherrypicks.length} unknown cherrypicks provided!',
);
}
}

167
dev/tools/lib/state.dart Normal file
View file

@ -0,0 +1,167 @@
// 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:platform/platform.dart';
import './globals.dart';
import './proto/conductor_state.pb.dart' as pb;
import './proto/conductor_state.pbenum.dart' show ReleasePhase;
const String kStateFileName = '.flutter_conductor_state.json';
String luciConsoleLink(String channel, String groupName) {
assert(
<String>['stable', 'beta', 'dev', 'master'].contains(channel),
'channel $channel not recognized',
);
assert(
<String>['framework', 'engine', 'devicelab'].contains(groupName),
'group named $groupName not recognized',
);
final String consoleName = channel == 'master' ? groupName : '${channel}_$groupName';
return 'https://ci.chromium.org/p/flutter/g/$consoleName/console';
}
String defaultStateFilePath(Platform platform) {
assert(platform.environment['HOME'] != null);
return <String>[
platform.environment['HOME'],
kStateFileName,
].join(platform.pathSeparator);
}
String presentState(pb.ConductorState state) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('Conductor version: ${state.conductorVersion}');
buffer.writeln('Release channel: ${state.releaseChannel}');
buffer.writeln('');
buffer.writeln(
'Release started at: ${DateTime.fromMillisecondsSinceEpoch(state.createdDate.toInt())}');
buffer.writeln(
'Last updated at: ${DateTime.fromMillisecondsSinceEpoch(state.lastUpdatedDate.toInt())}');
buffer.writeln('');
buffer.writeln('Engine Repo');
buffer.writeln('\tCandidate branch: ${state.engine.candidateBranch}');
buffer.writeln('\tStarting git HEAD: ${state.engine.startingGitHead}');
buffer.writeln('\tCurrent git HEAD: ${state.engine.currentGitHead}');
buffer.writeln('\tPath to checkout: ${state.engine.checkoutPath}');
buffer.writeln('\tPost-submit LUCI dashboard: ${luciConsoleLink(state.releaseChannel, 'engine')}');
if (state.engine.cherrypicks.isNotEmpty) {
buffer.writeln('${state.engine.cherrypicks.length} Engine Cherrypicks:');
for (final pb.Cherrypick cherrypick in state.engine.cherrypicks) {
buffer.writeln('\t${cherrypick.trunkRevision} - ${cherrypick.state}');
}
} else {
buffer.writeln('0 Engine cherrypicks.');
}
buffer.writeln('Framework Repo');
buffer.writeln('\tCandidate branch: ${state.framework.candidateBranch}');
buffer.writeln('\tStarting git HEAD: ${state.framework.startingGitHead}');
buffer.writeln('\tCurrent git HEAD: ${state.framework.currentGitHead}');
buffer.writeln('\tPath to checkout: ${state.framework.checkoutPath}');
buffer.writeln('\tPost-submit LUCI dashboard: ${luciConsoleLink(state.releaseChannel, 'framework')}');
buffer.writeln('\tDevicelab LUCI dashboard: ${luciConsoleLink(state.releaseChannel, 'devicelab')}');
if (state.framework.cherrypicks.isNotEmpty) {
buffer.writeln('${state.framework.cherrypicks.length} Framework Cherrypicks:');
for (final pb.Cherrypick cherrypick in state.framework.cherrypicks) {
buffer.writeln('\t${cherrypick.trunkRevision} - ${cherrypick.state}');
}
} else {
buffer.writeln('0 Framework cherrypicks.');
}
buffer.writeln('');
if (state.lastPhase == ReleasePhase.VERIFY_RELEASE) {
buffer.writeln(
'${state.releaseChannel} release ${state.releaseVersion} has been published and verified.\n',
);
return buffer.toString();
}
buffer.writeln('The next step is:');
buffer.writeln(presentPhases(state.lastPhase));
buffer.writeln(phaseInstructions(state));
buffer.writeln('');
buffer.writeln('Issue `conductor next` when you are ready to proceed.');
return buffer.toString();
}
String presentPhases(ReleasePhase lastPhase) {
final ReleasePhase nextPhase = getNextPhase(lastPhase);
final StringBuffer buffer = StringBuffer();
bool phaseCompleted = true;
for (final ReleasePhase phase in ReleasePhase.values) {
if (phase == nextPhase) {
// This phase will execute the next time `conductor next` is run.
buffer.writeln('> ${phase.name} (next)');
phaseCompleted = false;
} else if (phaseCompleted) {
// This phase was already completed.
buffer.writeln('${phase.name}');
} else {
// This phase has not been completed yet.
buffer.writeln(' ${phase.name}');
}
}
return buffer.toString();
}
String phaseInstructions(pb.ConductorState state) {
switch (state.lastPhase) {
case ReleasePhase.INITIALIZE:
if (state.engine.cherrypicks.isEmpty) {
return <String>[
'There are no engine cherrypicks, so issue `conductor next` to continue',
'to the next step.',
].join('\n');
}
return <String>[
'You must now manually apply the following engine cherrypicks to the checkout',
'at ${state.engine.checkoutPath} in order:',
for (final pb.Cherrypick cherrypick in state.engine.cherrypicks)
'\t${cherrypick.trunkRevision}',
'See $kReleaseDocumentationUrl for more information.',
].join('\n');
case ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
return <String>[
'You must verify Engine CI builds are successful and then codesign the',
'binaries at revision ${state.engine.currentGitHead}.',
].join('\n');
case ReleasePhase.CODESIGN_ENGINE_BINARIES:
return <String>[
'You must now manually apply the following framework cherrypicks to the checkout',
'at ${state.framework.checkoutPath} in order:',
for (final pb.Cherrypick cherrypick in state.framework.cherrypicks)
'\t${cherrypick.trunkRevision}',
].join('\n');
case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
return <String>[
'You must verify Framework CI builds are successful.',
'See $kReleaseDocumentationUrl for more information.',
].join('\n');
case ReleasePhase.PUBLISH_VERSION:
return 'Issue `conductor next` to publish your release to the release branch.';
case ReleasePhase.PUBLISH_CHANNEL:
return <String>[
'Release archive packages must be verified on cloud storage. Issue',
'`conductor next` to check if they are ready.',
].join('\n');
case ReleasePhase.VERIFY_RELEASE:
return 'This release has been completed.';
}
assert(false);
return ''; // For analyzer
}
/// Returns the next phase in the ReleasePhase enum.
///
/// Will throw a [ConductorException] if [ReleasePhase.RELEASE_VERIFIED] is
/// passed as an argument, as there is no next phase.
ReleasePhase getNextPhase(ReleasePhase previousPhase) {
assert(previousPhase != null);
if (previousPhase == ReleasePhase.VERIFY_RELEASE) {
throw ConductorException('There is no next ReleasePhase!');
}
return ReleasePhase.valueOf(previousPhase.value + 1);
}

68
dev/tools/lib/status.dart Normal file
View file

@ -0,0 +1,68 @@
// 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 'dart:convert' show jsonDecode;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import './proto/conductor_state.pb.dart' as pb;
import './repository.dart';
import './state.dart';
import './stdio.dart';
const String kVerboseFlag = 'verbose';
const String kStateOption = 'state-file';
/// Command to print the status of the current Flutter release.
class StatusCommand extends Command<void> {
StatusCommand({
@required this.checkouts,
}) : platform = checkouts.platform,
fileSystem = checkouts.fileSystem,
stdio = checkouts.stdio {
final String defaultPath = defaultStateFilePath(platform);
argParser.addOption(
kStateOption,
defaultsTo: defaultPath,
help: 'Path to persistent state file. Defaults to $defaultPath',
);
argParser.addFlag(
kVerboseFlag,
abbr: 'v',
defaultsTo: false,
help: 'Also print logs.',
);
}
final Checkouts checkouts;
final FileSystem fileSystem;
final Platform platform;
final Stdio stdio;
@override
String get name => 'status';
@override
String get description => 'Print status of current release.';
@override
void run() {
final File stateFile = checkouts.fileSystem.file(argResults[kStateOption]);
if (!stateFile.existsSync()) {
stdio.printStatus(
'No persistent state file found at ${argResults[kStateOption]}.');
return;
}
final pb.ConductorState state = pb.ConductorState();
state.mergeFromProto3Json(jsonDecode(stateFile.readAsStringSync()));
stdio.printStatus(presentState(state));
if (argResults[kVerboseFlag] as bool) {
stdio.printStatus('\nLogs:');
state.logs.forEach(stdio.printStatus);
}
}
}

View file

@ -7,17 +7,31 @@ import 'dart:io' as io;
import 'package:meta/meta.dart';
abstract class Stdio {
final List<String> logs = <String>[];
/// Error/warning messages printed to STDERR.
void printError(String message);
@mustCallSuper
void printError(String message) {
logs.add('[error] $message');
}
/// Ordinary STDOUT messages.
void printStatus(String message);
@mustCallSuper
void printStatus(String message) {
logs.add('[status] $message');
}
/// Debug messages that are only printed in verbose mode.
void printTrace(String message);
@mustCallSuper
void printTrace(String message) {
logs.add('[trace] $message');
}
/// Write string to STDOUT without trailing newline.
void write(String message);
@mustCallSuper
void write(String message) {
logs.add('[write] $message');
}
/// Read a line of text from STDIN.
String readLineSync();
@ -43,21 +57,25 @@ class VerboseStdio extends Stdio {
@override
void printError(String message) {
super.printError(message);
stderr.writeln(message);
}
@override
void printStatus(String message) {
super.printStatus(message);
stdout.writeln(message);
}
@override
void printTrace(String message) {
super.printTrace(message);
stdout.writeln(message);
}
@override
void write(String message) {
super.write(message);
stdout.write(message);
}

View file

@ -12,12 +12,14 @@ dependencies:
meta: 1.3.0
path: 1.8.0
process: 4.2.1
protobuf: 1.1.3
charcode: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
clock: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
collection: 1.15.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
crypto: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
file: 6.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
fixnum: 0.10.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_parser: 4.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pedantic: 1.11.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
platform: 3.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
@ -63,4 +65,4 @@ dev_dependencies:
webkit_inspection_protocol: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
yaml: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
# PUBSPEC CHECKSUM: 8750
# PUBSPEC CHECKSUM: e555

View file

@ -0,0 +1,97 @@
// 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/command_runner.dart';
import 'package:dev_tools/clean.dart';
import 'package:dev_tools/repository.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:platform/platform.dart';
import '../../../packages/flutter_tools/test/src/fake_process_manager.dart';
import './common.dart';
void main() {
group('clean command', () {
const String flutterRoot = '/flutter';
const String checkoutsParentDirectory = '$flutterRoot/dev/tools/';
MemoryFileSystem fileSystem;
FakePlatform platform;
TestStdio stdio;
FakeProcessManager processManager;
setUp(() {
stdio = TestStdio();
fileSystem = MemoryFileSystem.test();
});
tearDown(() {
// Ensure these don't get re-used between tests
stdio = null;
fileSystem = null;
processManager = null;
platform = null;
});
CommandRunner<void> createRunner({
List<FakeCommand> commands,
String operatingSystem,
}) {
operatingSystem ??= const LocalPlatform().operatingSystem;
final String pathSeparator = operatingSystem == 'windows' ? r'\' : '/';
processManager = FakeProcessManager.list(commands ?? <FakeCommand>[]);
platform = FakePlatform(
environment: <String, String>{'HOME': '/path/to/user/home'},
pathSeparator: pathSeparator,
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CleanCommand command = CleanCommand(
checkouts: checkouts,
);
return CommandRunner<void>('clean-test', '')..addCommand(command);
}
test('throws if no state file found', () async {
final CommandRunner<void> runner = createRunner();
const String stateFile = '/state-file.json';
await expectLater(
() async => runner.run(<String>[
'clean',
'--$kStateOption',
stateFile,
'--$kYesFlag',
]),
throwsExceptionWith(
'No persistent state file found at $stateFile',
),
);
});
test('deletes state file', () async {
final CommandRunner<void> runner = createRunner();
final File stateFile = fileSystem.file('/state-file.json');
stateFile.writeAsStringSync('{}');
await runner.run(<String>[
'clean',
'--$kStateOption',
stateFile.path,
'--$kYesFlag',
]);
expect(stateFile.existsSync(), false);
});
}, onPlatform: <String, dynamic>{
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
});
}

View file

@ -21,11 +21,14 @@ void main() {
() async {
const Platform platform = LocalPlatform();
const FileSystem fileSystem = LocalFileSystem();
final Directory tempDir = fileSystem.systemTempDirectory.createTempSync(
'conductor_integration_test',
);
const ProcessManager processManager = LocalProcessManager();
final TestStdio stdio = TestStdio(verbose: true);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: localFlutterRoot.parent,
parentDirectory: tempDir,
platform: platform,
processManager: processManager,
stdio: stdio,

View file

@ -109,6 +109,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
'file://$flutterRoot/',
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -194,6 +196,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
'file://$flutterRoot/',
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -279,6 +283,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
'file://$flutterRoot/',
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -336,6 +342,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
'file://$flutterRoot/',
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',

View file

@ -35,7 +35,7 @@ Matcher throwsExceptionWith(String messageSubString) {
);
}
class TestStdio implements Stdio {
class TestStdio extends Stdio {
TestStdio({
this.verbose = false,
List<String> stdin,
@ -43,36 +43,15 @@ class TestStdio implements Stdio {
_stdin = stdin ?? <String>[];
}
final StringBuffer _error = StringBuffer();
String get error => _error.toString();
String get error => logs.where((String log) => log.startsWith(r'[error] ')).join('\n');
String get stdout => logs.where((String log) {
return log.startsWith(r'[status] ') || log.startsWith(r'[trace] ');
}).join('\n');
final StringBuffer _stdout = StringBuffer();
String get stdout => _stdout.toString();
final bool verbose;
List<String> _stdin;
@override
void printError(String message) {
_error.writeln(message);
}
@override
void printStatus(String message) {
_stdout.writeln(message);
}
@override
void printTrace(String message) {
if (verbose) {
_stdout.writeln(message);
}
}
@override
void write(String message) {
_stdout.write(message);
}
@override
String readLineSync() {
if (_stdin.isEmpty) {

View file

@ -7,7 +7,6 @@ import 'package:file/local.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'package:dev_tools/globals.dart';
import 'package:dev_tools/roll_dev.dart' show rollDev;
import 'package:dev_tools/repository.dart';
import 'package:dev_tools/version.dart';
@ -25,15 +24,17 @@ void main() {
Checkouts checkouts;
FrameworkRepository frameworkUpstream;
FrameworkRepository framework;
Directory tempDir;
setUp(() {
platform = const LocalPlatform();
fileSystem = const LocalFileSystem();
processManager = const LocalProcessManager();
stdio = TestStdio(verbose: true);
tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_conductor_checkouts');
checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: localFlutterRoot.parent,
parentDirectory: tempDir,
platform: platform,
processManager: processManager,
stdio: stdio,
@ -45,7 +46,7 @@ void main() {
framework = FrameworkRepository(
checkouts,
name: 'test-framework',
upstream: 'file://${frameworkUpstream.checkoutDirectory.path}/',
fetchRemote: Remote(name: RemoteName.upstream, url: 'file://${frameworkUpstream.checkoutDirectory.path}/'),
);
});
@ -59,7 +60,7 @@ void main() {
commit: latestCommit,
// Ensure this test passes after a dev release with hotfixes
force: true,
remote: 'origin',
remote: 'upstream',
);
expect(
@ -96,7 +97,7 @@ void main() {
commit: latestCommit,
// Ensure this test passes after a dev release with hotfixes
force: true,
remote: 'origin',
remote: 'upstream',
);
expect(
@ -124,7 +125,7 @@ void main() {
expect(finalVersion.m, 0);
expect(finalVersion.n, 0);
expect(finalVersion.commits, null);
}, skip: 'TODO(fujino): https://github.com/flutter/flutter/issues/80463');
});
}, onPlatform: <String, dynamic>{
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
});

View file

@ -83,6 +83,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
kUpstreamRemote,
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -131,6 +133,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
kUpstreamRemote,
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -193,16 +197,16 @@ void main() {
),
false,
);
expect(stdio.stdout.contains(nextVersion), true);
expect(stdio.logs.join('').contains(nextVersion), true);
});
test(
'exits with exception if --skip-tagging is provided but commit isn\'t '
'already tagged', () {
test("exits with exception if --skip-tagging is provided but commit isn't already tagged", () {
processManager.addCommands(<FakeCommand>[
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
kUpstreamRemote,
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -283,6 +287,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
kUpstreamRemote,
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -356,6 +362,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
kUpstreamRemote,
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -433,6 +441,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
kUpstreamRemote,
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -526,6 +536,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
kUpstreamRemote,
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',
@ -623,6 +635,8 @@ void main() {
const FakeCommand(command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
kUpstreamRemote,
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework',

View file

@ -0,0 +1,255 @@
// 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 'dart:convert' show jsonDecode;
import 'package:args/command_runner.dart';
import 'package:dev_tools/proto/conductor_state.pb.dart' as pb;
import 'package:dev_tools/proto/conductor_state.pbenum.dart' show ReleasePhase;
import 'package:dev_tools/start.dart';
import 'package:dev_tools/state.dart';
import 'package:dev_tools/repository.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:platform/platform.dart';
import '../../../packages/flutter_tools/test/src/fake_process_manager.dart';
import './common.dart';
void main() {
group('start command', () {
const String flutterRoot = '/flutter';
const String checkoutsParentDirectory = '$flutterRoot/dev/tools/';
const String frameworkMirror = 'https://github.com/user/flutter.git';
const String engineMirror = 'https://github.com/user/engine.git';
const String candidateBranch = 'flutter-1.2-candidate.3';
const String releaseChannel = 'stable';
const String revision = 'abcd1234';
Checkouts checkouts;
MemoryFileSystem fileSystem;
FakePlatform platform;
TestStdio stdio;
FakeProcessManager processManager;
setUp(() {
stdio = TestStdio();
fileSystem = MemoryFileSystem.test();
});
CommandRunner<void> createRunner({
Map<String, String> environment,
String operatingSystem,
List<FakeCommand> commands,
}) {
operatingSystem ??= const LocalPlatform().operatingSystem;
final String pathSeparator = operatingSystem == 'windows' ? r'\' : '/';
environment ??= <String, String>{
'HOME': '/path/to/user/home',
};
final Directory homeDir = fileSystem.directory(
environment['HOME'],
);
// Tool assumes this exists
homeDir.createSync(recursive: true);
platform = FakePlatform(
environment: environment,
operatingSystem: operatingSystem,
pathSeparator: pathSeparator,
);
processManager = FakeProcessManager.list(commands ?? <FakeCommand>[]);
checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final StartCommand command = StartCommand(
checkouts: checkouts,
flutterRoot: fileSystem.directory(flutterRoot),
);
return CommandRunner<void>('codesign-test', '')..addCommand(command);
}
tearDown(() {
// Ensure we don't re-use these between tests.
processManager = null;
checkouts = null;
platform = null;
});
test('throws exception if run from Windows', () async {
final CommandRunner<void> runner = createRunner(
commands: <FakeCommand>[
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision,
),
],
operatingSystem: 'windows',
);
await expectLater(
() async => runner.run(<String>['start']),
throwsExceptionWith(
'Error! This tool is only supported on macOS and Linux',
),
);
});
test('throws if --$kFrameworkMirrorOption not provided', () async {
final CommandRunner<void> runner = createRunner(
commands: <FakeCommand>[
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision,
),
],
);
await expectLater(
() async => runner.run(<String>['start']),
throwsExceptionWith(
'Expected either the CLI arg --$kFrameworkMirrorOption or the environment variable FRAMEWORK_MIRROR to be provided',
),
);
});
test('creates state file if provided correct inputs', () async {
const String revision2 = 'def789';
const String revision3 = '123abc';
final List<FakeCommand> engineCommands = <FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'engine',
),
],
),
const FakeCommand(
command: <String>['git', 'remote', 'add', 'mirror', engineMirror],
),
const FakeCommand(
command: <String>['git', 'fetch', 'mirror'],
),
const FakeCommand(
command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision2,
),
const FakeCommand(
command: <String>[
'git',
'checkout',
'-b',
'cherrypicks-$candidateBranch',
],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision2,
),
];
final List<FakeCommand> frameworkCommands = <FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
FrameworkRepository.defaultUpstream,
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
),
],
),
const FakeCommand(
command: <String>['git', 'remote', 'add', 'mirror', frameworkMirror],
),
const FakeCommand(
command: <String>['git', 'fetch', 'mirror'],
),
const FakeCommand(
command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision3,
),
const FakeCommand(
command: <String>[
'git',
'checkout',
'-b',
'cherrypicks-$candidateBranch',
],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision3,
),
];
final CommandRunner<void> runner = createRunner(
commands: <FakeCommand>[
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision,
),
...engineCommands,
...frameworkCommands,
],
);
final String stateFilePath = fileSystem.path.join(
platform.environment['HOME'],
kStateFileName,
);
await runner.run(<String>[
'start',
'--$kFrameworkMirrorOption',
frameworkMirror,
'--$kEngineMirrorOption',
engineMirror,
'--$kCandidateOption',
candidateBranch,
'--$kReleaseOption',
releaseChannel,
'--$kStateOption',
stateFilePath,
]);
final File stateFile = fileSystem.file(stateFilePath);
final pb.ConductorState state = pb.ConductorState();
state.mergeFromProto3Json(
jsonDecode(stateFile.readAsStringSync()),
);
expect(state.isInitialized(), true);
expect(state.releaseChannel, releaseChannel);
expect(state.engine.candidateBranch, candidateBranch);
expect(state.engine.startingGitHead, revision2);
expect(state.framework.candidateBranch, candidateBranch);
expect(state.framework.startingGitHead, revision3);
expect(state.lastPhase, ReleasePhase.INITIALIZE);
expect(state.conductorVersion, revision);
});
}, onPlatform: <String, dynamic>{
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
});
}