Update conductor to support monorepos (#161704)

Fixes https://github.com/flutter/flutter/issues/161616

Tested this PR with the 3.29.0 stable release:

- https://github.com/flutter/flutter/pull/162899
This commit is contained in:
Christopher Fujino 2025-02-11 12:51:50 -08:00 committed by GitHub
parent 7f783e3587
commit 6a412a5f71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 489 additions and 1306 deletions

View File

@ -85,79 +85,7 @@ class NextContext extends Context {
CherrypickState.ABANDONED,
];
switch (state.currentPhase) {
case pb.ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
final Remote upstream = Remote.upstream(state.engine.upstream.url);
final EngineRepository engine = EngineRepository(
checkouts,
initialRef: state.engine.workingBranch,
upstreamRemote: upstream,
previousCheckoutLocation: state.engine.checkoutPath,
);
if (!state_import.requiresEnginePR(state)) {
stdio.printStatus('This release has no engine cherrypicks. No Engine PR is necessary.\n');
break;
}
final List<pb.Cherrypick> unappliedCherrypicks = <pb.Cherrypick>[
for (final pb.Cherrypick cherrypick in state.engine.cherrypicks)
if (!finishedStates.contains(cherrypick.state)) cherrypick,
];
if (unappliedCherrypicks.isEmpty) {
stdio.printStatus('All engine cherrypicks have been auto-applied by the conductor.\n');
} else {
if (unappliedCherrypicks.length == 1) {
stdio.printStatus(
'There was ${unappliedCherrypicks.length} cherrypick that was not auto-applied.',
);
} else {
stdio.printStatus(
'There were ${unappliedCherrypicks.length} cherrypicks that were not auto-applied.',
);
}
stdio.printStatus(
'These must be applied manually in the directory '
'${state.engine.checkoutPath} before proceeding.\n',
);
}
if (!autoAccept) {
final bool response = await prompt(
'Are you ready to push your engine branch to the repository '
'${state.engine.mirror.url}?',
);
if (!response) {
stdio.printError('Aborting command.');
updateState(state, stdio.logs);
return;
}
}
await pushWorkingBranch(engine, state.engine);
case pb.ReleasePhase.VERIFY_ENGINE_CI:
stdio.printStatus('You must validate post-submit CI for your engine PR and merge it');
if (!autoAccept) {
final bool response = await prompt(
'Has CI passed for the engine PR?\n\n'
'${state_import.luciConsoleLink(state.engine.candidateBranch, 'engine')}',
);
if (!response) {
stdio.printError('Aborting command.');
updateState(state, stdio.logs);
return;
}
}
case pb.ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
final Remote engineUpstreamRemote = Remote.upstream(state.engine.upstream.url);
final EngineRepository engine = EngineRepository(
checkouts,
// We explicitly want to check out the merged version from upstream
initialRef: '${engineUpstreamRemote.name}/${state.engine.candidateBranch}',
upstreamRemote: engineUpstreamRemote,
previousCheckoutLocation: state.engine.checkoutPath,
);
final String engineRevision = await engine.reverseParse('HEAD');
final Remote upstream = Remote.upstream(state.framework.upstream.url);
final FrameworkRepository framework = FrameworkRepository(
checkouts,
@ -166,7 +94,7 @@ class NextContext extends Context {
previousCheckoutLocation: state.framework.checkoutPath,
);
stdio.printStatus('Writing candidate branch...');
bool needsCommit = await framework.updateCandidateBranchVersion(
final bool needsCommit = await framework.updateCandidateBranchVersion(
state.framework.candidateBranch,
);
if (needsCommit) {
@ -181,20 +109,6 @@ class NextContext extends Context {
..state = pb.CherrypickState.COMPLETED,
);
}
stdio.printStatus('Rolling new engine hash $engineRevision to framework checkout...');
needsCommit = await framework.updateEngineRevision(engineRevision);
if (needsCommit) {
final String revision = await framework.commit(
'Update Engine revision to $engineRevision for ${state.releaseChannel} release ${state.releaseVersion}',
addFirst: true,
);
// append to list of cherrypicks so we know a PR is required
state.framework.cherrypicks.add(
pb.Cherrypick.create()
..appliedRevision = revision
..state = pb.CherrypickState.COMPLETED,
);
}
final List<pb.Cherrypick> unappliedCherrypicks = <pb.Cherrypick>[
for (final pb.Cherrypick cherrypick in state.framework.cherrypicks)

View File

@ -3,11 +3,25 @@
# 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 )"
set -euo pipefail
function follow_links() (
cd -P "$(dirname -- "$1")"
file="$PWD/$(basename -- "$1")"
while [[ -h "$file" ]]; do
cd -P "$(dirname -- "$file")"
file="$(readlink -- "$file")"
cd -P "$(dirname -- "$file")"
file="$PWD/$(basename -- "$file")"
done
echo "$file"
)
PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")"
DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
# Ensure dart-sdk is cached
"$DIR/../../../../bin/dart" --version
"$DIR/../../../../../../bin/dart" --version
if ! type protoc >/dev/null 2>&1; then
PROTOC_LINK='https://grpc.io/docs/protoc-installation/'
@ -22,13 +36,13 @@ if ! type dart >/dev/null 2>&1; then
fi
# Use null-safe protoc_plugin
dart pub global activate protoc_plugin 20.0.0
dart pub global activate protoc_plugin 21.1.2
protoc --dart_out="$DIR" --proto_path="$DIR" "$DIR/conductor_state.proto"
for SOURCE_FILE in $(ls "$DIR"/*.pb*.dart); do
# Format in place file
dart format --output=write --line-length 120 "$SOURCE_FILE"
dart format --output=write "$SOURCE_FILE"
# Create temp copy with the license header prepended
cp "$DIR/license_header.txt" "${SOURCE_FILE}.tmp"

View File

@ -8,7 +8,7 @@
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
@ -22,8 +22,21 @@ import 'conductor_state.pbenum.dart';
export 'conductor_state.pbenum.dart';
/// A git remote
class Remote extends $pb.GeneratedMessage {
factory Remote() => create();
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;
}
Remote._() : super();
factory Remote.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
@ -86,7 +99,23 @@ class Remote extends $pb.GeneratedMessage {
}
class Cherrypick extends $pb.GeneratedMessage {
factory Cherrypick() => create();
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;
}
Cherrypick._() : super();
factory Cherrypick.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
@ -127,6 +156,7 @@ class Cherrypick extends $pb.GeneratedMessage {
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Cherrypick>(create);
static Cherrypick? _defaultInstance;
/// The revision on trunk to cherrypick.
@$pb.TagNumber(1)
$core.String get trunkRevision => $_getSZ(0);
@$pb.TagNumber(1)
@ -139,6 +169,7 @@ class Cherrypick extends $pb.GeneratedMessage {
@$pb.TagNumber(1)
void clearTrunkRevision() => clearField(1);
/// Once applied, the actual commit revision of the cherrypick.
@$pb.TagNumber(2)
$core.String get appliedRevision => $_getSZ(1);
@$pb.TagNumber(2)
@ -165,7 +196,47 @@ class Cherrypick extends $pb.GeneratedMessage {
}
class Repository extends $pb.GeneratedMessage {
factory Repository() => create();
factory Repository({
$core.String? candidateBranch,
$core.String? startingGitHead,
$core.String? currentGitHead,
$core.String? checkoutPath,
Remote? upstream,
Remote? mirror,
$core.Iterable<Cherrypick>? cherrypicks,
$core.String? dartRevision,
$core.String? workingBranch,
}) {
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);
}
if (dartRevision != null) {
$result.dartRevision = dartRevision;
}
if (workingBranch != null) {
$result.workingBranch = workingBranch;
}
return $result;
}
Repository._() : super();
factory Repository.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
@ -210,6 +281,9 @@ class Repository extends $pb.GeneratedMessage {
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Repository>(create);
static Repository? _defaultInstance;
/// The development git branch the release is based on.
///
/// Must be of the form /flutter-(\d+)\.(\d+)-candidate\.(\d+)/
@$pb.TagNumber(1)
$core.String get candidateBranch => $_getSZ(0);
@$pb.TagNumber(1)
@ -222,6 +296,7 @@ class Repository extends $pb.GeneratedMessage {
@$pb.TagNumber(1)
void clearCandidateBranch() => clearField(1);
/// The commit hash at the tip of the branch before cherrypicks were applied.
@$pb.TagNumber(2)
$core.String get startingGitHead => $_getSZ(1);
@$pb.TagNumber(2)
@ -234,6 +309,8 @@ class Repository extends $pb.GeneratedMessage {
@$pb.TagNumber(2)
void clearStartingGitHead() => clearField(2);
/// The difference in commits between this and [startingGitHead] is the number
/// of cherrypicks that have been currently applied.
@$pb.TagNumber(3)
$core.String get currentGitHead => $_getSZ(2);
@$pb.TagNumber(3)
@ -246,6 +323,7 @@ class Repository extends $pb.GeneratedMessage {
@$pb.TagNumber(3)
void clearCurrentGitHead() => clearField(3);
/// Path to the git checkout on local disk.
@$pb.TagNumber(4)
$core.String get checkoutPath => $_getSZ(3);
@$pb.TagNumber(4)
@ -258,6 +336,7 @@ class Repository extends $pb.GeneratedMessage {
@$pb.TagNumber(4)
void clearCheckoutPath() => clearField(4);
/// The remote commits will be fetched from.
@$pb.TagNumber(5)
Remote get upstream => $_getN(4);
@$pb.TagNumber(5)
@ -272,6 +351,9 @@ class Repository extends $pb.GeneratedMessage {
@$pb.TagNumber(5)
Remote ensureUpstream() => $_ensure(4);
/// The remote cherrypicks will be pushed to create a Pull Request.
///
/// This should be a mirror owned by the user conducting the release.
@$pb.TagNumber(6)
Remote get mirror => $_getN(5);
@$pb.TagNumber(6)
@ -286,9 +368,11 @@ class Repository extends $pb.GeneratedMessage {
@$pb.TagNumber(6)
Remote ensureMirror() => $_ensure(5);
/// Desired cherrypicks.
@$pb.TagNumber(7)
$core.List<Cherrypick> get cherrypicks => $_getList(6);
/// For the repository that has a dart_revision in a DEPS file.
@$pb.TagNumber(8)
$core.String get dartRevision => $_getSZ(7);
@$pb.TagNumber(8)
@ -301,6 +385,10 @@ class Repository extends $pb.GeneratedMessage {
@$pb.TagNumber(8)
void clearDartRevision() => clearField(8);
/// Name of local and remote branch for applying cherrypicks.
///
/// When the pull request is merged, all commits here will be squashed to a
/// single commit on the [candidateBranch].
@$pb.TagNumber(9)
$core.String get workingBranch => $_getSZ(8);
@$pb.TagNumber(9)
@ -315,7 +403,51 @@ class Repository extends $pb.GeneratedMessage {
}
class ConductorState extends $pb.GeneratedMessage {
factory ConductorState() => create();
factory ConductorState({
$core.String? releaseChannel,
$core.String? releaseVersion,
Repository? engine,
Repository? framework,
$fixnum.Int64? createdDate,
$fixnum.Int64? lastUpdatedDate,
$core.Iterable<$core.String>? logs,
ReleasePhase? currentPhase,
$core.String? conductorVersion,
ReleaseType? releaseType,
}) {
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 (currentPhase != null) {
$result.currentPhase = currentPhase;
}
if (conductorVersion != null) {
$result.conductorVersion = conductorVersion;
}
if (releaseType != null) {
$result.releaseType = releaseType;
}
return $result;
}
ConductorState._() : super();
factory ConductorState.fromBuffer($core.List<$core.int> i,
[$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
@ -336,7 +468,7 @@ class ConductorState extends $pb.GeneratedMessage {
..pPS(8, _omitFieldNames ? '' : 'logs')
..e<ReleasePhase>(9, _omitFieldNames ? '' : 'currentPhase', $pb.PbFieldType.OE,
protoName: 'currentPhase',
defaultOrMaker: ReleasePhase.APPLY_ENGINE_CHERRYPICKS,
defaultOrMaker: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,
valueOf: ReleasePhase.valueOf,
enumValues: ReleasePhase.values)
..aOS(10, _omitFieldNames ? '' : 'conductorVersion', protoName: 'conductorVersion')
@ -368,6 +500,7 @@ class ConductorState extends $pb.GeneratedMessage {
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ConductorState>(create);
static ConductorState? _defaultInstance;
/// One of 'stable', 'beta', or 'dev'
@$pb.TagNumber(1)
$core.String get releaseChannel => $_getSZ(0);
@$pb.TagNumber(1)
@ -380,6 +513,7 @@ class ConductorState extends $pb.GeneratedMessage {
@$pb.TagNumber(1)
void clearReleaseChannel() => clearField(1);
/// The name of the release.
@$pb.TagNumber(2)
$core.String get releaseVersion => $_getSZ(1);
@$pb.TagNumber(2)
@ -447,6 +581,7 @@ class ConductorState extends $pb.GeneratedMessage {
@$pb.TagNumber(8)
$core.List<$core.String> get logs => $_getList(6);
/// The current [ReleasePhase] that has yet to be completed.
@$pb.TagNumber(9)
ReleasePhase get currentPhase => $_getN(7);
@$pb.TagNumber(9)
@ -459,6 +594,8 @@ class ConductorState extends $pb.GeneratedMessage {
@$pb.TagNumber(9)
void clearCurrentPhase() => clearField(9);
/// A string used to validate that the current conductor is the same version
/// that created the [ConductorState] object.
@$pb.TagNumber(10)
$core.String get conductorVersion => $_getSZ(8);
@$pb.TagNumber(10)

View File

@ -8,7 +8,7 @@
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
@ -18,22 +18,16 @@ import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class ReleasePhase extends $pb.ProtobufEnum {
static const ReleasePhase APPLY_ENGINE_CHERRYPICKS =
ReleasePhase._(0, _omitEnumNames ? '' : 'APPLY_ENGINE_CHERRYPICKS');
static const ReleasePhase VERIFY_ENGINE_CI =
ReleasePhase._(1, _omitEnumNames ? '' : 'VERIFY_ENGINE_CI');
static const ReleasePhase APPLY_FRAMEWORK_CHERRYPICKS =
ReleasePhase._(2, _omitEnumNames ? '' : 'APPLY_FRAMEWORK_CHERRYPICKS');
ReleasePhase._(0, _omitEnumNames ? '' : 'APPLY_FRAMEWORK_CHERRYPICKS');
static const ReleasePhase PUBLISH_VERSION =
ReleasePhase._(3, _omitEnumNames ? '' : 'PUBLISH_VERSION');
ReleasePhase._(1, _omitEnumNames ? '' : 'PUBLISH_VERSION');
static const ReleasePhase VERIFY_RELEASE =
ReleasePhase._(5, _omitEnumNames ? '' : 'VERIFY_RELEASE');
ReleasePhase._(2, _omitEnumNames ? '' : 'VERIFY_RELEASE');
static const ReleasePhase RELEASE_COMPLETED =
ReleasePhase._(6, _omitEnumNames ? '' : 'RELEASE_COMPLETED');
ReleasePhase._(3, _omitEnumNames ? '' : 'RELEASE_COMPLETED');
static const $core.List<ReleasePhase> values = <ReleasePhase>[
APPLY_ENGINE_CHERRYPICKS,
VERIFY_ENGINE_CI,
APPLY_FRAMEWORK_CHERRYPICKS,
PUBLISH_VERSION,
VERIFY_RELEASE,
@ -67,6 +61,9 @@ class CherrypickState extends $pb.ProtobufEnum {
const CherrypickState._($core.int v, $core.String n) : super(v, n);
}
/// The type of release that is being created.
///
/// This determines how the version will be calculated.
class ReleaseType extends $pb.ProtobufEnum {
static const ReleaseType STABLE_INITIAL =
ReleaseType._(0, _omitEnumNames ? '' : 'STABLE_INITIAL');

View File

@ -8,7 +8,7 @@
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
@ -21,24 +21,17 @@ import 'dart:typed_data' as $typed_data;
const ReleasePhase$json = {
'1': 'ReleasePhase',
'2': [
{'1': 'APPLY_ENGINE_CHERRYPICKS', '2': 0},
{'1': 'VERIFY_ENGINE_CI', '2': 1},
{'1': 'APPLY_FRAMEWORK_CHERRYPICKS', '2': 2},
{'1': 'PUBLISH_VERSION', '2': 3},
{'1': 'VERIFY_RELEASE', '2': 5},
{'1': 'RELEASE_COMPLETED', '2': 6},
],
'4': [
{'1': 4, '2': 4},
{'1': 'APPLY_FRAMEWORK_CHERRYPICKS', '2': 0},
{'1': 'PUBLISH_VERSION', '2': 1},
{'1': 'VERIFY_RELEASE', '2': 2},
{'1': 'RELEASE_COMPLETED', '2': 3},
],
};
/// Descriptor for `ReleasePhase`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List releasePhaseDescriptor = $convert
.base64Decode('CgxSZWxlYXNlUGhhc2USHAoYQVBQTFlfRU5HSU5FX0NIRVJSWVBJQ0tTEAASFAoQVkVSSUZZX0'
'VOR0lORV9DSRABEh8KG0FQUExZX0ZSQU1FV09SS19DSEVSUllQSUNLUxACEhMKD1BVQkxJU0hf'
'VkVSU0lPThADEhIKDlZFUklGWV9SRUxFQVNFEAUSFQoRUkVMRUFTRV9DT01QTEVURUQQBiIECA'
'QQBA==');
.base64Decode('CgxSZWxlYXNlUGhhc2USHwobQVBQTFlfRlJBTUVXT1JLX0NIRVJSWVBJQ0tTEAASEwoPUFVCTE'
'lTSF9WRVJTSU9OEAESEgoOVkVSSUZZX1JFTEVBU0UQAhIVChFSRUxFQVNFX0NPTVBMRVRFRBAD');
@$core.Deprecated('Use cherrypickStateDescriptor instead')
const CherrypickState$json = {

View File

@ -8,7 +8,7 @@
//
// @dart = 2.12
// ignore_for_file: annotate_overrides, camel_case_types
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_final_fields

View File

@ -9,24 +9,16 @@ message Remote {
}
enum ReleasePhase {
// Release was started with `conductor start` and repositories cloned.
APPLY_ENGINE_CHERRYPICKS = 0;
// Verify engine CI is green before opening framework PR.
VERIFY_ENGINE_CI = 1;
APPLY_FRAMEWORK_CHERRYPICKS = 2;
APPLY_FRAMEWORK_CHERRYPICKS = 0;
// Git tag applied to framework RC branch HEAD and pushed upstream.
PUBLISH_VERSION = 3;
reserved 4; // Formerly PUBLISH_CHANNEL, merged into PUBLISH_VERSION.
PUBLISH_VERSION = 1;
// Package artifacts verified to exist on cloud storage.
VERIFY_RELEASE = 5;
VERIFY_RELEASE = 2;
// There is no further work to be done.
RELEASE_COMPLETED = 6;
RELEASE_COMPLETED = 3;
}
enum CherrypickState {
@ -100,7 +92,7 @@ message Repository {
// Desired cherrypicks.
repeated Cherrypick cherrypicks = 7;
// Only for engine repositories.
// For the repository that has a dart_revision in a DEPS file.
string dartRevision = 8;
// Name of local and remote branch for applying cherrypicks.

View File

@ -683,33 +683,9 @@ class FrameworkRepository extends Repository {
return true;
}
/// Update this framework's engine version file.
///
/// Returns [true] if the version file was updated and a commit is needed.
Future<bool> updateEngineRevision(
String newEngine, {
@visibleForTesting File? engineVersionFile,
}) async {
assert(newEngine.isNotEmpty);
engineVersionFile ??= (await checkoutDirectory)
.childDirectory('bin')
.childDirectory('internal')
.childFile('engine.version');
assert(engineVersionFile.existsSync());
final String oldEngine = engineVersionFile.readAsStringSync();
if (oldEngine.trim() == newEngine.trim()) {
stdio.printTrace(
'Tried to update the engine revision but version file is already up to date at: $newEngine',
);
return false;
}
stdio.printStatus('Updating engine revision from $oldEngine to $newEngine');
engineVersionFile.writeAsStringSync(
// Version files have trailing newlines
'${newEngine.trim()}\n',
flush: true,
);
return true;
/// Update the `dart_revision` entry in the DEPS file.
Future<void> updateDartRevision(String newRevision, {@visibleForTesting File? depsFile}) async {
return _updateDartRevision(this, newRevision, depsFile: depsFile);
}
}
@ -770,63 +746,45 @@ class HostFrameworkRepository extends FrameworkRepository {
}
}
class EngineRepository extends Repository {
EngineRepository(
this.checkouts, {
super.name = 'engine',
String super.initialRef = EngineRepository.defaultBranch,
super.upstreamRemote = const Remote.upstream(EngineRepository.defaultUpstream),
super.localUpstream,
super.previousCheckoutLocation,
super.mirrorRemote,
List<String>? additionalRequiredLocalBranches,
}) : super(
fileSystem: checkouts.fileSystem,
parentDirectory: checkouts.directory,
platform: checkouts.platform,
processManager: checkouts.processManager,
stdio: checkouts.stdio,
requiredLocalBranches: additionalRequiredLocalBranches ?? const <String>[],
);
final Checkouts checkouts;
static const String defaultUpstream = 'git@github.com:flutter/engine.git';
static const String defaultBranch = 'main';
/// Update the `dart_revision` entry in the DEPS file.
Future<void> updateDartRevision(String newRevision, {@visibleForTesting File? depsFile}) async {
assert(newRevision.length == 40);
depsFile ??= (await checkoutDirectory).childFile('DEPS');
final String fileContent = depsFile.readAsStringSync();
final RegExp dartPattern = RegExp("[ ]+'dart_revision': '([a-z0-9]{40})',");
final Iterable<RegExpMatch> allMatches = dartPattern.allMatches(fileContent);
if (allMatches.length != 1) {
throw ConductorException(
'Unexpected content in the DEPS file at ${depsFile.path}\n'
'Expected to find pattern ${dartPattern.pattern} 1 times, but got '
'${allMatches.length}.',
);
}
final String updatedFileContent = fileContent.replaceFirst(
dartPattern,
" 'dart_revision': '$newRevision',",
);
depsFile.writeAsStringSync(updatedFileContent, flush: true);
}
@override
Future<Repository> cloneRepository(String? cloneName) async {
assert(localUpstream);
cloneName ??= 'clone-of-$name';
return EngineRepository(
checkouts,
name: cloneName,
upstreamRemote: Remote.upstream('file://${(await checkoutDirectory).path}/'),
);
}
}
//class EngineRepository extends Repository {
// EngineRepository(
// this.checkouts, {
// super.name = 'engine',
// String super.initialRef = EngineRepository.defaultBranch,
// super.upstreamRemote = const Remote.upstream(EngineRepository.defaultUpstream),
// super.localUpstream,
// super.previousCheckoutLocation,
// super.mirrorRemote,
// List<String>? additionalRequiredLocalBranches,
// }) : super(
// fileSystem: checkouts.fileSystem,
// parentDirectory: checkouts.directory,
// platform: checkouts.platform,
// processManager: checkouts.processManager,
// stdio: checkouts.stdio,
// requiredLocalBranches: additionalRequiredLocalBranches ?? const <String>[],
// );
//
// final Checkouts checkouts;
//
// static const String defaultUpstream = 'git@github.com:flutter/engine.git';
// static const String defaultBranch = 'main';
//
// /// Update the `dart_revision` entry in the DEPS file.
// Future<void> updateDartRevision(String newRevision, {@visibleForTesting File? depsFile}) =>
// _updateDartRevision(this, newRevision, depsFile: depsFile);
//
// @override
// Future<Repository> cloneRepository(String? cloneName) async {
// assert(localUpstream);
// cloneName ??= 'clone-of-$name';
// return EngineRepository(
// checkouts,
// name: cloneName,
// upstreamRemote: Remote.upstream('file://${(await checkoutDirectory).path}/'),
// );
// }
//}
/// An enum of all the repositories that the Conductor supports.
enum RepositoryType { framework, engine }
@ -851,3 +809,28 @@ class Checkouts {
final ProcessManager processManager;
final Stdio stdio;
}
Future<void> _updateDartRevision(
Repository repo,
String newRevision, {
@visibleForTesting File? depsFile,
}) async {
assert(newRevision.length == 40);
depsFile ??= (await repo.checkoutDirectory).childFile('DEPS');
final String fileContent = depsFile.readAsStringSync();
final RegExp dartPattern = RegExp("[ ]+'dart_revision': '([a-z0-9]{40})',");
final Iterable<RegExpMatch> allMatches = dartPattern.allMatches(fileContent);
if (allMatches.length != 1) {
throw ConductorException(
'Unexpected content in the DEPS file at ${depsFile.path}\n'
'Expected to find pattern ${dartPattern.pattern} 1 times, but got '
'${allMatches.length}.',
);
}
final String updatedFileContent = fileContent.replaceFirst(
dartPattern,
" 'dart_revision': '$newRevision',",
);
depsFile.writeAsStringSync(updatedFileContent, flush: true);
}

View File

@ -65,12 +65,6 @@ class StartCommand extends Command<void> {
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(
kStateOption,
defaultsTo: defaultPath,
@ -119,9 +113,6 @@ class StartCommand extends Command<void> {
allowNull: true,
) ??
'git@github.com:$githubUsername/flutter.git';
final String engineUpstream =
getValueFromEnvOrArgs(kEngineUpstreamOption, argumentResults, platform.environment)!;
final String engineMirror = 'git@github.com:$githubUsername/engine.git';
final String candidateBranch =
getValueFromEnvOrArgs(kCandidateOption, argumentResults, platform.environment)!;
final String releaseChannel =
@ -151,8 +142,6 @@ class StartCommand extends Command<void> {
candidateBranch: candidateBranch,
checkouts: checkouts,
dartRevision: dartRevision,
engineMirror: engineMirror,
engineUpstream: engineUpstream,
conductorVersion: conductorVersion,
frameworkMirror: frameworkMirror,
frameworkUpstream: frameworkUpstream,
@ -183,8 +172,6 @@ class StartContext extends Context {
StartContext({
required this.candidateBranch,
required this.dartRevision,
required this.engineMirror,
required this.engineUpstream,
required this.frameworkMirror,
required this.frameworkUpstream,
required this.conductorVersion,
@ -196,12 +183,6 @@ class StartContext extends Context {
this.force = false,
this.versionOverride,
}) : git = Git(processManager),
engine = EngineRepository(
checkouts,
initialRef: 'upstream/$candidateBranch',
upstreamRemote: Remote.upstream(engineUpstream),
mirrorRemote: Remote.mirror(engineMirror),
),
framework = FrameworkRepository(
checkouts,
initialRef: 'upstream/$candidateBranch',
@ -211,8 +192,6 @@ class StartContext extends Context {
final String candidateBranch;
final String? dartRevision;
final String engineMirror;
final String engineUpstream;
final String frameworkMirror;
final String frameworkUpstream;
final String conductorVersion;
@ -225,7 +204,6 @@ class StartContext extends Context {
/// If validations should be overridden.
final bool force;
final EngineRepository engine;
final FrameworkRepository framework;
/// Determine which part of the version to increment in the next release.
@ -272,71 +250,8 @@ class StartContext extends Context {
// Create a new branch so that we don't accidentally push to upstream
// candidateBranch.
final String workingBranchName = 'cherrypicks-$candidateBranch';
await engine.newBranch(workingBranchName);
if (dartRevision != null && dartRevision!.isNotEmpty) {
await engine.updateDartRevision(dartRevision!);
await engine.commit('Update Dart SDK to $dartRevision', addFirst: true);
}
final String engineHead = await engine.reverseParse('HEAD');
state.engine =
(pb.Repository.create()
..candidateBranch = candidateBranch
..workingBranch = workingBranchName
..startingGitHead = engineHead
..currentGitHead = engineHead
..checkoutPath = (await engine.checkoutDirectory).path
..upstream =
(pb.Remote.create()
..name = 'upstream'
..url = engine.upstreamRemote.url)
..mirror =
(pb.Remote.create()
..name = 'mirror'
..url = engine.mirrorRemote!.url));
if (dartRevision != null && dartRevision!.isNotEmpty) {
state.engine.dartRevision = dartRevision!;
}
await framework.newBranch(workingBranchName);
// Get framework version
final Version lastVersion = Version.fromString(
await framework.getFullTag(framework.upstreamRemote.name, candidateBranch, exact: false),
);
final String frameworkHead = await framework.reverseParse('HEAD');
final String branchPoint = await framework.branchPoint(
'${framework.upstreamRemote.name}/$candidateBranch',
'${framework.upstreamRemote.name}/${FrameworkRepository.defaultBranch}',
);
final bool atBranchPoint = branchPoint == frameworkHead;
final ReleaseType releaseType = computeReleaseType(lastVersion, atBranchPoint);
state.releaseType = releaseType;
try {
lastVersion.ensureValid(candidateBranch, releaseType);
} on ConductorException catch (e) {
// Let the user know, but resume execution
stdio.printError(e.message);
}
Version nextVersion;
if (versionOverride != null) {
nextVersion = versionOverride!;
} else {
nextVersion = calculateNextVersion(lastVersion, releaseType);
nextVersion = await ensureBranchPointTagged(
branchPoint: branchPoint,
requestedVersion: nextVersion,
framework: framework,
);
}
state.releaseVersion = nextVersion.toString();
state.framework =
(pb.Repository.create()
..candidateBranch = candidateBranch
@ -353,7 +268,46 @@ class StartContext extends Context {
..name = 'mirror'
..url = framework.mirrorRemote!.url));
state.currentPhase = ReleasePhase.APPLY_ENGINE_CHERRYPICKS;
if (dartRevision != null && dartRevision!.isNotEmpty) {
// In the monorepo, the DEPS file is in flutter/flutter
state.framework.dartRevision = dartRevision!;
}
// Get framework version
final Version lastVersion = Version.fromString(
await framework.getFullTag(framework.upstreamRemote.name, candidateBranch, exact: false),
);
final String branchPoint = await framework.branchPoint(
'${framework.upstreamRemote.name}/$candidateBranch',
'${framework.upstreamRemote.name}/${FrameworkRepository.defaultBranch}',
);
final bool atBranchPoint = branchPoint == frameworkHead;
final ReleaseType releaseType = computeReleaseType(lastVersion, atBranchPoint);
state.releaseType = releaseType;
Version nextVersion;
if (versionOverride != null) {
nextVersion = versionOverride!;
} else {
nextVersion = calculateNextVersion(lastVersion, releaseType);
nextVersion = await ensureBranchPointTagged(
branchPoint: branchPoint,
requestedVersion: nextVersion,
framework: framework,
);
}
await framework.newBranch(workingBranchName);
if (dartRevision != null && dartRevision!.isNotEmpty) {
await framework.updateDartRevision(dartRevision!);
await framework.commit('Update Dart SDK to $dartRevision', addFirst: true);
}
state.releaseVersion = nextVersion.toString();
state.currentPhase = ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS;
state.conductorVersion = conductorVersion;

View File

@ -82,26 +82,6 @@ String presentState(pb.ConductorState state) {
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.engine.candidateBranch, '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.');
}
if (state.engine.dartRevision.isNotEmpty) {
buffer.writeln('New Dart SDK revision: ${state.engine.dartRevision}');
}
buffer.writeln('Framework Repo');
buffer.writeln('\tCandidate branch: ${state.framework.candidateBranch}');
buffer.writeln('\tStarting git HEAD: ${state.framework.startingGitHead}');
@ -118,6 +98,9 @@ String presentState(pb.ConductorState state) {
} else {
buffer.writeln('0 Framework cherrypicks.');
}
if (state.framework.dartRevision.isNotEmpty) {
buffer.writeln('New Dart SDK revision: ${state.engine.dartRevision}');
}
buffer.writeln();
if (state.currentPhase == ReleasePhase.VERIFY_RELEASE) {
buffer.writeln(
@ -156,43 +139,6 @@ String presentPhases(ReleasePhase currentPhase) {
String phaseInstructions(pb.ConductorState state) {
switch (state.currentPhase) {
case ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
if (state.engine.cherrypicks.isEmpty) {
return <String>[
'There are no engine cherrypicks, so issue `conductor next` to continue',
'to the next step.',
'\n',
'******************************************************',
'* Create a new entry in http://go/release-eng-retros *',
'******************************************************',
].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 ${globals.kReleaseDocumentationUrl} for more information.',
].join('\n');
case ReleasePhase.VERIFY_ENGINE_CI:
if (!requiresEnginePR(state)) {
return 'You must verify engine CI has passed: '
'${luciConsoleLink(state.engine.candidateBranch, 'engine')}';
}
// User's working branch was pushed to their mirror, but a PR needs to be
// opened on GitHub.
final String newPrLink = globals.getNewPrLink(
userName: githubAccount(state.engine.mirror.url),
repoName: 'engine',
state: state,
);
final String consoleLink = luciConsoleLink(state.engine.candidateBranch, 'engine');
return <String>[
'Your working branch ${state.engine.workingBranch} was pushed to your mirror.',
'You must now open a pull request at $newPrLink, verify pre-submit CI',
'builds on your engine pull request are successful, merge your pull request,',
'validate post-submit CI at $consoleLink.',
].join('\n');
case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
final List<pb.Cherrypick> outstandingCherrypicks =
state.framework.cherrypicks.where((pb.Cherrypick cp) {
@ -280,8 +226,6 @@ ReleasePhase getNextPhase(ReleasePhase currentPhase) {
switch (currentPhase) {
case ReleasePhase.PUBLISH_VERSION:
return ReleasePhase.VERIFY_RELEASE;
case ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
case ReleasePhase.VERIFY_ENGINE_CI:
case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
case ReleasePhase.VERIFY_RELEASE:
case ReleasePhase.RELEASE_COMPLETED:

View File

@ -22,11 +22,8 @@ void main() {
const String checkoutsParentDirectory = '$flutterRoot/dev/conductor';
const String candidateBranch = 'flutter-1.2-candidate.3';
const String workingBranch = 'cherrypicks-$candidateBranch';
const String remoteUrl = 'https://github.com/org/repo.git';
const String revision1 = 'd3af60d18e01fcb36e0c0fa06c8502e4935ed095';
const String revision2 = 'f99555c1e1392bf2a8135056b9446680c2af4ddf';
const String revision3 = 'ffffffffffffffffffffffffffffffffffffffff';
const String revision4 = '280e23318a0d8341415c66aa32581352a421d974';
const String releaseVersion = '1.2.0-3.0.pre';
const String releaseChannel = 'beta';
const String stateFile = '/state-file.json';
@ -73,226 +70,6 @@ void main() {
);
});
group('APPLY_ENGINE_CHERRYPICKS to VERIFY_ENGINE_CI', () {
test('confirms to stdout when all engine cherrypicks were auto-applied', () async {
stdio.stdin.add('n');
final File ciYaml = fileSystem.file('$checkoutsParentDirectory/engine/.ci.yaml')
..createSync(recursive: true);
_initializeCiYamlFile(ciYaml);
final FakeProcessManager processManager = FakeProcessManager.empty();
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state =
(pb.ConductorState.create()
..releaseChannel = releaseChannel
..engine =
(pb.Repository.create()
..candidateBranch = candidateBranch
..cherrypicks.add(
pb.Cherrypick.create()
..trunkRevision = 'abc123'
..state = pb.CherrypickState.COMPLETED,
)
..checkoutPath = fileSystem.path.join(checkoutsParentDirectory, 'engine')
..workingBranch = workingBranch
..upstream =
(pb.Remote.create()
..name = 'upstream'
..url = remoteUrl)
..mirror =
(pb.Remote.create()
..name = 'mirror'
..url = remoteUrl))
..currentPhase = ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
writeStateToFile(fileSystem.file(stateFile), state, <String>[]);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)
..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>['next', '--$kStateOption', stateFile]);
expect(processManager, hasNoRemainingExpectations);
expect(
stdio.stdout,
contains('All engine cherrypicks have been auto-applied by the conductor'),
);
});
test('updates lastPhase if user responds yes', () async {
const String remoteUrl = 'https://github.com/org/repo.git';
const String releaseChannel = 'beta';
stdio.stdin.add('y');
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(command: <String>['git', 'fetch', 'upstream']),
FakeCommand(
command: const <String>['git', 'checkout', workingBranch],
onRun: (_) {
final File file = fileSystem.file('$checkoutsParentDirectory/engine/.ci.yaml')
..createSync(recursive: true);
_initializeCiYamlFile(file);
},
),
const FakeCommand(
command: <String>['git', 'push', 'mirror', 'HEAD:refs/heads/$workingBranch'],
),
]);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state =
(pb.ConductorState.create()
..currentPhase = ReleasePhase.APPLY_ENGINE_CHERRYPICKS
..engine =
(pb.Repository.create()
..candidateBranch = candidateBranch
..checkoutPath = fileSystem.path.join(checkoutsParentDirectory, 'engine')
..cherrypicks.add(
pb.Cherrypick.create()
..trunkRevision = revision2
..state = pb.CherrypickState.PENDING,
)
..workingBranch = workingBranch
..upstream =
(pb.Remote.create()
..name = 'upstream'
..url = remoteUrl)
..mirror =
(pb.Remote.create()
..name = 'mirror'
..url = remoteUrl))
..releaseChannel = releaseChannel
..releaseVersion = releaseVersion);
writeStateToFile(fileSystem.file(stateFile), state, <String>[]);
// engine dir is expected to already exist
fileSystem
.directory(checkoutsParentDirectory)
.childDirectory('engine')
.createSync(recursive: true);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>['next', '--$kStateOption', stateFile]);
final pb.ConductorState finalState = readStateFromFile(fileSystem.file(stateFile));
expect(processManager, hasNoRemainingExpectations);
expect(
stdio.stdout,
contains(
'You must now open a pull request at https://github.com/flutter/engine/compare/flutter-1.2-candidate.3...org:cherrypicks-flutter-1.2-candidate.3?expand=1',
),
);
expect(
stdio.stdout,
contains(
'Are you ready to push your engine branch to the repository $remoteUrl? (y/n) ',
),
);
expect(finalState.currentPhase, ReleasePhase.VERIFY_ENGINE_CI);
expect(stdio.error, isEmpty);
});
});
group('VERIFY_ENGINE_CI to APPLY_FRAMEWORK_CHERRYPICKS', () {
late pb.ConductorState state;
late FakeProcessManager processManager;
late FakePlatform platform;
setUp(() {
state =
(pb.ConductorState.create()
..releaseChannel = releaseChannel
..engine =
(pb.Repository.create()
..cherrypicks.add(
pb.Cherrypick.create()
..trunkRevision = 'abc123'
..state = pb.CherrypickState.PENDING,
)
..candidateBranch = 'flutter-1.0-candidate.0')
..currentPhase = ReleasePhase.VERIFY_ENGINE_CI);
processManager = FakeProcessManager.empty();
platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
});
test('does not update currentPhase if user responds no', () async {
stdio.stdin.add('n');
writeStateToFile(fileSystem.file(stateFile), state, <String>[]);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)
..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>['next', '--$kStateOption', stateFile]);
final pb.ConductorState finalState = readStateFromFile(fileSystem.file(stateFile));
expect(processManager, hasNoRemainingExpectations);
expect(stdio.stdout, contains('Has CI passed for the engine PR?'));
expect(finalState.currentPhase, ReleasePhase.VERIFY_ENGINE_CI);
expect(stdio.error.contains('Aborting command.'), true);
});
test('updates currentPhase if user responds yes', () async {
stdio.stdin.add('y');
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
writeStateToFile(fileSystem.file(stateFile), state, <String>[]);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)
..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>['next', '--$kStateOption', stateFile]);
final pb.ConductorState finalState = readStateFromFile(fileSystem.file(stateFile));
expect(processManager, hasNoRemainingExpectations);
expect(stdio.stdout, contains('Has CI passed for the engine PR?'));
expect(finalState.currentPhase, ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS);
});
});
group('APPLY_FRAMEWORK_CHERRYPICKS to PUBLISH_VERSION', () {
const String mirrorRemoteUrl = 'https://github.com/org/repo.git';
const String upstreamRemoteUrl = 'https://github.com/mirror/repo.git';
@ -359,218 +136,9 @@ void main() {
engineRevisionFile.writeAsStringSync(oldEngineVersion, flush: true);
});
test(
'with no dart, engine or framework cherrypicks, updates engine revision if version mismatch',
() async {
stdio.stdin.add('n');
processManager.addCommands(<FakeCommand>[
const FakeCommand(command: <String>['git', 'fetch', 'upstream']),
// we want merged upstream commit, not local working commit
const FakeCommand(command: <String>['git', 'checkout', 'upstream/$candidateBranch']),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision1),
const FakeCommand(command: <String>['git', 'fetch', 'upstream']),
FakeCommand(
command: const <String>['git', 'checkout', workingBranch],
onRun: (_) {
final File file = fileSystem.file('$checkoutsParentDirectory/framework/.ci.yaml')
..createSync();
_initializeCiYamlFile(file);
},
),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM bin/internal/release-candidate-branch.version',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>[
'git',
'commit',
'--message',
'Create candidate branch version $candidateBranch for $releaseChannel',
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM bin/internal/engine.version',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>[
'git',
'commit',
'--message',
'Update Engine revision to $revision1 for $releaseChannel release $releaseVersion',
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision4),
]);
final pb.ConductorState state =
(pb.ConductorState.create()
..releaseChannel = releaseChannel
..releaseVersion = releaseVersion
..currentPhase = ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS
..framework =
(pb.Repository.create()
..candidateBranch = candidateBranch
..checkoutPath = frameworkCheckoutPath
..mirror =
(pb.Remote.create()
..name = 'mirror'
..url = mirrorRemoteUrl)
..upstream =
(pb.Remote.create()
..name = 'upstream'
..url = upstreamRemoteUrl)
..workingBranch = workingBranch)
..engine =
(pb.Repository.create()
..candidateBranch = candidateBranch
..checkoutPath = engineCheckoutPath
..upstream =
(pb.Remote.create()
..name = 'upstream'
..url = engineUpstreamRemoteUrl)
..currentGitHead = revision1));
writeStateToFile(fileSystem.file(stateFile), state, <String>[]);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)
..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>['next', '--$kStateOption', stateFile]);
expect(processManager, hasNoRemainingExpectations);
expect(
stdio.stdout,
contains('release-candidate-branch.version containing $candidateBranch'),
);
expect(
stdio.stdout,
contains('Updating engine revision from $oldEngineVersion to $revision1'),
);
expect(stdio.stdout, contains('Are you ready to push your framework branch'));
},
);
test(
'with no engine cherrypicks but a dart revision update, updates engine revision',
() async {
stdio.stdin.add('n');
processManager.addCommands(<FakeCommand>[
const FakeCommand(command: <String>['git', 'fetch', 'upstream']),
// we want merged upstream commit, not local working commit
const FakeCommand(command: <String>['git', 'checkout', 'upstream/$candidateBranch']),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision1),
const FakeCommand(command: <String>['git', 'fetch', 'upstream']),
FakeCommand(
command: const <String>['git', 'checkout', workingBranch],
onRun: (_) {
final File file = fileSystem.file('$checkoutsParentDirectory/framework/.ci.yaml')
..createSync();
_initializeCiYamlFile(file);
},
),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM bin/internal/release-candidate-branch.version',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>[
'git',
'commit',
'--message',
'Create candidate branch version $candidateBranch for $releaseChannel',
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM bin/internal/engine.version',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>[
'git',
'commit',
'--message',
'Update Engine revision to $revision1 for $releaseChannel release $releaseVersion',
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision4),
]);
final pb.ConductorState state =
(pb.ConductorState.create()
..releaseChannel = releaseChannel
..releaseVersion = releaseVersion
..currentPhase = ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS
..framework =
(pb.Repository.create()
..candidateBranch = candidateBranch
..checkoutPath = frameworkCheckoutPath
..mirror =
(pb.Remote.create()
..name = 'mirror'
..url = mirrorRemoteUrl)
..upstream =
(pb.Remote.create()
..name = 'upstream'
..url = upstreamRemoteUrl)
..workingBranch = workingBranch)
..engine =
(pb.Repository.create()
..candidateBranch = candidateBranch
..checkoutPath = engineCheckoutPath
..upstream =
(pb.Remote.create()
..name = 'upstream'
..url = engineUpstreamRemoteUrl)
..dartRevision = 'abc123'));
writeStateToFile(fileSystem.file(stateFile), state, <String>[]);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)
..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>['next', '--$kStateOption', stateFile]);
expect(processManager, hasNoRemainingExpectations);
expect(
stdio.stdout,
contains('release-candidate-branch.version containing $candidateBranch'),
);
expect(
stdio.stdout,
contains('Updating engine revision from $oldEngineVersion to $revision1'),
);
expect(stdio.stdout, contains('Are you ready to push your framework branch'));
},
);
test('does not update state.currentPhase if user responds no', () async {
stdio.stdin.add('n');
processManager.addCommands(<FakeCommand>[
const FakeCommand(command: <String>['git', 'fetch', 'upstream']),
// we want merged upstream commit, not local working commit
FakeCommand(
command: const <String>['git', 'checkout', 'upstream/$candidateBranch'],
onRun: (_) {
final File file = fileSystem.file('$checkoutsParentDirectory/framework/.ci.yaml')
..createSync();
_initializeCiYamlFile(file);
},
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision1),
const FakeCommand(command: <String>['git', 'fetch', 'upstream']),
const FakeCommand(command: <String>['git', 'checkout', workingBranch]),
const FakeCommand(
@ -587,20 +155,6 @@ void main() {
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM bin/internal/engine.version',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>[
'git',
'commit',
'--message',
'Update Engine revision to $revision1 for $releaseChannel release $releaseVersion',
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision4),
]);
writeStateToFile(fileSystem.file(stateFile), state, <String>[]);
final Checkouts checkouts = Checkouts(
@ -616,6 +170,7 @@ void main() {
final pb.ConductorState finalState = readStateFromFile(fileSystem.file(stateFile));
expect(processManager, hasNoRemainingExpectations);
expect(
stdio.stdout,
contains(
@ -631,11 +186,6 @@ void main() {
processManager.addCommands(<FakeCommand>[
// Engine repo
const FakeCommand(command: <String>['git', 'fetch', 'upstream']),
// we want merged upstream commit, not local working commit
const FakeCommand(command: <String>['git', 'checkout', 'upstream/$candidateBranch']),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision1),
// Framework repo
const FakeCommand(command: <String>['git', 'fetch', 'upstream']),
FakeCommand(
command: const <String>['git', 'checkout', workingBranch],
onRun: (_) {
@ -658,20 +208,6 @@ void main() {
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM bin/internal/engine.version',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>[
'git',
'commit',
'--message',
'Update Engine revision to $revision1 for $releaseChannel release $releaseVersion',
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision4),
const FakeCommand(
command: <String>['git', 'push', 'mirror', 'HEAD:refs/heads/$workingBranch'],
),
@ -691,10 +227,6 @@ void main() {
final pb.ConductorState finalState = readStateFromFile(fileSystem.file(stateFile));
expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION);
expect(
stdio.stdout,
contains('Rolling new engine hash $revision1 to framework checkout...'),
);
expect(stdio.stdout, contains('There was 1 cherrypick that was not auto-applied'));
expect(
stdio.stdout,
@ -723,10 +255,6 @@ void main() {
(pb.Repository.create()
..candidateBranch = candidateBranch
..upstream = (pb.Remote.create()..url = FrameworkRepository.defaultUpstream))
..engine =
(pb.Repository.create()
..candidateBranch = candidateBranch
..upstream = (pb.Remote.create()..url = EngineRepository.defaultUpstream))
..releaseVersion = releaseVersion);
});

View File

@ -43,7 +43,7 @@ void main() {
stdio: stdio,
);
final EngineRepository repo = EngineRepository(checkouts);
final FrameworkRepository repo = FrameworkRepository(checkouts);
final File depsFile = fileSystem.file('/DEPS');
depsFile.writeAsStringSync(generateMockDeps(previousDartRevision));
await repo.updateDartRevision(nextDartRevision, depsFile: depsFile);
@ -62,7 +62,7 @@ void main() {
stdio: stdio,
);
final EngineRepository repo = EngineRepository(checkouts);
final FrameworkRepository repo = FrameworkRepository(checkouts);
final File depsFile = fileSystem.file('/DEPS');
depsFile.writeAsStringSync('''
vars = {
@ -85,11 +85,11 @@ vars = {
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
fileSystem.path.join(rootDir, 'flutter_conductor_checkouts', 'engine'),
FrameworkRepository.defaultUpstream,
fileSystem.path.join(rootDir, 'flutter_conductor_checkouts', 'framework'),
],
),
const FakeCommand(command: <String>['git', 'checkout', EngineRepository.defaultBranch]),
const FakeCommand(command: <String>['git', 'checkout', FrameworkRepository.defaultBranch]),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: commit1),
const FakeCommand(command: <String>['git', 'status', '--porcelain']),
const FakeCommand(command: <String>['git', 'commit', '--message', message]),
@ -104,7 +104,7 @@ vars = {
stdio: stdio,
);
final EngineRepository repo = EngineRepository(checkouts);
final FrameworkRepository repo = FrameworkRepository(checkouts);
expect(
() async => repo.commit(message),
throwsExceptionWith('Tried to commit with message $message but no changes were present'),
@ -123,11 +123,11 @@ vars = {
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
fileSystem.path.join(rootDir, 'flutter_conductor_checkouts', 'engine'),
FrameworkRepository.defaultUpstream,
fileSystem.path.join(rootDir, 'flutter_conductor_checkouts', 'framework'),
],
),
const FakeCommand(command: <String>['git', 'checkout', EngineRepository.defaultBranch]),
const FakeCommand(command: <String>['git', 'checkout', FrameworkRepository.defaultBranch]),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: commit1),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
@ -145,7 +145,7 @@ vars = {
stdio: stdio,
);
final EngineRepository repo = EngineRepository(checkouts);
final FrameworkRepository repo = FrameworkRepository(checkouts);
await repo.commit(message);
expect(processManager.hasRemainingExpectations, false);
});
@ -174,41 +174,6 @@ vars = {
},
);
test('updateEngineRevision() returns false if newCommit is the same as version file', () async {
const String commit1 = 'abc123';
const String commit2 = 'def456';
final File engineVersionFile = fileSystem.file('/engine.version')..writeAsStringSync(commit2);
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
FrameworkRepository.defaultUpstream,
fileSystem.path.join(rootDir, 'flutter_conductor_checkouts', 'framework'),
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: commit1),
]);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(rootDir),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final FrameworkRepository repo = FrameworkRepository(checkouts);
final bool didUpdate = await repo.updateEngineRevision(
commit2,
engineVersionFile: engineVersionFile,
);
expect(didUpdate, false);
});
test(
'framework repo set as localUpstream ensures requiredLocalBranches exist locally',
() async {
@ -262,49 +227,6 @@ vars = {
},
);
test('engine repo set as localUpstream ensures requiredLocalBranches exist locally', () async {
const String commit = 'deadbeef';
const String candidateBranch = 'flutter-1.2-candidate.3';
bool createdCandidateBranch = false;
processManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
fileSystem.path.join(rootDir, 'flutter_conductor_checkouts', 'engine'),
],
),
FakeCommand(
command: const <String>['git', 'checkout', candidateBranch, '--'],
onRun: (_) => createdCandidateBranch = true,
),
const FakeCommand(command: <String>['git', 'checkout', EngineRepository.defaultBranch]),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: commit),
]);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(rootDir),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final Repository repo = EngineRepository(
checkouts,
additionalRequiredLocalBranches: <String>[candidateBranch],
localUpstream: true,
);
// call this so that repo.lazilyInitialize() is called.
await repo.checkoutDirectory;
expect(processManager.hasRemainingExpectations, false);
expect(createdCandidateBranch, true);
});
test('.listRemoteBranches() parses git output', () async {
const String remoteName = 'mirror';
const String lsRemoteOutput = '''
@ -326,11 +248,14 @@ Extraneous debug information that should be ignored.
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
'${rootDir}flutter_conductor_checkouts/engine',
FrameworkRepository.defaultUpstream,
'${rootDir}flutter_conductor_checkouts/framework',
],
),
FakeCommand(command: <String>['git', 'checkout', 'main']),
FakeCommand(command: <String>['git', 'checkout', 'stable', '--']),
FakeCommand(command: <String>['git', 'checkout', 'beta', '--']),
FakeCommand(command: <String>['git', 'checkout', 'master', '--']),
FakeCommand(command: <String>['git', 'checkout', 'master']),
FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision),
FakeCommand(
command: <String>['git', 'ls-remote', '--heads', remoteName],
@ -345,7 +270,7 @@ Extraneous debug information that should be ignored.
stdio: stdio,
);
final Repository repo = EngineRepository(checkouts, localUpstream: true);
final Repository repo = FrameworkRepository(checkouts, localUpstream: true);
final List<String> branchNames = await repo.listRemoteBranches(remoteName);
expect(
branchNames,

View File

@ -25,7 +25,6 @@ void main() {
const String checkoutsParentDirectory = '$flutterRoot/dev/tools/';
const String githubUsername = 'user';
const String frameworkMirror = 'git@github.com:$githubUsername/flutter.git';
const String engineMirror = 'git@github.com:$githubUsername/engine.git';
const String candidateBranch = 'flutter-1.2-candidate.3';
const String releaseChannel = 'beta';
const String revision = 'abcd1234';
@ -117,7 +116,6 @@ void main() {
expect(context.frameworkUpstream, FrameworkRepository.defaultUpstream);
expect(context.frameworkMirror, contains(githubUsername));
expect(context.frameworkMirror, contains('/flutter.git'));
expect(context.engineUpstream, EngineRepository.defaultUpstream);
});
test('overridden mirror', () async {
@ -202,50 +200,8 @@ void main() {
const String nextVersion = '1.2.0-1.1.pre';
const String candidateBranch = 'flutter-1.2-candidate.1';
final Directory engine = fileSystem
.directory(checkoutsParentDirectory)
.childDirectory('flutter_conductor_checkouts')
.childDirectory('engine');
final File depsFile = engine.childFile('DEPS');
final List<FakeCommand> engineCommands = <FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
engine.path,
],
onRun: (_) {
// Create the DEPS file which the tool will update
engine.createSync(recursive: true);
depsFile.writeAsStringSync(generateMockDeps(previousDartRevision));
},
),
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', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>['git', 'commit', '--message', 'Update Dart SDK to $nextDartRevision'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
];
final List<FakeCommand> frameworkCommands = <FakeCommand>[
final List<FakeCommand> commands = <FakeCommand>[
// clone and rev-parse framework
FakeCommand(
command: <String>[
'git',
@ -260,14 +216,35 @@ void main() {
'framework',
),
],
onRun: (_) {
// ensure this is a monorepo checkout
fileSystem
.directory(
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
'engine',
),
)
.createSync(recursive: true);
fileSystem
.file(
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
'DEPS',
),
)
.writeAsStringSync(generateMockDeps(previousDartRevision));
},
),
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),
const FakeCommand(
command: <String>[
'git',
@ -279,7 +256,7 @@ void main() {
],
stdout: '$previousVersion-42-gabc123',
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
//const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(
command: <String>['git', 'merge-base', 'upstream/$candidateBranch', 'upstream/master'],
stdout: branchPointRevision,
@ -288,11 +265,24 @@ void main() {
const FakeCommand(
command: <String>['git', 'describe', '--exact-match', '--tags', branchPointRevision],
),
const FakeCommand(
command: <String>['git', 'checkout', '-b', 'cherrypicks-$candidateBranch'],
),
// update DEPS
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>['git', 'commit', '--message', 'Update Dart SDK to $nextDartRevision'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
];
final CommandRunner<void> runner = createRunner(
commands: <FakeCommand>[...engineCommands, ...frameworkCommands],
);
final CommandRunner<void> runner = createRunner(commands: commands);
final String stateFilePath = fileSystem.path.join(
platform.environment['HOME']!,
@ -327,14 +317,10 @@ void main() {
expect(state.isInitialized(), true);
expect(state.releaseChannel, releaseChannel);
expect(state.releaseVersion, nextVersion);
expect(state.engine.candidateBranch, candidateBranch);
expect(state.engine.startingGitHead, revision2);
expect(state.engine.dartRevision, nextDartRevision);
expect(state.engine.upstream.url, 'git@github.com:flutter/engine.git');
expect(state.framework.candidateBranch, candidateBranch);
expect(state.framework.startingGitHead, revision3);
expect(state.framework.upstream.url, 'git@github.com:flutter/flutter.git');
expect(state.currentPhase, ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
expect(state.currentPhase, ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS);
expect(state.conductorVersion, conductorVersion);
});
@ -348,50 +334,8 @@ void main() {
const String candidateBranch = 'flutter-1.2-candidate.1';
const String versionOverride = '42.0.0-42.0.pre';
final Directory engine = fileSystem
.directory(checkoutsParentDirectory)
.childDirectory('flutter_conductor_checkouts')
.childDirectory('engine');
final File depsFile = engine.childFile('DEPS');
final List<FakeCommand> engineCommands = <FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
engine.path,
],
onRun: (_) {
// Create the DEPS file which the tool will update
engine.createSync(recursive: true);
depsFile.writeAsStringSync(generateMockDeps(previousDartRevision));
},
),
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', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>['git', 'commit', '--message', 'Update Dart SDK to $nextDartRevision'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
];
final List<FakeCommand> frameworkCommands = <FakeCommand>[
final List<FakeCommand> commands = <FakeCommand>[
// clone and rev-parse framework
FakeCommand(
command: <String>[
'git',
@ -406,14 +350,35 @@ void main() {
'framework',
),
],
onRun: (_) {
// ensure this is a monorepo checkout
fileSystem
.directory(
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
'engine',
),
)
.createSync(recursive: true);
fileSystem
.file(
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
'DEPS',
),
)
.writeAsStringSync(generateMockDeps(previousDartRevision));
},
),
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),
const FakeCommand(
command: <String>[
'git',
@ -425,16 +390,26 @@ void main() {
],
stdout: '$previousVersion-42-gabc123',
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(
command: <String>['git', 'merge-base', 'upstream/$candidateBranch', 'upstream/master'],
stdout: branchPointRevision,
),
const FakeCommand(
command: <String>['git', 'checkout', '-b', 'cherrypicks-$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>['git', 'commit', '--message', 'Update Dart SDK to $nextDartRevision'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
];
final CommandRunner<void> runner = createRunner(
commands: <FakeCommand>[...engineCommands, ...frameworkCommands],
);
final CommandRunner<void> runner = createRunner(commands: commands);
final String stateFilePath = fileSystem.path.join(
platform.environment['HOME']!,
@ -466,156 +441,6 @@ void main() {
expect(state.releaseVersion, versionOverride);
});
test('logs to STDERR but does not fail on an unexpected candidate branch', () async {
stdio.stdin.add('y'); // accept prompt from ensureBranchPointTagged()
const String revision2 = 'def789';
const String revision3 = '123abc';
const String previousDartRevision = '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
const String nextDartRevision = 'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e';
// This is significantly behind the candidate branch name
const String previousVersion = '0.9.0-1.0.pre';
// This is what this release will be
const String nextVersion = '0.9.0-1.1.pre';
final Directory engine = fileSystem
.directory(checkoutsParentDirectory)
.childDirectory('flutter_conductor_checkouts')
.childDirectory('engine');
final File depsFile = engine.childFile('DEPS');
final List<FakeCommand> engineCommands = <FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
engine.path,
],
onRun: (_) {
// Create the DEPS file which the tool will update
engine.createSync(recursive: true);
depsFile.writeAsStringSync(generateMockDeps(previousDartRevision));
},
),
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', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>['git', 'commit', '--message', 'Update Dart SDK to $nextDartRevision'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
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',
'describe',
'--match',
'*.*.*',
'--tags',
'refs/remotes/upstream/$candidateBranch',
],
stdout: '$previousVersion-42-gabc123',
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(
command: <String>['git', 'merge-base', 'upstream/$candidateBranch', 'upstream/master'],
stdout: branchPointRevision,
),
// check if commit is tagged, 0 exit code means it is tagged
const FakeCommand(
command: <String>['git', 'describe', '--exact-match', '--tags', branchPointRevision],
),
];
final CommandRunner<void> runner = createRunner(
commands: <FakeCommand>[...engineCommands, ...frameworkCommands],
);
final String stateFilePath = fileSystem.path.join(
platform.environment['HOME']!,
kStateFileName,
);
await runner.run(<String>[
'start',
'--$kCandidateOption',
candidateBranch,
'--$kReleaseOption',
releaseChannel,
'--$kStateOption',
stateFilePath,
'--$kDartRevisionOption',
nextDartRevision,
'--$kGithubUsernameOption',
githubUsername,
]);
final File stateFile = fileSystem.file(stateFilePath);
final pb.ConductorState state = pb.ConductorState();
state.mergeFromProto3Json(jsonDecode(stateFile.readAsStringSync()));
expect(stdio.error, isNot(contains('Tried to tag the branch point, however')));
expect(processManager, hasNoRemainingExpectations);
expect(state.isInitialized(), true);
expect(state.releaseChannel, releaseChannel);
expect(state.releaseVersion, nextVersion);
expect(state.engine.candidateBranch, candidateBranch);
expect(state.engine.startingGitHead, revision2);
expect(state.engine.dartRevision, nextDartRevision);
expect(state.engine.upstream.url, 'git@github.com:flutter/engine.git');
expect(state.framework.candidateBranch, candidateBranch);
expect(state.framework.startingGitHead, revision3);
expect(state.framework.upstream.url, 'git@github.com:flutter/flutter.git');
expect(state.currentPhase, ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
expect(state.conductorVersion, conductorVersion);
expect(state.releaseType, ReleaseType.BETA_HOTFIX);
expect(
stdio.error,
contains(
'Parsed version $previousVersion.42 has a different x value than candidate branch $candidateBranch',
),
);
});
test('can convert from dev style version to stable version', () async {
const String revision2 = 'def789';
const String revision3 = '123abc';
@ -624,50 +449,8 @@ void main() {
const String previousVersion = '1.2.0-3.0.pre';
const String nextVersion = '1.2.0';
final Directory engine = fileSystem
.directory(checkoutsParentDirectory)
.childDirectory('flutter_conductor_checkouts')
.childDirectory('engine');
final File depsFile = engine.childFile('DEPS');
final List<FakeCommand> engineCommands = <FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
engine.path,
],
onRun: (_) {
// Create the DEPS file which the tool will update
engine.createSync(recursive: true);
depsFile.writeAsStringSync(generateMockDeps(previousDartRevision));
},
),
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', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>['git', 'commit', '--message', 'Update Dart SDK to $nextDartRevision'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
];
final List<FakeCommand> frameworkCommands = <FakeCommand>[
final List<FakeCommand> commands = <FakeCommand>[
// rev-parse framework
FakeCommand(
command: <String>[
'git',
@ -682,14 +465,35 @@ void main() {
'framework',
),
],
onRun: (_) {
// ensure this is a monorepo checkout
fileSystem
.directory(
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
'engine',
),
)
.createSync(recursive: true);
fileSystem
.file(
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
'DEPS',
),
)
.writeAsStringSync(generateMockDeps(previousDartRevision));
},
),
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),
const FakeCommand(
command: <String>[
'git',
@ -701,7 +505,6 @@ void main() {
],
stdout: '$previousVersion-42-gabc123',
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(
command: <String>['git', 'merge-base', 'upstream/$candidateBranch', 'upstream/master'],
stdout: branchPointRevision,
@ -710,11 +513,22 @@ void main() {
const FakeCommand(
command: <String>['git', 'describe', '--exact-match', '--tags', branchPointRevision],
),
const FakeCommand(
command: <String>['git', 'checkout', '-b', 'cherrypicks-$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>['git', 'commit', '--message', 'Update Dart SDK to $nextDartRevision'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
];
final CommandRunner<void> runner = createRunner(
commands: <FakeCommand>[...engineCommands, ...frameworkCommands],
);
final CommandRunner<void> runner = createRunner(commands: commands);
final String stateFilePath = fileSystem.path.join(
platform.environment['HOME']!,
@ -740,23 +554,21 @@ void main() {
final pb.ConductorState state = pb.ConductorState();
state.mergeFromProto3Json(jsonDecode(stateFile.readAsStringSync()));
expect(processManager.hasRemainingExpectations, false);
expect(processManager, hasNoRemainingExpectations);
expect(state.isInitialized(), true);
expect(state.releaseChannel, 'stable');
expect(state.releaseVersion, nextVersion);
expect(state.engine.candidateBranch, candidateBranch);
expect(state.engine.startingGitHead, revision2);
expect(state.engine.dartRevision, nextDartRevision);
expect(state.framework.dartRevision, nextDartRevision);
expect(state.framework.candidateBranch, candidateBranch);
expect(state.framework.startingGitHead, revision3);
expect(state.currentPhase, ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
expect(state.currentPhase, ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS);
expect(state.conductorVersion, conductorVersion);
expect(state.releaseType, ReleaseType.STABLE_INITIAL);
});
test('StartContext gets engine and framework checkout directories after run', () async {
test('StartContext gets framework checkout directory after run', () async {
stdio.stdin.add('y');
const String revision2 = 'def789';
const String revision3 = '123abc';
const String branchPointRevision = 'deadbeef';
const String previousDartRevision = '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
const String nextDartRevision = 'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e';
@ -764,55 +576,13 @@ void main() {
// This is a git tag applied to the branch point, not an actual release
const String branchPointTag = '1.2.0-3.0.pre';
final Directory engine = fileSystem
.directory(checkoutsParentDirectory)
.childDirectory('flutter_conductor_checkouts')
.childDirectory('engine');
final Directory framework = fileSystem
.directory(checkoutsParentDirectory)
.childDirectory('flutter_conductor_checkouts')
.childDirectory('framework');
final File depsFile = engine.childFile('DEPS');
final List<FakeCommand> engineCommands = <FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
engine.path,
],
onRun: (_) {
// Create the DEPS file which the tool will update
engine.createSync(recursive: true);
depsFile.writeAsStringSync(generateMockDeps(previousDartRevision));
},
),
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', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>['git', 'commit', '--message', 'Update Dart SDK to $nextDartRevision'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
];
final List<FakeCommand> frameworkCommands = <FakeCommand>[
final List<FakeCommand> commands = <FakeCommand>[
// checkout and rev-parse framework
FakeCommand(
command: <String>[
'git',
@ -823,13 +593,41 @@ void main() {
FrameworkRepository.defaultUpstream,
framework.path,
],
onRun: (_) {
// ensure this is a monorepo checkout
fileSystem
.directory(
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
'engine',
),
)
.createSync(recursive: true);
fileSystem
.file(
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
'DEPS',
),
)
.writeAsStringSync(generateMockDeps(previousDartRevision));
},
),
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'],
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: branchPointRevision,
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: branchPointRevision,
),
const FakeCommand(
command: <String>[
@ -842,11 +640,6 @@ void main() {
],
stdout: '$previousVersion-42-gabc123',
),
// HEAD and branch point are same
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: branchPointRevision,
),
const FakeCommand(
command: <String>['git', 'merge-base', 'upstream/$candidateBranch', 'upstream/master'],
stdout: branchPointRevision,
@ -857,10 +650,25 @@ void main() {
// non-zero exit code means branch point is NOT tagged
exitCode: 128,
),
const FakeCommand(command: <String>['git', 'tag', branchPointTag, branchPointRevision]),
const FakeCommand(
command: <String>['git', 'push', FrameworkRepository.defaultUpstream, branchPointTag],
),
const FakeCommand(
command: <String>['git', 'checkout', '-b', 'cherrypicks-$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(command: <String>['git', 'add', '--all']),
const FakeCommand(
command: <String>['git', 'commit', '--message', 'Update Dart SDK to $nextDartRevision'],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
];
final String operatingSystem = const LocalPlatform().operatingSystem;
@ -876,10 +684,7 @@ void main() {
);
final File stateFile = fileSystem.file(stateFilePath);
processManager = FakeProcessManager.list(<FakeCommand>[
...engineCommands,
...frameworkCommands,
]);
processManager = FakeProcessManager.list(<FakeCommand>[...commands]);
checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory),
@ -892,8 +697,6 @@ void main() {
candidateBranch: candidateBranch,
checkouts: checkouts,
dartRevision: nextDartRevision,
engineMirror: engineMirror,
engineUpstream: EngineRepository.defaultUpstream,
frameworkMirror: frameworkMirror,
frameworkUpstream: FrameworkRepository.defaultUpstream,
releaseChannel: releaseChannel,
@ -908,7 +711,6 @@ void main() {
final pb.ConductorState state = pb.ConductorState();
state.mergeFromProto3Json(jsonDecode(stateFile.readAsStringSync()));
expect((await startContext.engine.checkoutDirectory).path, equals(engine.path));
expect((await startContext.framework.checkoutDirectory).path, equals(framework.path));
expect(state.releaseType, ReleaseType.BETA_INITIAL);
expect(processManager, hasNoRemainingExpectations);