[flutter_conductor] Refactor next command (#91768)
This commit is contained in:
parent
7a7d9a276a
commit
5d587f9528
@ -4,12 +4,12 @@
|
||||
|
||||
import 'package:args/command_runner.dart';
|
||||
import 'package:file/file.dart' show File;
|
||||
import 'package:meta/meta.dart' show visibleForTesting;
|
||||
import 'package:meta/meta.dart' show visibleForTesting, visibleForOverriding;
|
||||
import './globals.dart';
|
||||
import './proto/conductor_state.pb.dart' as pb;
|
||||
import './proto/conductor_state.pbenum.dart';
|
||||
import './repository.dart';
|
||||
import './state.dart';
|
||||
import './state.dart' as state_import;
|
||||
import './stdio.dart';
|
||||
|
||||
const String kStateOption = 'state-file';
|
||||
@ -21,7 +21,7 @@ class NextCommand extends Command<void> {
|
||||
NextCommand({
|
||||
required this.checkouts,
|
||||
}) {
|
||||
final String defaultPath = defaultStateFilePath(checkouts.platform);
|
||||
final String defaultPath = state_import.defaultStateFilePath(checkouts.platform);
|
||||
argParser.addOption(
|
||||
kStateOption,
|
||||
defaultsTo: defaultPath,
|
||||
@ -48,352 +48,374 @@ class NextCommand extends Command<void> {
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
await runNext(
|
||||
await NextContext(
|
||||
autoAccept: argResults![kYesFlag] as bool,
|
||||
checkouts: checkouts,
|
||||
force: argResults![kForceFlag] as bool,
|
||||
stateFile: checkouts.fileSystem.file(argResults![kStateOption]),
|
||||
);
|
||||
).run();
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
bool prompt(String message, Stdio stdio) {
|
||||
stdio.write('${message.trim()} (y/n) ');
|
||||
final String response = stdio.readLineSync().trim();
|
||||
final String firstChar = response[0].toUpperCase();
|
||||
if (firstChar == 'Y') {
|
||||
return true;
|
||||
}
|
||||
if (firstChar == 'N') {
|
||||
return false;
|
||||
}
|
||||
throw ConductorException(
|
||||
'Unknown user input (expected "y" or "n"): $response',
|
||||
);
|
||||
}
|
||||
/// Utility class for proceeding to the next step in a release.
|
||||
///
|
||||
/// Any calls to functions that cause side effects are wrapped in methods to
|
||||
/// allow overriding in unit tests.
|
||||
class NextContext {
|
||||
NextContext({
|
||||
required this.autoAccept,
|
||||
required this.force,
|
||||
required this.checkouts,
|
||||
required this.stateFile,
|
||||
});
|
||||
|
||||
@visibleForTesting
|
||||
Future<void> runNext({
|
||||
required bool autoAccept,
|
||||
required bool force,
|
||||
required Checkouts checkouts,
|
||||
required File stateFile,
|
||||
}) async {
|
||||
final Stdio stdio = checkouts.stdio;
|
||||
const List<CherrypickState> finishedStates = <CherrypickState>[
|
||||
CherrypickState.COMPLETED,
|
||||
CherrypickState.ABANDONED,
|
||||
];
|
||||
if (!stateFile.existsSync()) {
|
||||
throw ConductorException(
|
||||
'No persistent state file found at ${stateFile.path}.',
|
||||
);
|
||||
}
|
||||
final bool autoAccept;
|
||||
final bool force;
|
||||
final Checkouts checkouts;
|
||||
final File stateFile;
|
||||
|
||||
final pb.ConductorState state = readStateFromFile(stateFile);
|
||||
|
||||
switch (state.currentPhase) {
|
||||
case pb.ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
|
||||
final Remote upstream = Remote(
|
||||
name: RemoteName.upstream,
|
||||
url: state.engine.upstream.url,
|
||||
Future<void> run() async {
|
||||
final Stdio stdio = checkouts.stdio;
|
||||
const List<CherrypickState> finishedStates = <CherrypickState>[
|
||||
CherrypickState.COMPLETED,
|
||||
CherrypickState.ABANDONED,
|
||||
];
|
||||
if (!stateFile.existsSync()) {
|
||||
throw ConductorException(
|
||||
'No persistent state file found at ${stateFile.path}.',
|
||||
);
|
||||
final EngineRepository engine = EngineRepository(
|
||||
checkouts,
|
||||
initialRef: state.engine.workingBranch,
|
||||
upstreamRemote: upstream,
|
||||
previousCheckoutLocation: state.engine.checkoutPath,
|
||||
);
|
||||
// check if the candidate branch is enabled in .ci.yaml
|
||||
final CiYaml engineCiYaml = await engine.ciYaml;
|
||||
if (!engineCiYaml.enabledBranches.contains(state.engine.candidateBranch)) {
|
||||
engineCiYaml.enableBranch(state.engine.candidateBranch);
|
||||
// commit
|
||||
final String revision = await engine.commit(
|
||||
'add branch ${state.engine.candidateBranch} to enabled_branches in .ci.yaml',
|
||||
addFirst: true,
|
||||
);
|
||||
// append to list of cherrypicks so we know a PR is required
|
||||
state.engine.cherrypicks.add(pb.Cherrypick(
|
||||
appliedRevision: revision,
|
||||
state: pb.CherrypickState.COMPLETED,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (!requiresEnginePR(state)) {
|
||||
stdio.printStatus(
|
||||
'This release has no engine cherrypicks. No Engine PR is necessary.\n',
|
||||
);
|
||||
break;
|
||||
}
|
||||
final pb.ConductorState state = readStateFromFile(stateFile);
|
||||
|
||||
final List<pb.Cherrypick> unappliedCherrypicks = <pb.Cherrypick>[];
|
||||
for (final pb.Cherrypick cherrypick in state.engine.cherrypicks) {
|
||||
if (!finishedStates.contains(cherrypick.state)) {
|
||||
unappliedCherrypicks.add(cherrypick);
|
||||
switch (state.currentPhase) {
|
||||
case pb.ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
|
||||
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,
|
||||
);
|
||||
// check if the candidate branch is enabled in .ci.yaml
|
||||
final CiYaml engineCiYaml = await engine.ciYaml;
|
||||
if (!engineCiYaml.enabledBranches.contains(state.engine.candidateBranch)) {
|
||||
engineCiYaml.enableBranch(state.engine.candidateBranch);
|
||||
// commit
|
||||
final String revision = await engine.commit(
|
||||
'add branch ${state.engine.candidateBranch} to enabled_branches in .ci.yaml',
|
||||
addFirst: true,
|
||||
);
|
||||
// append to list of cherrypicks so we know a PR is required
|
||||
state.engine.cherrypicks.add(pb.Cherrypick(
|
||||
appliedRevision: revision,
|
||||
state: pb.CherrypickState.COMPLETED,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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 == 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;
|
||||
}
|
||||
}
|
||||
|
||||
await engine.pushRef(
|
||||
fromRef: 'HEAD',
|
||||
// Explicitly create new branch
|
||||
toRef: 'refs/heads/${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(
|
||||
'Has CI passed for the engine PR and binaries been codesigned?',
|
||||
stdio,
|
||||
);
|
||||
if (!response) {
|
||||
stdio.printError('Aborting command.');
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case pb.ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
|
||||
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_import.requiresEnginePR(state)) {
|
||||
stdio.printStatus(
|
||||
'This release also has no framework cherrypicks. Therefore, a framework\n'
|
||||
'pull request is not required.',
|
||||
'This release has no engine cherrypicks. No Engine PR is necessary.\n',
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
final Remote engineUpstreamRemote = Remote(
|
||||
name: RemoteName.upstream,
|
||||
url: 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(
|
||||
name: RemoteName.upstream,
|
||||
url: state.framework.upstream.url,
|
||||
);
|
||||
final FrameworkRepository framework = FrameworkRepository(
|
||||
checkouts,
|
||||
initialRef: state.framework.workingBranch,
|
||||
upstreamRemote: upstream,
|
||||
previousCheckoutLocation: state.framework.checkoutPath,
|
||||
);
|
||||
|
||||
// Check if the current candidate branch is enabled
|
||||
if (!(await framework.ciYaml).enabledBranches.contains(state.framework.candidateBranch)) {
|
||||
(await framework.ciYaml).enableBranch(state.framework.candidateBranch);
|
||||
// commit
|
||||
final String revision = await framework.commit(
|
||||
'add branch ${state.framework.candidateBranch} to enabled_branches in .ci.yaml',
|
||||
addFirst: true,
|
||||
);
|
||||
// append to list of cherrypicks so we know a PR is required
|
||||
state.framework.cherrypicks.add(pb.Cherrypick(
|
||||
appliedRevision: revision,
|
||||
state: pb.CherrypickState.COMPLETED,
|
||||
));
|
||||
}
|
||||
|
||||
stdio.printStatus('Rolling new engine hash $engineRevision to framework checkout...');
|
||||
final bool 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(
|
||||
appliedRevision: revision,
|
||||
state: pb.CherrypickState.COMPLETED,
|
||||
));
|
||||
}
|
||||
|
||||
final List<pb.Cherrypick> unappliedCherrypicks = <pb.Cherrypick>[];
|
||||
for (final pb.Cherrypick cherrypick in state.framework.cherrypicks) {
|
||||
if (!finishedStates.contains(cherrypick.state)) {
|
||||
unappliedCherrypicks.add(cherrypick);
|
||||
final List<pb.Cherrypick> unappliedCherrypicks = <pb.Cherrypick>[];
|
||||
for (final pb.Cherrypick cherrypick in state.engine.cherrypicks) {
|
||||
if (!finishedStates.contains(cherrypick.state)) {
|
||||
unappliedCherrypicks.add(cherrypick);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if (unappliedCherrypicks.length == 1) {
|
||||
stdio.printStatus('There was ${unappliedCherrypicks.length} cherrypick that was not auto-applied.',);
|
||||
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');
|
||||
}
|
||||
else {
|
||||
stdio.printStatus('There were ${unappliedCherrypicks.length} cherrypicks that were not auto-applied.',);
|
||||
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;
|
||||
}
|
||||
}
|
||||
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,
|
||||
await engine.pushRef(
|
||||
fromRef: 'HEAD',
|
||||
// Explicitly create new branch
|
||||
toRef: 'refs/heads/${state.engine.workingBranch}',
|
||||
remote: state.engine.mirror.name,
|
||||
);
|
||||
if (!response) {
|
||||
stdio.printError('Aborting command.');
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await framework.pushRef(
|
||||
fromRef: 'HEAD',
|
||||
// Explicitly create new branch
|
||||
toRef: 'refs/heads/${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');
|
||||
stdio.printStatus('post-submit CI has finished successfully.\n');
|
||||
final Remote upstream = Remote(
|
||||
name: RemoteName.upstream,
|
||||
url: state.framework.upstream.url,
|
||||
);
|
||||
final FrameworkRepository framework = FrameworkRepository(
|
||||
checkouts,
|
||||
// We explicitly want to check out the merged version from upstream
|
||||
initialRef: '${upstream.name}/${state.framework.candidateBranch}',
|
||||
upstreamRemote: upstream,
|
||||
previousCheckoutLocation: state.framework.checkoutPath,
|
||||
);
|
||||
final String headRevision = await framework.reverseParse('HEAD');
|
||||
if (autoAccept == false) {
|
||||
final bool response = prompt(
|
||||
'Are you ready to tag commit $headRevision as ${state.releaseVersion}\n'
|
||||
'and push to remote ${state.framework.upstream.url}?',
|
||||
stdio,
|
||||
);
|
||||
if (!response) {
|
||||
stdio.printError('Aborting command.');
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
return;
|
||||
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(
|
||||
'Has CI passed for the engine PR and binaries been codesigned?',
|
||||
stdio,
|
||||
);
|
||||
if (!response) {
|
||||
stdio.printError('Aborting command.');
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
await framework.tag(headRevision, state.releaseVersion, upstream.name);
|
||||
break;
|
||||
case pb.ReleasePhase.PUBLISH_CHANNEL:
|
||||
final Remote upstream = Remote(
|
||||
name: RemoteName.upstream,
|
||||
url: state.framework.upstream.url,
|
||||
);
|
||||
final FrameworkRepository framework = FrameworkRepository(
|
||||
checkouts,
|
||||
// We explicitly want to check out the merged version from upstream
|
||||
initialRef: '${upstream.name}/${state.framework.candidateBranch}',
|
||||
upstreamRemote: upstream,
|
||||
previousCheckoutLocation: state.framework.checkoutPath,
|
||||
);
|
||||
final String headRevision = await framework.reverseParse('HEAD');
|
||||
if (autoAccept == false) {
|
||||
// dryRun: true means print out git command
|
||||
break;
|
||||
case pb.ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
|
||||
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.',
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
final Remote engineUpstreamRemote = Remote(
|
||||
name: RemoteName.upstream,
|
||||
url: 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(
|
||||
name: RemoteName.upstream,
|
||||
url: state.framework.upstream.url,
|
||||
);
|
||||
final FrameworkRepository framework = FrameworkRepository(
|
||||
checkouts,
|
||||
initialRef: state.framework.workingBranch,
|
||||
upstreamRemote: upstream,
|
||||
previousCheckoutLocation: state.framework.checkoutPath,
|
||||
);
|
||||
|
||||
// Check if the current candidate branch is enabled
|
||||
if (!(await framework.ciYaml).enabledBranches.contains(state.framework.candidateBranch)) {
|
||||
(await framework.ciYaml).enableBranch(state.framework.candidateBranch);
|
||||
// commit
|
||||
final String revision = await framework.commit(
|
||||
'add branch ${state.framework.candidateBranch} to enabled_branches in .ci.yaml',
|
||||
addFirst: true,
|
||||
);
|
||||
// append to list of cherrypicks so we know a PR is required
|
||||
state.framework.cherrypicks.add(pb.Cherrypick(
|
||||
appliedRevision: revision,
|
||||
state: pb.CherrypickState.COMPLETED,
|
||||
));
|
||||
}
|
||||
|
||||
stdio.printStatus('Rolling new engine hash $engineRevision to framework checkout...');
|
||||
final bool 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(
|
||||
appliedRevision: revision,
|
||||
state: pb.CherrypickState.COMPLETED,
|
||||
));
|
||||
}
|
||||
|
||||
final List<pb.Cherrypick> unappliedCherrypicks = <pb.Cherrypick>[];
|
||||
for (final pb.Cherrypick cherrypick in state.framework.cherrypicks) {
|
||||
if (!finishedStates.contains(cherrypick.state)) {
|
||||
unappliedCherrypicks.add(cherrypick);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
||||
await framework.pushRef(
|
||||
fromRef: headRevision,
|
||||
toRef: state.releaseChannel,
|
||||
remote: state.framework.upstream.url,
|
||||
force: force,
|
||||
dryRun: true,
|
||||
fromRef: 'HEAD',
|
||||
// Explicitly create new branch
|
||||
toRef: 'refs/heads/${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');
|
||||
stdio.printStatus('post-submit CI has finished successfully.\n');
|
||||
final Remote upstream = Remote(
|
||||
name: RemoteName.upstream,
|
||||
url: state.framework.upstream.url,
|
||||
);
|
||||
final FrameworkRepository framework = FrameworkRepository(
|
||||
checkouts,
|
||||
// We explicitly want to check out the merged version from upstream
|
||||
initialRef: '${upstream.name}/${state.framework.candidateBranch}',
|
||||
upstreamRemote: upstream,
|
||||
previousCheckoutLocation: state.framework.checkoutPath,
|
||||
);
|
||||
final String headRevision = await framework.reverseParse('HEAD');
|
||||
if (autoAccept == false) {
|
||||
final bool response = prompt(
|
||||
'Are you ready to tag commit $headRevision as ${state.releaseVersion}\n'
|
||||
'and push to remote ${state.framework.upstream.url}?',
|
||||
stdio,
|
||||
);
|
||||
if (!response) {
|
||||
stdio.printError('Aborting command.');
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await framework.tag(headRevision, state.releaseVersion, upstream.name);
|
||||
break;
|
||||
case pb.ReleasePhase.PUBLISH_CHANNEL:
|
||||
final Remote upstream = Remote(
|
||||
name: RemoteName.upstream,
|
||||
url: state.framework.upstream.url,
|
||||
);
|
||||
final FrameworkRepository framework = FrameworkRepository(
|
||||
checkouts,
|
||||
// We explicitly want to check out the merged version from upstream
|
||||
initialRef: '${upstream.name}/${state.framework.candidateBranch}',
|
||||
upstreamRemote: upstream,
|
||||
previousCheckoutLocation: state.framework.checkoutPath,
|
||||
);
|
||||
final String headRevision = await framework.reverseParse('HEAD');
|
||||
if (autoAccept == false) {
|
||||
// dryRun: true means print out git command
|
||||
await 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 this release?',
|
||||
stdio,
|
||||
);
|
||||
if (!response) {
|
||||
stdio.printError('Aborting command.');
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
return;
|
||||
final bool response = prompt(
|
||||
'Are you ready to publish this release?',
|
||||
stdio,
|
||||
);
|
||||
if (!response) {
|
||||
stdio.printError('Aborting command.');
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
await framework.pushRef(
|
||||
fromRef: headRevision,
|
||||
toRef: state.releaseChannel,
|
||||
remote: state.framework.upstream.url,
|
||||
force: force,
|
||||
);
|
||||
break;
|
||||
case pb.ReleasePhase.VERIFY_RELEASE:
|
||||
stdio.printStatus(
|
||||
'The current status of packaging builds can be seen at:\n'
|
||||
'\t$kLuciPackagingConsoleLink',
|
||||
);
|
||||
if (autoAccept == false) {
|
||||
final bool response = prompt(
|
||||
'Have all packaging builds finished successfully?',
|
||||
stdio,
|
||||
await framework.pushRef(
|
||||
fromRef: headRevision,
|
||||
toRef: state.releaseChannel,
|
||||
remote: state.framework.upstream.url,
|
||||
force: force,
|
||||
);
|
||||
if (!response) {
|
||||
stdio.printError('Aborting command.');
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
return;
|
||||
break;
|
||||
case pb.ReleasePhase.VERIFY_RELEASE:
|
||||
stdio.printStatus(
|
||||
'The current status of packaging builds can be seen at:\n'
|
||||
'\t$kLuciPackagingConsoleLink',
|
||||
);
|
||||
if (autoAccept == false) {
|
||||
final bool response = prompt(
|
||||
'Have all packaging builds finished successfully?',
|
||||
stdio,
|
||||
);
|
||||
if (!response) {
|
||||
stdio.printError('Aborting command.');
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case pb.ReleasePhase.RELEASE_COMPLETED:
|
||||
throw ConductorException('This release is finished.');
|
||||
break;
|
||||
case pb.ReleasePhase.RELEASE_COMPLETED:
|
||||
throw ConductorException('This release is finished.');
|
||||
}
|
||||
final ReleasePhase nextPhase = state_import.getNextPhase(state.currentPhase);
|
||||
stdio.printStatus('\nUpdating phase from ${state.currentPhase} to $nextPhase...\n');
|
||||
state.currentPhase = nextPhase;
|
||||
stdio.printStatus(state_import.phaseInstructions(state));
|
||||
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
}
|
||||
final ReleasePhase nextPhase = getNextPhase(state.currentPhase);
|
||||
stdio.printStatus('\nUpdating phase from ${state.currentPhase} to $nextPhase...\n');
|
||||
state.currentPhase = nextPhase;
|
||||
stdio.printStatus(phaseInstructions(state));
|
||||
|
||||
writeStateToFile(stateFile, state, stdio.logs);
|
||||
/// Persist the state to a file.
|
||||
@visibleForOverriding
|
||||
void writeStateToFile(File file, pb.ConductorState state, [List<String> logs = const <String>[]]) {
|
||||
state_import.writeStateToFile(file, state, logs);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
bool prompt(String message, Stdio stdio) {
|
||||
stdio.write('${message.trim()} (y/n) ');
|
||||
final String response = stdio.readLineSync().trim();
|
||||
final String firstChar = response[0].toUpperCase();
|
||||
if (firstChar == 'Y') {
|
||||
return true;
|
||||
}
|
||||
if (firstChar == 'N') {
|
||||
return false;
|
||||
}
|
||||
throw ConductorException(
|
||||
'Unknown user input (expected "y" or "n"): $response',
|
||||
);
|
||||
}
|
||||
|
||||
/// Read the state from a file.
|
||||
@visibleForOverriding
|
||||
pb.ConductorState readStateFromFile(File file) => state_import.readStateFromFile(file);
|
||||
}
|
||||
|
@ -1052,6 +1052,34 @@ void main() {
|
||||
}, onPlatform: <String, dynamic>{
|
||||
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
|
||||
});
|
||||
|
||||
group('prompt', () {
|
||||
test('throws if user inputs character that is not "y" or "n"', () {
|
||||
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||
final TestStdio stdio = TestStdio(
|
||||
stdin: <String>['x'],
|
||||
verbose: true,
|
||||
);
|
||||
final Checkouts checkouts = Checkouts(
|
||||
fileSystem: fileSystem,
|
||||
parentDirectory: fileSystem.directory('/'),
|
||||
platform: FakePlatform(),
|
||||
processManager: FakeProcessManager.empty(),
|
||||
stdio: stdio,
|
||||
);
|
||||
final NextContext context = NextContext(
|
||||
autoAccept: false,
|
||||
force: false,
|
||||
checkouts: checkouts,
|
||||
stateFile: fileSystem.file('/statefile.json'),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => context.prompt('Asking a question?', stdio),
|
||||
throwsExceptionWith('Unknown user input (expected "y" or "n")'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _initializeCiYamlFile(
|
||||
|
Loading…
x
Reference in New Issue
Block a user