Unverified Commit 1aad8c8c authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

[flutter_conductor] support pushing local changes to remote (#85797)

parent e9736efb
...@@ -104,29 +104,22 @@ void runNext({ ...@@ -104,29 +104,22 @@ void runNext({
} }
} }
if (state.engine.cherrypicks.isEmpty) { if (state.engine.cherrypicks.isEmpty && state.engine.dartRevision.isEmpty) {
stdio.printStatus('This release has no engine cherrypicks.'); stdio.printStatus(
'This release has no engine cherrypicks. No Engine PR is necessary.\n',
);
break; break;
} else if (unappliedCherrypicks.isEmpty) { }
if (unappliedCherrypicks.isEmpty) {
stdio.printStatus('All engine cherrypicks have been auto-applied by ' stdio.printStatus('All engine cherrypicks have been auto-applied by '
'the conductor.\n'); 'the conductor.\n');
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to push your changes to the repository '
'${state.engine.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
} else { } else {
stdio.printStatus( stdio.printStatus(
'There were ${unappliedCherrypicks.length} cherrypicks that were not auto-applied.'); 'There were ${unappliedCherrypicks.length} cherrypicks that were not auto-applied.');
stdio.printStatus('These must be applied manually in the directory ' stdio.printStatus('These must be applied manually in the directory '
'${state.engine.checkoutPath} before proceeding.\n'); '${state.engine.checkoutPath} before proceeding.\n');
}
if (autoAccept == false) { if (autoAccept == false) {
final bool response = prompt( final bool response = prompt(
'Are you ready to push your engine branch to the repository ' 'Are you ready to push your engine branch to the repository '
...@@ -139,9 +132,30 @@ void runNext({ ...@@ -139,9 +132,30 @@ void runNext({
return; return;
} }
} }
} final Remote upstream = Remote(
name: RemoteName.upstream,
url: state.engine.upstream.url,
);
final EngineRepository engine = EngineRepository(
checkouts,
initialRef: state.engine.workingBranch,
upstreamRemote: upstream,
previousCheckoutLocation: state.engine.checkoutPath,
);
final String headRevision = engine.reverseParse('HEAD');
engine.pushRef(
fromRef: headRevision,
toRef: state.engine.workingBranch,
remote: state.engine.mirror.name,
);
break; break;
case pb.ReleasePhase.CODESIGN_ENGINE_BINARIES: case pb.ReleasePhase.CODESIGN_ENGINE_BINARIES:
stdio.printStatus(<String>[
'You must validate pre-submit CI for your engine PR, merge it, and codesign',
'binaries before proceeding.\n',
].join('\n'));
if (autoAccept == false) { if (autoAccept == false) {
// TODO(fujino): actually test if binaries have been codesigned on macOS // TODO(fujino): actually test if binaries have been codesigned on macOS
final bool response = prompt( final bool response = prompt(
...@@ -163,29 +177,65 @@ void runNext({ ...@@ -163,29 +177,65 @@ void runNext({
} }
} }
if (state.engine.cherrypicks.isEmpty && state.engine.dartRevision.isEmpty) {
stdio.printStatus(
'This release has no engine cherrypicks, and thus the engine.version file\n'
'in the framework does not need to be updated.',
);
if (state.framework.cherrypicks.isEmpty) { if (state.framework.cherrypicks.isEmpty) {
stdio.printStatus('This release has no framework cherrypicks.'); stdio.printStatus(
break; 'This release also has no framework cherrypicks. Therefore, a framework\n'
} else if (unappliedCherrypicks.isEmpty) { 'pull request is not required.',
stdio.printStatus('All framework cherrypicks have been auto-applied by '
'the conductor.\n');
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to push your changes to the repository '
'${state.framework.mirror.url}?',
stdio,
); );
if (!response) { break;
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
} }
} }
final EngineRepository engine = EngineRepository(
checkouts,
initialRef: state.engine.candidateBranch,
upstreamRemote: Remote(
name: RemoteName.upstream,
url: state.engine.upstream.url,
),
previousCheckoutLocation: state.engine.checkoutPath,
);
final String engineRevision = engine.reverseParse('HEAD');
final Remote upstream = Remote(
name: RemoteName.upstream,
url: state.framework.upstream.url,
);
final FrameworkRepository framework = FrameworkRepository(
checkouts,
initialRef: state.framework.workingBranch,
upstreamRemote: upstream,
previousCheckoutLocation: state.framework.checkoutPath,
);
final String headRevision = framework.reverseParse('HEAD');
stdio.printStatus('Rolling new engine hash $engineRevision to framework checkout...');
framework.updateEngineRevision(engineRevision);
framework.commit('Update Engine revision to $engineRevision for ${state.releaseChannel} release ${state.releaseVersion}', addFirst: true);
if (state.framework.cherrypicks.isEmpty) {
stdio.printStatus(
'This release has no framework cherrypicks. However, a framework PR is still\n'
'required to roll engine cherrypicks.',
);
} else if (unappliedCherrypicks.isEmpty) {
stdio.printStatus('All framework cherrypicks were auto-applied by the conductor.');
} else { } else {
stdio.printStatus( stdio.printStatus(
'There were ${unappliedCherrypicks.length} cherrypicks that were not auto-applied.'); 'There were ${unappliedCherrypicks.length} cherrypicks that were not auto-applied.',
stdio.printStatus('These must be applied manually in the directory ' );
'${state.framework.checkoutPath} before proceeding.\n'); stdio.printStatus(
'These must be applied manually in the directory '
'${state.framework.checkoutPath} before proceeding.\n',
);
}
if (autoAccept == false) { if (autoAccept == false) {
final bool response = prompt( final bool response = prompt(
'Are you ready to push your framework branch to the repository ' 'Are you ready to push your framework branch to the repository '
...@@ -198,7 +248,12 @@ void runNext({ ...@@ -198,7 +248,12 @@ void runNext({
return; return;
} }
} }
}
framework.pushRef(
fromRef: headRevision,
toRef: state.framework.workingBranch,
remote: state.framework.mirror.name,
);
break; break;
case pb.ReleasePhase.PUBLISH_VERSION: case pb.ReleasePhase.PUBLISH_VERSION:
stdio.printStatus('Please ensure that you have merged your framework PR and that'); stdio.printStatus('Please ensure that you have merged your framework PR and that');
...@@ -216,7 +271,8 @@ void runNext({ ...@@ -216,7 +271,8 @@ void runNext({
final String headRevision = framework.reverseParse('HEAD'); final String headRevision = framework.reverseParse('HEAD');
if (autoAccept == false) { if (autoAccept == false) {
final bool response = prompt( final bool response = prompt(
'Has CI passed for the framework PR?', 'Are you ready to tag commit $headRevision as ${state.releaseVersion}\n'
'and push to remote ${state.framework.upstream.url}?',
stdio, stdio,
); );
if (!response) { if (!response) {
...@@ -240,9 +296,17 @@ void runNext({ ...@@ -240,9 +296,17 @@ void runNext({
); );
final String headRevision = framework.reverseParse('HEAD'); final String headRevision = framework.reverseParse('HEAD');
if (autoAccept == false) { if (autoAccept == false) {
// dryRun: true means print out git command
framework.pushRef(
fromRef: headRevision,
toRef: state.releaseChannel,
remote: state.framework.upstream.url,
force: force,
dryRun: true,
);
final bool response = prompt( final bool response = prompt(
'Are you ready to publish release ${state.releaseVersion} to ' 'Are you ready to publish this release?',
'channel ${state.releaseChannel} at ${state.framework.upstream.url}?',
stdio, stdio,
); );
if (!response) { if (!response) {
...@@ -251,10 +315,10 @@ void runNext({ ...@@ -251,10 +315,10 @@ void runNext({
return; return;
} }
} }
framework.updateChannel( framework.pushRef(
headRevision, fromRef: headRevision,
state.framework.upstream.url, toRef: state.releaseChannel,
state.releaseChannel, remote: state.framework.upstream.url,
force: force, force: force,
); );
break; break;
......
...@@ -204,6 +204,8 @@ class Repository extends $pb.GeneratedMessage { ...@@ -204,6 +204,8 @@ class Repository extends $pb.GeneratedMessage {
subBuilder: Cherrypick.create) subBuilder: Cherrypick.create)
..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dartRevision', ..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dartRevision',
protoName: 'dartRevision') protoName: 'dartRevision')
..aOS(9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'workingBranch',
protoName: 'workingBranch')
..hasRequiredFields = false; ..hasRequiredFields = false;
Repository._() : super(); Repository._() : super();
...@@ -216,6 +218,7 @@ class Repository extends $pb.GeneratedMessage { ...@@ -216,6 +218,7 @@ class Repository extends $pb.GeneratedMessage {
Remote mirror, Remote mirror,
$core.Iterable<Cherrypick> cherrypicks, $core.Iterable<Cherrypick> cherrypicks,
$core.String dartRevision, $core.String dartRevision,
$core.String workingBranch,
}) { }) {
final _result = create(); final _result = create();
if (candidateBranch != null) { if (candidateBranch != null) {
...@@ -242,6 +245,9 @@ class Repository extends $pb.GeneratedMessage { ...@@ -242,6 +245,9 @@ class Repository extends $pb.GeneratedMessage {
if (dartRevision != null) { if (dartRevision != null) {
_result.dartRevision = dartRevision; _result.dartRevision = dartRevision;
} }
if (workingBranch != null) {
_result.workingBranch = workingBranch;
}
return _result; return _result;
} }
factory Repository.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => factory Repository.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
...@@ -356,6 +362,18 @@ class Repository extends $pb.GeneratedMessage { ...@@ -356,6 +362,18 @@ class Repository extends $pb.GeneratedMessage {
$core.bool hasDartRevision() => $_has(7); $core.bool hasDartRevision() => $_has(7);
@$pb.TagNumber(8) @$pb.TagNumber(8)
void clearDartRevision() => clearField(8); void clearDartRevision() => clearField(8);
@$pb.TagNumber(9)
$core.String get workingBranch => $_getSZ(8);
@$pb.TagNumber(9)
set workingBranch($core.String v) {
$_setString(8, v);
}
@$pb.TagNumber(9)
$core.bool hasWorkingBranch() => $_has(8);
@$pb.TagNumber(9)
void clearWorkingBranch() => clearField(9);
} }
class ConductorState extends $pb.GeneratedMessage { class ConductorState extends $pb.GeneratedMessage {
......
...@@ -81,12 +81,13 @@ const Repository$json = const { ...@@ -81,12 +81,13 @@ const Repository$json = const {
const {'1': 'mirror', '3': 6, '4': 1, '5': 11, '6': '.conductor_state.Remote', '10': 'mirror'}, 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'}, const {'1': 'cherrypicks', '3': 7, '4': 3, '5': 11, '6': '.conductor_state.Cherrypick', '10': 'cherrypicks'},
const {'1': 'dartRevision', '3': 8, '4': 1, '5': 9, '10': 'dartRevision'}, const {'1': 'dartRevision', '3': 8, '4': 1, '5': 9, '10': 'dartRevision'},
const {'1': 'workingBranch', '3': 9, '4': 1, '5': 9, '10': 'workingBranch'},
], ],
}; };
/// Descriptor for `Repository`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `Repository`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List repositoryDescriptor = $convert.base64Decode( final $typed_data.Uint8List repositoryDescriptor = $convert.base64Decode(
'CgpSZXBvc2l0b3J5EigKD2NhbmRpZGF0ZUJyYW5jaBgBIAEoCVIPY2FuZGlkYXRlQnJhbmNoEigKD3N0YXJ0aW5nR2l0SGVhZBgCIAEoCVIPc3RhcnRpbmdHaXRIZWFkEiYKDmN1cnJlbnRHaXRIZWFkGAMgASgJUg5jdXJyZW50R2l0SGVhZBIiCgxjaGVja291dFBhdGgYBCABKAlSDGNoZWNrb3V0UGF0aBIzCgh1cHN0cmVhbRgFIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSCHVwc3RyZWFtEi8KBm1pcnJvchgGIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSBm1pcnJvchI9CgtjaGVycnlwaWNrcxgHIAMoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5DaGVycnlwaWNrUgtjaGVycnlwaWNrcxIiCgxkYXJ0UmV2aXNpb24YCCABKAlSDGRhcnRSZXZpc2lvbg=='); 'CgpSZXBvc2l0b3J5EigKD2NhbmRpZGF0ZUJyYW5jaBgBIAEoCVIPY2FuZGlkYXRlQnJhbmNoEigKD3N0YXJ0aW5nR2l0SGVhZBgCIAEoCVIPc3RhcnRpbmdHaXRIZWFkEiYKDmN1cnJlbnRHaXRIZWFkGAMgASgJUg5jdXJyZW50R2l0SGVhZBIiCgxjaGVja291dFBhdGgYBCABKAlSDGNoZWNrb3V0UGF0aBIzCgh1cHN0cmVhbRgFIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSCHVwc3RyZWFtEi8KBm1pcnJvchgGIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSBm1pcnJvchI9CgtjaGVycnlwaWNrcxgHIAMoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5DaGVycnlwaWNrUgtjaGVycnlwaWNrcxIiCgxkYXJ0UmV2aXNpb24YCCABKAlSDGRhcnRSZXZpc2lvbhIkCg13b3JraW5nQnJhbmNoGAkgASgJUg13b3JraW5nQnJhbmNo');
@$core.Deprecated('Use conductorStateDescriptor instead') @$core.Deprecated('Use conductorStateDescriptor instead')
const ConductorState$json = const { const ConductorState$json = const {
'1': 'ConductorState', '1': 'ConductorState',
......
...@@ -84,6 +84,12 @@ message Repository { ...@@ -84,6 +84,12 @@ message Repository {
// Only for engine repositories. // Only for engine repositories.
string dartRevision = 8; string dartRevision = 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].
string workingBranch = 9;
} }
message ConductorState { message ConductorState {
......
...@@ -25,7 +25,9 @@ class Remote { ...@@ -25,7 +25,9 @@ class Remote {
const Remote({ const Remote({
required RemoteName name, required RemoteName name,
required this.url, required this.url,
}) : _name = name, assert(url != null), assert (url != ''); }) : _name = name,
assert(url != null),
assert(url != '');
final RemoteName _name; final RemoteName _name;
...@@ -63,9 +65,15 @@ abstract class Repository { ...@@ -63,9 +65,15 @@ abstract class Repository {
if (previousCheckoutLocation != null) { if (previousCheckoutLocation != null) {
_checkoutDirectory = fileSystem.directory(previousCheckoutLocation); _checkoutDirectory = fileSystem.directory(previousCheckoutLocation);
if (!_checkoutDirectory!.existsSync()) { if (!_checkoutDirectory!.existsSync()) {
throw ConductorException('Provided previousCheckoutLocation $previousCheckoutLocation does not exist on disk!'); throw ConductorException(
'Provided previousCheckoutLocation $previousCheckoutLocation does not exist on disk!');
} }
if (initialRef != null) { if (initialRef != null) {
git.run(
<String>['fetch', upstreamRemote.name],
'Fetch ${upstreamRemote.name} to ensure we have latest refs',
workingDirectory: _checkoutDirectory!.path,
);
git.run( git.run(
<String>['checkout', '${upstreamRemote.name}/$initialRef'], <String>['checkout', '${upstreamRemote.name}/$initialRef'],
'Checking out initialRef $initialRef', 'Checking out initialRef $initialRef',
...@@ -255,11 +263,9 @@ abstract class Repository { ...@@ -255,11 +263,9 @@ abstract class Repository {
/// List commits in reverse chronological order. /// List commits in reverse chronological order.
List<String> revList(List<String> args) { List<String> revList(List<String> args) {
return git return git
.getOutput( .getOutput(<String>['rev-list', ...args],
<String>['rev-list', ...args],
'rev-list with args ${args.join(' ')}', 'rev-list with args ${args.join(' ')}',
workingDirectory: checkoutDirectory.path workingDirectory: checkoutDirectory.path)
)
.trim() .trim()
.split('\n'); .split('\n');
} }
...@@ -356,22 +362,33 @@ abstract class Repository { ...@@ -356,22 +362,33 @@ abstract class Repository {
} }
/// Push [commit] to the release channel [branch]. /// Push [commit] to the release channel [branch].
void updateChannel( void pushRef({
String commit, required String fromRef,
String remote, required String remote,
String branch, { required String toRef,
bool force = false, bool force = false,
bool dryRun = false,
}) { }) {
git.run( final List<String> args = <String>[
<String>[
'push', 'push',
if (force) '--force', if (force) '--force',
remote, remote,
'$commit:$branch', '$fromRef:$toRef',
], ];
final String command = <String>[
'git',
...args,
].join(' ');
if (dryRun) {
stdio.printStatus('About to execute command: `$command`');
} else {
git.run(
args,
'update the release branch with the commit', 'update the release branch with the commit',
workingDirectory: checkoutDirectory.path, workingDirectory: checkoutDirectory.path,
); );
stdio.printStatus('Executed command: `$command`');
}
} }
String commit( String commit(
...@@ -566,6 +583,21 @@ class FrameworkRepository extends Repository { ...@@ -566,6 +583,21 @@ class FrameworkRepository extends Repository {
) as Map<String, dynamic>; ) as Map<String, dynamic>;
return Version.fromString(versionJson['frameworkVersion'] as String); return Version.fromString(versionJson['frameworkVersion'] as String);
} }
void updateEngineRevision(
String newEngine, {
@visibleForTesting File? engineVersionFile,
}) {
assert(newEngine.isNotEmpty);
engineVersionFile ??= checkoutDirectory
.childDirectory('bin')
.childDirectory('internal')
.childFile('engine.version');
assert(engineVersionFile.existsSync());
final String oldEngine = engineVersionFile.readAsStringSync();
stdio.printStatus('Updating engine revision from $oldEngine to $newEngine');
engineVersionFile.writeAsStringSync(newEngine.trim(), flush: true);
}
} }
/// A wrapper around the host repository that is executing the conductor. /// A wrapper around the host repository that is executing the conductor.
...@@ -594,17 +626,20 @@ class HostFrameworkRepository extends FrameworkRepository { ...@@ -594,17 +626,20 @@ class HostFrameworkRepository extends FrameworkRepository {
@override @override
void newBranch(String branchName) { void newBranch(String branchName) {
throw ConductorException('newBranch not implemented for the host repository'); throw ConductorException(
'newBranch not implemented for the host repository');
} }
@override @override
void checkout(String ref) { void checkout(String ref) {
throw ConductorException('checkout not implemented for the host repository'); throw ConductorException(
'checkout not implemented for the host repository');
} }
@override @override
String cherryPick(String commit) { String cherryPick(String commit) {
throw ConductorException('cherryPick not implemented for the host repository'); throw ConductorException(
'cherryPick not implemented for the host repository');
} }
@override @override
...@@ -617,14 +652,15 @@ class HostFrameworkRepository extends FrameworkRepository { ...@@ -617,14 +652,15 @@ class HostFrameworkRepository extends FrameworkRepository {
throw ConductorException('tag not implemented for the host repository'); throw ConductorException('tag not implemented for the host repository');
} }
@override
void updateChannel( void updateChannel(
String commit, String commit,
String remote, String remote,
String branch, { String branch, {
bool force = false, bool force = false,
bool dryRun = false,
}) { }) {
throw ConductorException('updateChannel not implemented for the host repository'); throw ConductorException(
'updateChannel not implemented for the host repository');
} }
@override @override
...@@ -673,20 +709,20 @@ class EngineRepository extends Repository { ...@@ -673,20 +709,20 @@ class EngineRepository extends Repository {
depsFile ??= checkoutDirectory.childFile('DEPS'); depsFile ??= checkoutDirectory.childFile('DEPS');
final String fileContent = depsFile.readAsStringSync(); final String fileContent = depsFile.readAsStringSync();
final RegExp dartPattern = RegExp("[ ]+'dart_revision': '([a-z0-9]{40})',"); final RegExp dartPattern = RegExp("[ ]+'dart_revision': '([a-z0-9]{40})',");
final Iterable<RegExpMatch> allMatches = dartPattern.allMatches(fileContent); final Iterable<RegExpMatch> allMatches =
dartPattern.allMatches(fileContent);
if (allMatches.length != 1) { if (allMatches.length != 1) {
throw ConductorException( throw ConductorException(
'Unexpected content in the DEPS file at ${depsFile.path}\n' 'Unexpected content in the DEPS file at ${depsFile.path}\n'
'Expected to find pattern ${dartPattern.pattern} 1 times, but got ' 'Expected to find pattern ${dartPattern.pattern} 1 times, but got '
'${allMatches.length}.' '${allMatches.length}.');
);
} }
final String updatedFileContent = fileContent.replaceFirst( final String updatedFileContent = fileContent.replaceFirst(
dartPattern, dartPattern,
" 'dart_revision': '$newRevision',", " 'dart_revision': '$newRevision',",
); );
depsFile.writeAsStringSync(updatedFileContent); depsFile.writeAsStringSync(updatedFileContent, flush: true);
} }
@override @override
......
...@@ -191,10 +191,10 @@ bool rollDev({ ...@@ -191,10 +191,10 @@ bool rollDev({
repository.tag(commit, version.toString(), remoteName); repository.tag(commit, version.toString(), remoteName);
} }
repository.updateChannel( repository.pushRef(
commit, fromRef: commit,
remoteName, remote: remoteName,
'dev', toRef: 'dev',
force: force, force: force,
); );
......
...@@ -100,7 +100,7 @@ class StartCommand extends Command<void> { ...@@ -100,7 +100,7 @@ class StartCommand extends Command<void> {
'y': 'Indicates the first dev release after a beta release.', 'y': 'Indicates the first dev release after a beta release.',
'z': 'Indicates a hotfix to a stable release.', 'z': 'Indicates a hotfix to a stable release.',
'm': 'Indicates a standard dev release.', 'm': 'Indicates a standard dev release.',
'n': 'Indicates a hotfix to a dev release.', 'n': 'Indicates a hotfix to a dev or beta release.',
}, },
); );
final Git git = Git(processManager); final Git git = Git(processManager);
...@@ -230,7 +230,8 @@ class StartCommand extends Command<void> { ...@@ -230,7 +230,8 @@ class StartCommand extends Command<void> {
// Create a new branch so that we don't accidentally push to upstream // Create a new branch so that we don't accidentally push to upstream
// candidateBranch. // candidateBranch.
engine.newBranch('cherrypicks-$candidateBranch'); final String workingBranchName = 'cherrypicks-$candidateBranch';
engine.newBranch(workingBranchName);
if (dartRevision != null && dartRevision.isNotEmpty) { if (dartRevision != null && dartRevision.isNotEmpty) {
engine.updateDartRevision(dartRevision); engine.updateDartRevision(dartRevision);
...@@ -262,6 +263,7 @@ class StartCommand extends Command<void> { ...@@ -262,6 +263,7 @@ class StartCommand extends Command<void> {
final String engineHead = engine.reverseParse('HEAD'); final String engineHead = engine.reverseParse('HEAD');
state.engine = pb.Repository( state.engine = pb.Repository(
candidateBranch: candidateBranch, candidateBranch: candidateBranch,
workingBranch: workingBranchName,
startingGitHead: engineHead, startingGitHead: engineHead,
currentGitHead: engineHead, currentGitHead: engineHead,
checkoutPath: engine.checkoutDirectory.path, checkoutPath: engine.checkoutDirectory.path,
...@@ -282,7 +284,7 @@ class StartCommand extends Command<void> { ...@@ -282,7 +284,7 @@ class StartCommand extends Command<void> {
url: frameworkMirror, url: frameworkMirror,
), ),
); );
framework.newBranch('cherrypicks-$candidateBranch'); framework.newBranch(workingBranchName);
final List<pb.Cherrypick> frameworkCherrypicks = _sortCherrypicks( final List<pb.Cherrypick> frameworkCherrypicks = _sortCherrypicks(
repository: framework, repository: framework,
cherrypicks: frameworkCherrypickRevisions, cherrypicks: frameworkCherrypickRevisions,
...@@ -320,6 +322,7 @@ class StartCommand extends Command<void> { ...@@ -320,6 +322,7 @@ class StartCommand extends Command<void> {
final String frameworkHead = framework.reverseParse('HEAD'); final String frameworkHead = framework.reverseParse('HEAD');
state.framework = pb.Repository( state.framework = pb.Repository(
candidateBranch: candidateBranch, candidateBranch: candidateBranch,
workingBranch: workingBranchName,
startingGitHead: frameworkHead, startingGitHead: frameworkHead,
currentGitHead: frameworkHead, currentGitHead: frameworkHead,
checkoutPath: framework.checkoutDirectory.path, checkoutPath: framework.checkoutDirectory.path,
......
...@@ -21,7 +21,7 @@ String luciConsoleLink(String channel, String groupName) { ...@@ -21,7 +21,7 @@ String luciConsoleLink(String channel, String groupName) {
'channel $channel not recognized', 'channel $channel not recognized',
); );
assert( assert(
<String>['framework', 'engine', 'devicelab'].contains(groupName), <String>['framework', 'engine', 'devicelab', 'packaging'].contains(groupName),
'group named $groupName not recognized', 'group named $groupName not recognized',
); );
final String consoleName = channel == 'master' ? groupName : '${channel}_$groupName'; final String consoleName = channel == 'master' ? groupName : '${channel}_$groupName';
...@@ -133,8 +133,9 @@ String phaseInstructions(pb.ConductorState state) { ...@@ -133,8 +133,9 @@ String phaseInstructions(pb.ConductorState state) {
].join('\n'); ].join('\n');
case ReleasePhase.CODESIGN_ENGINE_BINARIES: case ReleasePhase.CODESIGN_ENGINE_BINARIES:
return <String>[ return <String>[
'You must verify Engine CI builds are successful and then codesign the', 'You must verify pre-submit CI builds on your engine pull request are successful,',
'binaries at revision ${state.engine.currentGitHead}.', 'merge your pull request, validate post-submit CI, and then codesign the binaries ',
'on the merge commit.',
].join('\n'); ].join('\n');
case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS: case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
final List<pb.Cherrypick> outstandingCherrypicks = state.framework.cherrypicks.where( final List<pb.Cherrypick> outstandingCherrypicks = state.framework.cherrypicks.where(
...@@ -150,13 +151,14 @@ String phaseInstructions(pb.ConductorState state) { ...@@ -150,13 +151,14 @@ String phaseInstructions(pb.ConductorState state) {
].join('\n'); ].join('\n');
case ReleasePhase.PUBLISH_VERSION: case ReleasePhase.PUBLISH_VERSION:
return <String>[ return <String>[
'You must verify Framework CI builds are successful.', 'You must verify pre-submit CI builds on your framework pull request are successful,',
'See $kReleaseDocumentationUrl for more information.', 'merge your pull request, and validate post-submit CI. See $kReleaseDocumentationUrl,',
'for more information.',
].join('\n'); ].join('\n');
case ReleasePhase.PUBLISH_CHANNEL: case ReleasePhase.PUBLISH_CHANNEL:
return 'Issue `conductor next` to publish your release to the release branch.'; return 'Issue `conductor next` to publish your release to the release branch.';
case ReleasePhase.VERIFY_RELEASE: case ReleasePhase.VERIFY_RELEASE:
return 'Release archive packages must be verified on cloud storage.'; return 'Release archive packages must be verified on cloud storage: ${luciConsoleLink(state.releaseChannel, 'packaging')}';
case ReleasePhase.RELEASE_COMPLETED: case ReleasePhase.RELEASE_COMPLETED:
return 'This release has been completed.'; return 'This release has been completed.';
} }
......
...@@ -10,6 +10,7 @@ import 'package:conductor/proto/conductor_state.pb.dart' as pb; ...@@ -10,6 +10,7 @@ import 'package:conductor/proto/conductor_state.pb.dart' as pb;
import 'package:conductor/proto/conductor_state.pbenum.dart' show ReleasePhase; import 'package:conductor/proto/conductor_state.pbenum.dart' show ReleasePhase;
import 'package:conductor/repository.dart'; import 'package:conductor/repository.dart';
import 'package:conductor/state.dart'; import 'package:conductor/state.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
...@@ -20,11 +21,16 @@ import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; ...@@ -20,11 +21,16 @@ import '../../../packages/flutter_tools/test/src/fake_process_manager.dart';
void main() { void main() {
group('next command', () { group('next command', () {
const String flutterRoot = '/flutter'; const String flutterRoot = '/flutter';
const String checkoutsParentDirectory = '$flutterRoot/dev/tools/'; const String checkoutsParentDirectory = '$flutterRoot/dev/conductor';
const String candidateBranch = 'flutter-1.2-candidate.3'; const String candidateBranch = 'flutter-1.2-candidate.3';
const String workingBranch = 'cherrypicks-$candidateBranch';
final String localPathSeparator = const LocalPlatform().pathSeparator; final String localPathSeparator = const LocalPlatform().pathSeparator;
final String localOperatingSystem = const LocalPlatform().pathSeparator; final String localOperatingSystem = const LocalPlatform().pathSeparator;
const String revision1 = 'abc123'; const String revision1 = 'abc123';
const String revision2 = 'def456';
const String revision3 = '789aaa';
const String releaseVersion = '1.2.0-3.0.pre';
const String releaseChannel = 'beta';
MemoryFileSystem fileSystem; MemoryFileSystem fileSystem;
TestStdio stdio; TestStdio stdio;
const String stateFile = '/state-file.json'; const String stateFile = '/state-file.json';
...@@ -72,7 +78,8 @@ void main() { ...@@ -72,7 +78,8 @@ void main() {
); );
}); });
test('does not prompt user and updates state.currentPhase from APPLY_ENGINE_CHERRYPICKS to CODESIGN_ENGINE_BINARIES if there are no engine cherrypicks', () async { group('APPLY_ENGINE_CHERRYPICKS to CODESIGN_ENGINE_BINARIES', () {
test('does not prompt user and updates currentPhase if there are no engine cherrypicks', () async {
final FakeProcessManager processManager = FakeProcessManager.list( final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[], <FakeCommand>[],
); );
...@@ -109,17 +116,15 @@ void main() { ...@@ -109,17 +116,15 @@ void main() {
fileSystem.file(stateFile), fileSystem.file(stateFile),
); );
expect(processManager, hasNoRemainingExpectations);
expect(finalState.currentPhase, ReleasePhase.CODESIGN_ENGINE_BINARIES); expect(finalState.currentPhase, ReleasePhase.CODESIGN_ENGINE_BINARIES);
expect(stdio.error, isEmpty); expect(stdio.error, isEmpty);
}); });
test('confirms to stdout when all engine cherrypicks were auto-applied', () async {
test('updates state.lastPhase from APPLY_ENGINE_CHERRYPICKS to CODESIGN_ENGINE_BINARIES if user responds yes', () async {
const String remoteUrl = 'https://githost.com/org/repo.git'; const String remoteUrl = 'https://githost.com/org/repo.git';
stdio.stdin.add('y'); stdio.stdin.add('n');
final FakeProcessManager processManager = FakeProcessManager.list( final FakeProcessManager processManager = FakeProcessManager.empty();
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform( final FakePlatform platform = FakePlatform(
environment: <String, String>{ environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator), 'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
...@@ -132,10 +137,12 @@ void main() { ...@@ -132,10 +137,12 @@ void main() {
cherrypicks: <pb.Cherrypick>[ cherrypicks: <pb.Cherrypick>[
pb.Cherrypick( pb.Cherrypick(
trunkRevision: 'abc123', trunkRevision: 'abc123',
state: pb.CherrypickState.PENDING, state: pb.CherrypickState.COMPLETED,
), ),
], ],
mirror: pb.Remote(url: remoteUrl), workingBranch: workingBranch,
upstream: pb.Remote(name: 'upstream', url: remoteUrl),
mirror: pb.Remote(name: 'mirror', url: remoteUrl),
), ),
currentPhase: ReleasePhase.APPLY_ENGINE_CHERRYPICKS, currentPhase: ReleasePhase.APPLY_ENGINE_CHERRYPICKS,
); );
...@@ -158,21 +165,27 @@ void main() { ...@@ -158,21 +165,27 @@ void main() {
stateFile, stateFile,
]); ]);
final pb.ConductorState finalState = readStateFromFile( expect(processManager, hasNoRemainingExpectations);
fileSystem.file(stateFile), expect(
stdio.stdout,
contains('All engine cherrypicks have been auto-applied by the conductor'),
); );
expect(stdio.stdout, contains(
'Are you ready to push your engine branch to the repository $remoteUrl? (y/n) '));
expect(finalState.currentPhase, ReleasePhase.CODESIGN_ENGINE_BINARIES);
expect(stdio.error, isEmpty);
}); });
test('does not update state.currentPhase from CODESIGN_ENGINE_BINARIES if user responds no', () async { test('updates lastPhase if user responds yes', () async {
stdio.stdin.add('n'); const String remoteUrl = 'https://githost.com/org/repo.git';
final FakeProcessManager processManager = FakeProcessManager.list( stdio.stdin.add('y');
<FakeCommand>[], final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
); const FakeCommand(
command: <String>['git', 'fetch', 'upstream'],
),
const FakeCommand(command: <String>['git', 'checkout', 'upstream/$workingBranch']),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision1,
),
const FakeCommand(command: <String>['git', 'push', 'mirror', '$revision1:$workingBranch']),
]);
final FakePlatform platform = FakePlatform( final FakePlatform platform = FakePlatform(
environment: <String, String>{ environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator), 'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
...@@ -188,8 +201,11 @@ void main() { ...@@ -188,8 +201,11 @@ void main() {
state: pb.CherrypickState.PENDING, state: pb.CherrypickState.PENDING,
), ),
], ],
workingBranch: workingBranch,
upstream: pb.Remote(name: 'upstream', url: remoteUrl),
mirror: pb.Remote(name: 'mirror', url: remoteUrl),
), ),
currentPhase: ReleasePhase.CODESIGN_ENGINE_BINARIES, currentPhase: ReleasePhase.APPLY_ENGINE_CHERRYPICKS,
); );
writeStateToFile( writeStateToFile(
fileSystem.file(stateFile), fileSystem.file(stateFile),
...@@ -214,26 +230,45 @@ void main() { ...@@ -214,26 +230,45 @@ void main() {
fileSystem.file(stateFile), fileSystem.file(stateFile),
); );
expect(stdio.stdout, contains('Has CI passed for the engine PR and binaries been codesigned? (y/n) ')); expect(processManager, hasNoRemainingExpectations);
expect(stdio.stdout, contains(
'Are you ready to push your engine branch to the repository $remoteUrl? (y/n) '));
expect(finalState.currentPhase, ReleasePhase.CODESIGN_ENGINE_BINARIES); expect(finalState.currentPhase, ReleasePhase.CODESIGN_ENGINE_BINARIES);
expect(stdio.error.contains('Aborting command.'), true); expect(stdio.error, isEmpty);
});
}); });
test('updates state.currentPhase from CODESIGN_ENGINE_BINARIES to APPLY_FRAMEWORK_CHERRYPICKS if user responds yes', () async { group('CODESIGN_ENGINE_BINARIES to APPLY_FRAMEWORK_CHERRYPICKS', () {
stdio.stdin.add('y'); pb.ConductorState state;
final FakeProcessManager processManager = FakeProcessManager.list( FakeProcessManager processManager;
<FakeCommand>[], FakePlatform platform;
setUp(() {
state = pb.ConductorState(
engine: pb.Repository(
cherrypicks: <pb.Cherrypick>[
pb.Cherrypick(
trunkRevision: 'abc123',
state: pb.CherrypickState.PENDING,
),
],
),
currentPhase: ReleasePhase.CODESIGN_ENGINE_BINARIES,
); );
final FakePlatform platform = FakePlatform(
processManager = FakeProcessManager.empty();
platform = FakePlatform(
environment: <String, String>{ environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator), 'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
}, },
operatingSystem: localOperatingSystem, operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator, pathSeparator: localPathSeparator,
); );
final pb.ConductorState state = pb.ConductorState( });
currentPhase: ReleasePhase.CODESIGN_ENGINE_BINARIES,
); test('does not update currentPhase if user responds no', () async {
stdio.stdin.add('n');
writeStateToFile( writeStateToFile(
fileSystem.file(stateFile), fileSystem.file(stateFile),
state, state,
...@@ -258,13 +293,12 @@ void main() { ...@@ -258,13 +293,12 @@ void main() {
); );
expect(stdio.stdout, contains('Has CI passed for the engine PR and binaries been codesigned? (y/n) ')); expect(stdio.stdout, contains('Has CI passed for the engine PR and binaries been codesigned? (y/n) '));
expect(finalState.currentPhase, ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS); expect(finalState.currentPhase, ReleasePhase.CODESIGN_ENGINE_BINARIES);
expect(stdio.error.contains('Aborting command.'), true);
}); });
test('does not prompt user and updates state.currentPhase from APPLY_FRAMEWORK_CHERRYPICKS to PUBLISH_VERSION if there are no framework cherrypicks', () async { test('updates currentPhase if user responds yes', () async {
final FakeProcessManager processManager = FakeProcessManager.list( stdio.stdin.add('y');
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform( final FakePlatform platform = FakePlatform(
environment: <String, String>{ environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator), 'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
...@@ -272,9 +306,6 @@ void main() { ...@@ -272,9 +306,6 @@ void main() {
operatingSystem: localOperatingSystem, operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator, pathSeparator: localPathSeparator,
); );
final pb.ConductorState state = pb.ConductorState(
currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,
);
writeStateToFile( writeStateToFile(
fileSystem.file(stateFile), fileSystem.file(stateFile),
state, state,
...@@ -298,41 +329,90 @@ void main() { ...@@ -298,41 +329,90 @@ void main() {
fileSystem.file(stateFile), fileSystem.file(stateFile),
); );
expect(stdio.stdout, isNot(contains('Did you apply all framework cherrypicks? (y/n) '))); expect(stdio.stdout, contains('Has CI passed for the engine PR and binaries been codesigned? (y/n) '));
expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION); expect(finalState.currentPhase, ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS);
expect(stdio.error, isEmpty); });
}); });
test('does not update state.currentPhase from APPLY_FRAMEWORK_CHERRYPICKS if user responds no', () async { group('APPLY_FRAMEWORK_CHERRYPICKS to PUBLISH_VERSION', () {
const String remoteUrl = 'https://githost.com/org/repo.git'; const String mirrorRemoteUrl = 'https://githost.com/org/repo.git';
stdio.stdin.add('n'); const String upstreamRemoteUrl = 'https://githost.com/mirror/repo.git';
final FakeProcessManager processManager = FakeProcessManager.list( const String engineUpstreamRemoteUrl = 'https://githost.com/mirror/engine.git';
<FakeCommand>[], const String frameworkCheckoutPath = '$checkoutsParentDirectory/framework';
); const String engineCheckoutPath = '$checkoutsParentDirectory/engine';
final FakePlatform platform = FakePlatform( const String oldEngineVersion = '000000001';
FakeProcessManager processManager;
FakePlatform platform;
pb.ConductorState state;
setUp(() {
processManager = FakeProcessManager.empty();
platform = FakePlatform(
environment: <String, String>{ environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator), 'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
}, },
operatingSystem: localOperatingSystem, operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator, pathSeparator: localPathSeparator,
); );
final pb.ConductorState state = pb.ConductorState( state = pb.ConductorState(
releaseChannel: releaseChannel,
releaseVersion: releaseVersion,
framework: pb.Repository( framework: pb.Repository(
candidateBranch: candidateBranch,
checkoutPath: frameworkCheckoutPath,
cherrypicks: <pb.Cherrypick>[ cherrypicks: <pb.Cherrypick>[
pb.Cherrypick( pb.Cherrypick(
trunkRevision: 'abc123', trunkRevision: 'abc123',
state: pb.CherrypickState.PENDING, state: pb.CherrypickState.PENDING,
), ),
], ],
mirror: pb.Remote(url: remoteUrl), mirror: pb.Remote(name: 'mirror', url: mirrorRemoteUrl),
upstream: pb.Remote(name: 'upstream', url: upstreamRemoteUrl),
workingBranch: workingBranch,
),
engine: pb.Repository(
candidateBranch: candidateBranch,
checkoutPath: engineCheckoutPath,
dartRevision: 'cdef0123',
upstream: pb.Remote(name: 'upstream', url: engineUpstreamRemoteUrl),
), ),
currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS, currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,
); );
// create engine repo
fileSystem.directory(engineCheckoutPath).createSync(recursive: true);
// create framework repo
final Directory frameworkDir = fileSystem.directory(frameworkCheckoutPath);
final File engineRevisionFile = frameworkDir
.childDirectory('bin')
.childDirectory('internal')
.childFile('engine.version');
engineRevisionFile.createSync(recursive: true);
engineRevisionFile.writeAsStringSync(oldEngineVersion, flush: true);
});
test('with no dart, engine or framework cherrypicks, no user input, no PR needed', () async {
state = pb.ConductorState(
framework: pb.Repository(
candidateBranch: candidateBranch,
checkoutPath: frameworkCheckoutPath,
mirror: pb.Remote(name: 'mirror', url: mirrorRemoteUrl),
upstream: pb.Remote(name: 'upstream', url: upstreamRemoteUrl),
workingBranch: workingBranch,
),
engine: pb.Repository(
candidateBranch: candidateBranch,
checkoutPath: engineCheckoutPath,
upstream: pb.Remote(name: 'upstream', url: engineUpstreamRemoteUrl),
),
currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,
);
writeStateToFile( writeStateToFile(
fileSystem.file(stateFile), fileSystem.file(stateFile),
state, state,
<String>[], <String>[],
); );
final Checkouts checkouts = Checkouts( final Checkouts checkouts = Checkouts(
fileSystem: fileSystem, fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true), parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
...@@ -341,6 +421,7 @@ void main() { ...@@ -341,6 +421,7 @@ void main() {
stdio: stdio, stdio: stdio,
); );
final CommandRunner<void> runner = createRunner(checkouts: checkouts); final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[ await runner.run(<String>[
'next', 'next',
'--$kStateOption', '--$kStateOption',
...@@ -351,35 +432,57 @@ void main() { ...@@ -351,35 +432,57 @@ void main() {
fileSystem.file(stateFile), fileSystem.file(stateFile),
); );
expect(stdio.stdout, contains('Are you ready to push your framework branch to the repository $remoteUrl? (y/n) ')); expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION);
expect(stdio.error, contains('Aborting command.')); expect(stdio.error, isEmpty);
expect(finalState.currentPhase, ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS); expect(
stdio.stdout,
contains('pull request is not required'),
);
}); });
test('updates state.currentPhase from APPLY_FRAMEWORK_CHERRYPICKS to PUBLISH_VERSION if user responds yes', () async { test('with no engine cherrypicks but a dart revision update, updates engine revision', () async {
const String remoteUrl = 'https://githost.com/org/repo.git'; stdio.stdin.add('n');
stdio.stdin.add('y'); processManager.addCommands(const <FakeCommand>[
final FakeProcessManager processManager = FakeProcessManager.list( FakeCommand(command: <String>['git', 'fetch', 'upstream']),
<FakeCommand>[], FakeCommand(command: <String>['git', 'checkout', 'upstream/$candidateBranch']),
); FakeCommand(
final FakePlatform platform = FakePlatform( command: <String>['git', 'rev-parse', 'HEAD'],
environment: <String, String>{ stdout: revision1,
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator), ),
}, FakeCommand(command: <String>['git', 'fetch', 'upstream']),
operatingSystem: localOperatingSystem, FakeCommand(command: <String>['git', 'checkout', 'upstream/$workingBranch']),
pathSeparator: localPathSeparator, FakeCommand(
); command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision2,
),
FakeCommand(command: <String>['git', 'add', '--all']),
FakeCommand(command: <String>[
'git',
'commit',
"--message='Update Engine revision to $revision1 for $releaseChannel release $releaseVersion'",
]),
FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision3,
),
]);
final pb.ConductorState state = pb.ConductorState( final pb.ConductorState state = pb.ConductorState(
releaseChannel: releaseChannel,
releaseVersion: releaseVersion,
currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,
framework: pb.Repository( framework: pb.Repository(
cherrypicks: <pb.Cherrypick>[ candidateBranch: candidateBranch,
pb.Cherrypick( checkoutPath: frameworkCheckoutPath,
trunkRevision: 'abc123', mirror: pb.Remote(name: 'mirror', url: mirrorRemoteUrl),
state: pb.CherrypickState.PENDING, upstream: pb.Remote(name: 'upstream', url: upstreamRemoteUrl),
workingBranch: workingBranch,
), ),
], engine: pb.Repository(
mirror: pb.Remote(url: remoteUrl), candidateBranch: candidateBranch,
checkoutPath: engineCheckoutPath,
upstream: pb.Remote(name: 'upstream', url: engineUpstreamRemoteUrl),
dartRevision: 'abc123',
), ),
currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,
); );
writeStateToFile( writeStateToFile(
fileSystem.file(stateFile), fileSystem.file(stateFile),
...@@ -400,20 +503,171 @@ void main() { ...@@ -400,20 +503,171 @@ void main() {
stateFile, stateFile,
]); ]);
expect(processManager, hasNoRemainingExpectations);
expect(stdio.stdout, contains('Updating engine revision from $oldEngineVersion to $revision1'));
expect(stdio.stdout, contains('a framework PR is still\nrequired to roll engine cherrypicks.'));
});
test('does not update state.currentPhase if user responds no', () async {
stdio.stdin.add('n');
processManager.addCommands(const <FakeCommand>[
FakeCommand(command: <String>['git', 'fetch', 'upstream']),
FakeCommand(command: <String>['git', 'checkout', 'upstream/$candidateBranch']),
FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision1,
),
FakeCommand(command: <String>['git', 'fetch', 'upstream']),
FakeCommand(command: <String>['git', 'checkout', 'upstream/$workingBranch']),
FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision2,
),
FakeCommand(command: <String>['git', 'add', '--all']),
FakeCommand(command: <String>[
'git',
'commit',
"--message='Update Engine revision to $revision1 for $releaseChannel release $releaseVersion'",
]),
FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision3,
),
]);
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( final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile), fileSystem.file(stateFile),
); );
expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION); expect(stdio.stdout, contains('Are you ready to push your framework branch to the repository $mirrorRemoteUrl? (y/n) '));
expect(stdio.stdout, contains('Are you ready to push your framework branch to the repository $remoteUrl? (y/n)')); expect(stdio.error, contains('Aborting command.'));
expect(finalState.currentPhase, ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS);
}); });
test('updates state.currentPhase if user responds yes', () async {
stdio.stdin.add('y');
processManager.addCommands(const <FakeCommand>[
// Engine repo
FakeCommand(command: <String>['git', 'fetch', 'upstream']),
FakeCommand(command: <String>['git', 'checkout', 'upstream/$candidateBranch']),
FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision1,
),
// Framework repo
FakeCommand(command: <String>['git', 'fetch', 'upstream']),
FakeCommand(command: <String>['git', 'checkout', 'upstream/$workingBranch']),
FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision2,
),
FakeCommand(command: <String>['git', 'add', '--all']),
FakeCommand(command: <String>[
'git',
'commit',
"--message='Update Engine revision to $revision1 for $releaseChannel release $releaseVersion'",
]),
FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision3,
),
FakeCommand(
command: <String>['git', 'push', 'mirror', '$revision2:$workingBranch'],
),
]);
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),
);
test('does not update state.currentPhase from PUBLISH_VERSION if user responds no', () async { expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION);
expect(
stdio.stdout,
contains('Rolling new engine hash $revision1 to framework checkout...'),
);
expect(
stdio.stdout,
contains('There were 1 cherrypicks that were not auto-applied'),
);
expect(
stdio.stdout,
contains('Are you ready to push your framework branch to the repository $mirrorRemoteUrl? (y/n)'),
);
expect(
stdio.stdout,
contains('Executed command: `git push mirror $revision2:$workingBranch`'),
);
expect(stdio.error, isEmpty);
});
});
group('PUBLISH_VERSION to PUBLISH_CHANNEL', () {
const String remoteName = 'upstream'; const String remoteName = 'upstream';
const String releaseVersion = '1.2.0-3.0.pre';
pb.ConductorState state;
FakePlatform platform;
setUp(() {
state = pb.ConductorState(
currentPhase: ReleasePhase.PUBLISH_VERSION,
framework: pb.Repository(
candidateBranch: candidateBranch,
upstream: pb.Remote(url: FrameworkRepository.defaultUpstream),
),
releaseVersion: releaseVersion,
);
platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
});
test('does not update state.currentPhase if user responds no', () async {
stdio.stdin.add('n'); stdio.stdin.add('n');
final FakeProcessManager processManager = FakeProcessManager.list( final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[ <FakeCommand>[
const FakeCommand(
command: <String>['git', 'fetch', 'upstream'],
),
const FakeCommand( const FakeCommand(
command: <String>['git', 'checkout', '$remoteName/$candidateBranch'], command: <String>['git', 'checkout', '$remoteName/$candidateBranch'],
), ),
...@@ -423,20 +677,6 @@ void main() { ...@@ -423,20 +677,6 @@ void main() {
), ),
], ],
); );
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
currentPhase: ReleasePhase.PUBLISH_VERSION,
framework: pb.Repository(
candidateBranch: candidateBranch,
upstream: pb.Remote(url: FrameworkRepository.defaultUpstream),
),
);
writeStateToFile( writeStateToFile(
fileSystem.file(stateFile), fileSystem.file(stateFile),
state, state,
...@@ -460,18 +700,19 @@ void main() { ...@@ -460,18 +700,19 @@ void main() {
fileSystem.file(stateFile), fileSystem.file(stateFile),
); );
expect(stdio.stdout, contains('Has CI passed for the framework PR?')); expect(processManager, hasNoRemainingExpectations);
expect(stdio.stdout, contains('Are you ready to tag commit $revision1 as $releaseVersion'));
expect(stdio.error, contains('Aborting command.')); expect(stdio.error, contains('Aborting command.'));
expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION); expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION);
expect(finalState.logs, stdio.logs); expect(finalState.logs, stdio.logs);
expect(processManager.hasRemainingExpectations, false);
}); });
test('updates state.currentPhase from PUBLISH_VERSION to PUBLISH_CHANNEL if user responds yes', () async { test('updates state.currentPhase if user responds yes', () async {
const String remoteName = 'upstream';
const String releaseVersion = '1.2.0-3.0.pre';
stdio.stdin.add('y'); stdio.stdin.add('y');
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[ final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['git', 'fetch', 'upstream'],
),
const FakeCommand( const FakeCommand(
command: <String>['git', 'checkout', '$remoteName/$candidateBranch'], command: <String>['git', 'checkout', '$remoteName/$candidateBranch'],
), ),
...@@ -493,14 +734,74 @@ void main() { ...@@ -493,14 +734,74 @@ void main() {
operatingSystem: localOperatingSystem, operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator, pathSeparator: localPathSeparator,
); );
final pb.ConductorState state = pb.ConductorState( writeStateToFile(
currentPhase: ReleasePhase.PUBLISH_VERSION, 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(finalState.currentPhase, ReleasePhase.PUBLISH_CHANNEL);
expect(stdio.stdout, contains('Are you ready to tag commit $revision1 as $releaseVersion'));
expect(finalState.logs, stdio.logs);
});
});
group('PUBLISH_CHANNEL to VERIFY_RELEASE', () {
const String remoteName = 'upstream';
pb.ConductorState state;
FakePlatform platform;
setUp(() {
state = pb.ConductorState(
currentPhase: ReleasePhase.PUBLISH_CHANNEL,
framework: pb.Repository( framework: pb.Repository(
candidateBranch: candidateBranch, candidateBranch: candidateBranch,
upstream: pb.Remote(url: FrameworkRepository.defaultUpstream), upstream: pb.Remote(url: FrameworkRepository.defaultUpstream),
), ),
releaseChannel: releaseChannel,
releaseVersion: releaseVersion, releaseVersion: releaseVersion,
); );
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');
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['git', 'fetch', 'upstream'],
),
const FakeCommand(
command: <String>['git', 'checkout', '$remoteName/$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision1,
),
]);
writeStateToFile( writeStateToFile(
fileSystem.file(stateFile), fileSystem.file(stateFile),
state, state,
...@@ -524,16 +825,71 @@ void main() { ...@@ -524,16 +825,71 @@ void main() {
fileSystem.file(stateFile), fileSystem.file(stateFile),
); );
expect(processManager, hasNoRemainingExpectations);
expect(stdio.error, contains('Aborting command.'));
expect(
stdio.stdout,
contains('About to execute command: `git push ${FrameworkRepository.defaultUpstream} $revision1:$releaseChannel`'),
);
expect(finalState.currentPhase, ReleasePhase.PUBLISH_CHANNEL); expect(finalState.currentPhase, ReleasePhase.PUBLISH_CHANNEL);
expect(stdio.stdout, contains('Has CI passed for the framework PR?'));
expect(finalState.logs, stdio.logs);
expect(processManager.hasRemainingExpectations, false);
}); });
test('throws exception if state.currentPhase is RELEASE_COMPLETED', () async { test('updates currentPhase if user responds yes', () async {
final FakeProcessManager processManager = FakeProcessManager.list( stdio.stdin.add('y');
<FakeCommand>[], final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['git', 'fetch', 'upstream'],
),
const FakeCommand(
command: <String>['git', 'checkout', '$remoteName/$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision1,
),
const FakeCommand(
command: <String>['git', 'push', FrameworkRepository.defaultUpstream, '$revision1:$releaseChannel'],
),
]);
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.error, isEmpty);
expect(
stdio.stdout,
contains('About to execute command: `git push ${FrameworkRepository.defaultUpstream} $revision1:$releaseChannel`'),
);
expect(
stdio.stdout,
contains('Release archive packages must be verified on cloud storage: https://ci.chromium.org/p/flutter/g/beta_packaging/console'),
);
expect(finalState.currentPhase, ReleasePhase.VERIFY_RELEASE);
});
});
test('throws exception if state.currentPhase is RELEASE_COMPLETED', () async {
final FakeProcessManager processManager = FakeProcessManager.empty();
final FakePlatform platform = FakePlatform( final FakePlatform platform = FakePlatform(
environment: <String, String>{ environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator), 'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment