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,44 +104,58 @@ void runNext({
}
}
if (state.engine.cherrypicks.isEmpty) {
stdio.printStatus('This release has no engine cherrypicks.');
if (state.engine.cherrypicks.isEmpty && state.engine.dartRevision.isEmpty) {
stdio.printStatus(
'This release has no engine cherrypicks. No Engine PR is necessary.\n',
);
break;
} else if (unappliedCherrypicks.isEmpty) {
}
if (unappliedCherrypicks.isEmpty) {
stdio.printStatus('All engine 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.engine.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
} 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 == false) {
final bool response = prompt(
'Are you ready to push your engine branch to the repository '
'${state.engine.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to push your engine branch to the repository '
'${state.engine.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
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;
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) {
// TODO(fujino): actually test if binaries have been codesigned on macOS
final bool response = prompt(
......@@ -163,42 +177,83 @@ void runNext({
}
}
if (state.framework.cherrypicks.isEmpty) {
stdio.printStatus('This release has no framework cherrypicks.');
break;
} else if (unappliedCherrypicks.isEmpty) {
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 (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) {
stdio.printStatus(
'This release also has no framework cherrypicks. Therefore, a framework\n'
'pull request is not required.',
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
break;
}
}
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 {
stdio.printStatus(
'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');
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to push your framework branch to the repository '
'${state.framework.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
'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',
);
}
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to push your framework branch to the repository '
'${state.framework.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
framework.pushRef(
fromRef: headRevision,
toRef: state.framework.workingBranch,
remote: state.framework.mirror.name,
);
break;
case pb.ReleasePhase.PUBLISH_VERSION:
stdio.printStatus('Please ensure that you have merged your framework PR and that');
......@@ -216,7 +271,8 @@ void runNext({
final String headRevision = framework.reverseParse('HEAD');
if (autoAccept == false) {
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,
);
if (!response) {
......@@ -240,9 +296,17 @@ void runNext({
);
final String headRevision = framework.reverseParse('HEAD');
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(
'Are you ready to publish release ${state.releaseVersion} to '
'channel ${state.releaseChannel} at ${state.framework.upstream.url}?',
'Are you ready to publish this release?',
stdio,
);
if (!response) {
......@@ -251,10 +315,10 @@ void runNext({
return;
}
}
framework.updateChannel(
headRevision,
state.framework.upstream.url,
state.releaseChannel,
framework.pushRef(
fromRef: headRevision,
toRef: state.releaseChannel,
remote: state.framework.upstream.url,
force: force,
);
break;
......
......@@ -204,6 +204,8 @@ class Repository extends $pb.GeneratedMessage {
subBuilder: Cherrypick.create)
..aOS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dartRevision',
protoName: 'dartRevision')
..aOS(9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'workingBranch',
protoName: 'workingBranch')
..hasRequiredFields = false;
Repository._() : super();
......@@ -216,6 +218,7 @@ class Repository extends $pb.GeneratedMessage {
Remote mirror,
$core.Iterable<Cherrypick> cherrypicks,
$core.String dartRevision,
$core.String workingBranch,
}) {
final _result = create();
if (candidateBranch != null) {
......@@ -242,6 +245,9 @@ class Repository extends $pb.GeneratedMessage {
if (dartRevision != null) {
_result.dartRevision = dartRevision;
}
if (workingBranch != null) {
_result.workingBranch = workingBranch;
}
return _result;
}
factory Repository.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
......@@ -356,6 +362,18 @@ class Repository extends $pb.GeneratedMessage {
$core.bool hasDartRevision() => $_has(7);
@$pb.TagNumber(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 {
......
......@@ -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': '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': 'workingBranch', '3': 9, '4': 1, '5': 9, '10': 'workingBranch'},
],
};
/// Descriptor for `Repository`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List repositoryDescriptor = $convert.base64Decode(
'CgpSZXBvc2l0b3J5EigKD2NhbmRpZGF0ZUJyYW5jaBgBIAEoCVIPY2FuZGlkYXRlQnJhbmNoEigKD3N0YXJ0aW5nR2l0SGVhZBgCIAEoCVIPc3RhcnRpbmdHaXRIZWFkEiYKDmN1cnJlbnRHaXRIZWFkGAMgASgJUg5jdXJyZW50R2l0SGVhZBIiCgxjaGVja291dFBhdGgYBCABKAlSDGNoZWNrb3V0UGF0aBIzCgh1cHN0cmVhbRgFIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSCHVwc3RyZWFtEi8KBm1pcnJvchgGIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSBm1pcnJvchI9CgtjaGVycnlwaWNrcxgHIAMoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5DaGVycnlwaWNrUgtjaGVycnlwaWNrcxIiCgxkYXJ0UmV2aXNpb24YCCABKAlSDGRhcnRSZXZpc2lvbg==');
'CgpSZXBvc2l0b3J5EigKD2NhbmRpZGF0ZUJyYW5jaBgBIAEoCVIPY2FuZGlkYXRlQnJhbmNoEigKD3N0YXJ0aW5nR2l0SGVhZBgCIAEoCVIPc3RhcnRpbmdHaXRIZWFkEiYKDmN1cnJlbnRHaXRIZWFkGAMgASgJUg5jdXJyZW50R2l0SGVhZBIiCgxjaGVja291dFBhdGgYBCABKAlSDGNoZWNrb3V0UGF0aBIzCgh1cHN0cmVhbRgFIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSCHVwc3RyZWFtEi8KBm1pcnJvchgGIAEoCzIXLmNvbmR1Y3Rvcl9zdGF0ZS5SZW1vdGVSBm1pcnJvchI9CgtjaGVycnlwaWNrcxgHIAMoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5DaGVycnlwaWNrUgtjaGVycnlwaWNrcxIiCgxkYXJ0UmV2aXNpb24YCCABKAlSDGRhcnRSZXZpc2lvbhIkCg13b3JraW5nQnJhbmNoGAkgASgJUg13b3JraW5nQnJhbmNo');
@$core.Deprecated('Use conductorStateDescriptor instead')
const ConductorState$json = const {
'1': 'ConductorState',
......
......@@ -84,6 +84,12 @@ message Repository {
// Only for engine repositories.
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 {
......
......@@ -25,7 +25,9 @@ class Remote {
const Remote({
required RemoteName name,
required this.url,
}) : _name = name, assert(url != null), assert (url != '');
}) : _name = name,
assert(url != null),
assert(url != '');
final RemoteName _name;
......@@ -63,9 +65,15 @@ abstract class Repository {
if (previousCheckoutLocation != null) {
_checkoutDirectory = fileSystem.directory(previousCheckoutLocation);
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) {
git.run(
<String>['fetch', upstreamRemote.name],
'Fetch ${upstreamRemote.name} to ensure we have latest refs',
workingDirectory: _checkoutDirectory!.path,
);
git.run(
<String>['checkout', '${upstreamRemote.name}/$initialRef'],
'Checking out initialRef $initialRef',
......@@ -255,11 +263,9 @@ abstract class Repository {
/// List commits in reverse chronological order.
List<String> revList(List<String> args) {
return git
.getOutput(
<String>['rev-list', ...args],
'rev-list with args ${args.join(' ')}',
workingDirectory: checkoutDirectory.path
)
.getOutput(<String>['rev-list', ...args],
'rev-list with args ${args.join(' ')}',
workingDirectory: checkoutDirectory.path)
.trim()
.split('\n');
}
......@@ -356,22 +362,33 @@ abstract class Repository {
}
/// Push [commit] to the release channel [branch].
void updateChannel(
String commit,
String remote,
String branch, {
void pushRef({
required String fromRef,
required String remote,
required String toRef,
bool force = false,
bool dryRun = false,
}) {
git.run(
<String>[
'push',
if (force) '--force',
remote,
'$commit:$branch',
],
'update the release branch with the commit',
workingDirectory: checkoutDirectory.path,
);
final List<String> args = <String>[
'push',
if (force) '--force',
remote,
'$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',
workingDirectory: checkoutDirectory.path,
);
stdio.printStatus('Executed command: `$command`');
}
}
String commit(
......@@ -566,6 +583,21 @@ class FrameworkRepository extends Repository {
) as Map<String, dynamic>;
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.
......@@ -578,14 +610,14 @@ class HostFrameworkRepository extends FrameworkRepository {
String name = 'host-framework',
required String upstreamPath,
}) : super(
checkouts,
name: name,
upstreamRemote: Remote(
name: RemoteName.upstream,
url: 'file://$upstreamPath/',
),
localUpstream: false,
) {
checkouts,
name: name,
upstreamRemote: Remote(
name: RemoteName.upstream,
url: 'file://$upstreamPath/',
),
localUpstream: false,
) {
_checkoutDirectory = checkouts.fileSystem.directory(upstreamPath);
}
......@@ -594,17 +626,20 @@ class HostFrameworkRepository extends FrameworkRepository {
@override
void newBranch(String branchName) {
throw ConductorException('newBranch not implemented for the host repository');
throw ConductorException(
'newBranch not implemented for the host repository');
}
@override
void checkout(String ref) {
throw ConductorException('checkout not implemented for the host repository');
throw ConductorException(
'checkout not implemented for the host repository');
}
@override
String cherryPick(String commit) {
throw ConductorException('cherryPick not implemented for the host repository');
throw ConductorException(
'cherryPick not implemented for the host repository');
}
@override
......@@ -617,14 +652,15 @@ class HostFrameworkRepository extends FrameworkRepository {
throw ConductorException('tag not implemented for the host repository');
}
@override
void updateChannel(
String commit,
String remote,
String branch, {
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
......@@ -673,20 +709,20 @@ class EngineRepository extends Repository {
depsFile ??= 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);
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}.'
);
'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);
depsFile.writeAsStringSync(updatedFileContent, flush: true);
}
@override
......@@ -716,7 +752,7 @@ class Checkouts {
required this.stdio,
required Directory parentDirectory,
String directoryName = 'flutter_conductor_checkouts',
}) : directory = parentDirectory.childDirectory(directoryName) {
}) : directory = parentDirectory.childDirectory(directoryName) {
if (!directory.existsSync()) {
directory.createSync(recursive: true);
}
......
......@@ -191,10 +191,10 @@ bool rollDev({
repository.tag(commit, version.toString(), remoteName);
}
repository.updateChannel(
commit,
remoteName,
'dev',
repository.pushRef(
fromRef: commit,
remote: remoteName,
toRef: 'dev',
force: force,
);
......
......@@ -100,7 +100,7 @@ class StartCommand extends Command<void> {
'y': 'Indicates the first dev release after a beta release.',
'z': 'Indicates a hotfix to a stable 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);
......@@ -230,7 +230,8 @@ class StartCommand extends Command<void> {
// Create a new branch so that we don't accidentally push to upstream
// candidateBranch.
engine.newBranch('cherrypicks-$candidateBranch');
final String workingBranchName = 'cherrypicks-$candidateBranch';
engine.newBranch(workingBranchName);
if (dartRevision != null && dartRevision.isNotEmpty) {
engine.updateDartRevision(dartRevision);
......@@ -262,6 +263,7 @@ class StartCommand extends Command<void> {
final String engineHead = engine.reverseParse('HEAD');
state.engine = pb.Repository(
candidateBranch: candidateBranch,
workingBranch: workingBranchName,
startingGitHead: engineHead,
currentGitHead: engineHead,
checkoutPath: engine.checkoutDirectory.path,
......@@ -282,7 +284,7 @@ class StartCommand extends Command<void> {
url: frameworkMirror,
),
);
framework.newBranch('cherrypicks-$candidateBranch');
framework.newBranch(workingBranchName);
final List<pb.Cherrypick> frameworkCherrypicks = _sortCherrypicks(
repository: framework,
cherrypicks: frameworkCherrypickRevisions,
......@@ -320,6 +322,7 @@ class StartCommand extends Command<void> {
final String frameworkHead = framework.reverseParse('HEAD');
state.framework = pb.Repository(
candidateBranch: candidateBranch,
workingBranch: workingBranchName,
startingGitHead: frameworkHead,
currentGitHead: frameworkHead,
checkoutPath: framework.checkoutDirectory.path,
......
......@@ -21,7 +21,7 @@ String luciConsoleLink(String channel, String groupName) {
'channel $channel not recognized',
);
assert(
<String>['framework', 'engine', 'devicelab'].contains(groupName),
<String>['framework', 'engine', 'devicelab', 'packaging'].contains(groupName),
'group named $groupName not recognized',
);
final String consoleName = channel == 'master' ? groupName : '${channel}_$groupName';
......@@ -133,8 +133,9 @@ String phaseInstructions(pb.ConductorState state) {
].join('\n');
case ReleasePhase.CODESIGN_ENGINE_BINARIES:
return <String>[
'You must verify Engine CI builds are successful and then codesign the',
'binaries at revision ${state.engine.currentGitHead}.',
'You must verify pre-submit CI builds on your engine pull request are successful,',
'merge your pull request, validate post-submit CI, and then codesign the binaries ',
'on the merge commit.',
].join('\n');
case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
final List<pb.Cherrypick> outstandingCherrypicks = state.framework.cherrypicks.where(
......@@ -150,13 +151,14 @@ String phaseInstructions(pb.ConductorState state) {
].join('\n');
case ReleasePhase.PUBLISH_VERSION:
return <String>[
'You must verify Framework CI builds are successful.',
'See $kReleaseDocumentationUrl for more information.',
'You must verify pre-submit CI builds on your framework pull request are successful,',
'merge your pull request, and validate post-submit CI. See $kReleaseDocumentationUrl,',
'for more information.',
].join('\n');
case ReleasePhase.PUBLISH_CHANNEL:
return 'Issue `conductor next` to publish your release to the release branch.';
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:
return 'This release has been completed.';
}
......
This diff is collapsed.
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