[flutter_conductor] Support publishing to multiple channels (#94770)

diff --git a/dev/conductor/core/lib/src/globals.dart b/dev/conductor/core/lib/src/globals.dart
index 9c010c8..b0a5d25 100644
--- a/dev/conductor/core/lib/src/globals.dart
+++ b/dev/conductor/core/lib/src/globals.dart
@@ -16,7 +16,7 @@
 
 const List<String> kReleaseChannels = <String>[...kBaseReleaseChannels, FrameworkRepository.defaultBranch];
 
-const List<String> KReleaseIncrements = <String>['y', 'z', 'm', 'n'];
+const List<String> kReleaseIncrements = <String>['y', 'z', 'm', 'n'];
 
 const String kReleaseDocumentationUrl = 'https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process';
 
@@ -28,6 +28,10 @@
   r'flutter-(\d+)\.(\d+)-candidate\.(\d+)',
 );
 
+/// Whether all releases published to the beta channel should be mirrored to
+/// dev.
+const bool kSynchronizeDevWithBeta = true;
+
 /// Cast a dynamic to String and trim.
 String stdoutToString(dynamic input) {
   final String str = input as String;
diff --git a/dev/conductor/core/lib/src/next.dart b/dev/conductor/core/lib/src/next.dart
index 9ece748..575ce03 100644
--- a/dev/conductor/core/lib/src/next.dart
+++ b/dev/conductor/core/lib/src/next.dart
@@ -48,7 +48,7 @@
   String get description => 'Proceed to the next release phase.';
 
   @override
-  Future<void> run() async {
+  Future<void> run() {
     final File stateFile = checkouts.fileSystem.file(argResults![kStateOption]);
     if (!stateFile.existsSync()) {
       throw ConductorException(
@@ -57,7 +57,7 @@
     }
     final pb.ConductorState state = state_import.readStateFromFile(stateFile);
 
-    await NextContext(
+    return NextContext(
       autoAccept: argResults![kYesFlag] as bool,
       checkouts: checkouts,
       force: argResults![kForceFlag] as bool,
@@ -324,29 +324,37 @@
             previousCheckoutLocation: state.framework.checkoutPath,
         );
         final String headRevision = await framework.reverseParse('HEAD');
-        if (autoAccept == false) {
-          // dryRun: true means print out git command
-          await framework.pushRef(
+        final List<String> releaseRefs = <String>[state.releaseChannel];
+        if (kSynchronizeDevWithBeta && state.releaseChannel == 'beta') {
+          releaseRefs.add('dev');
+        }
+        for (final String releaseRef in releaseRefs) {
+          if (autoAccept == false) {
+            // dryRun: true means print out git command
+            await framework.pushRef(
               fromRef: headRevision,
-              toRef: state.releaseChannel,
+              toRef: releaseRef,
               remote: state.framework.upstream.url,
               force: force,
               dryRun: true,
-          );
+            );
 
-          final bool response = await prompt('Are you ready to publish this release?');
-          if (!response) {
-            stdio.printError('Aborting command.');
-            updateState(state, stdio.logs);
-            return;
+            final bool response = await prompt(
+              'Are you ready to publish version ${state.releaseVersion} to $releaseRef?',
+            );
+            if (!response) {
+              stdio.printError('Aborting command.');
+              updateState(state, stdio.logs);
+              return;
+            }
           }
-        }
-        await framework.pushRef(
+          await framework.pushRef(
             fromRef: headRevision,
-            toRef: state.releaseChannel,
+            toRef: releaseRef,
             remote: state.framework.upstream.url,
             force: force,
-        );
+          );
+        }
         break;
       case pb.ReleasePhase.VERIFY_RELEASE:
         stdio.printStatus(
diff --git a/dev/conductor/core/lib/src/start.dart b/dev/conductor/core/lib/src/start.dart
index 4135441..3227396 100644
--- a/dev/conductor/core/lib/src/start.dart
+++ b/dev/conductor/core/lib/src/start.dart
@@ -93,7 +93,7 @@
       kIncrementOption,
       help: 'Specifies which part of the x.y.z version number to increment. Required.',
       valueHelp: 'level',
-      allowed: KReleaseIncrements,
+      allowed: kReleaseIncrements,
       allowedHelp: <String, String>{
         'y': 'Indicates the first dev release after a beta release.',
         'z': 'Indicates a hotfix to a stable release.',
diff --git a/dev/conductor/core/lib/src/version.dart b/dev/conductor/core/lib/src/version.dart
index 85b21fd..8c29e61 100644
--- a/dev/conductor/core/lib/src/version.dart
+++ b/dev/conductor/core/lib/src/version.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import './globals.dart' show ConductorException, KReleaseIncrements, releaseCandidateBranchRegex;
+import './globals.dart' show ConductorException, kReleaseIncrements, releaseCandidateBranchRegex;
 
 /// Possible string formats that `flutter --version` can return.
 enum VersionType {
@@ -262,7 +262,7 @@
   /// Will throw a [ConductorException] if the version is not possible given the
   /// [candidateBranch] and [incrementLetter].
   void ensureValid(String candidateBranch, String incrementLetter) {
-    if (!KReleaseIncrements.contains(incrementLetter)) {
+    if (!kReleaseIncrements.contains(incrementLetter)) {
       throw ConductorException('Invalid incrementLetter: $incrementLetter');
     }
     final RegExpMatch? branchMatch = releaseCandidateBranchRegex.firstMatch(candidateBranch);
diff --git a/dev/conductor/core/test/common.dart b/dev/conductor/core/test/common.dart
index 93821b9..b661271 100644
--- a/dev/conductor/core/test/common.dart
+++ b/dev/conductor/core/test/common.dart
@@ -48,7 +48,7 @@
   @override
   String readLineSync() {
     if (stdin.isEmpty) {
-      throw Exception('Unexpected call to readLineSync!');
+      throw Exception('Unexpected call to readLineSync! Last stdout was ${logs.last}');
     }
     return stdin.removeAt(0);
   }
diff --git a/dev/conductor/core/test/next_test.dart b/dev/conductor/core/test/next_test.dart
index 1eb1703..edfb98c 100644
--- a/dev/conductor/core/test/next_test.dart
+++ b/dev/conductor/core/test/next_test.dart
@@ -967,6 +967,8 @@
 
       test('updates currentPhase if user responds yes', () async {
         stdio.stdin.add('y');
+        // for kSynchronizeDevWithBeta
+        stdio.stdin.add('y');
         final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
           const FakeCommand(
             command: <String>['git', 'fetch', 'upstream'],
@@ -981,6 +983,10 @@
           const FakeCommand(
             command: <String>['git', 'push', FrameworkRepository.defaultUpstream, '$revision1:$releaseChannel'],
           ),
+          // for kSynchronizeDevWithBeta
+          const FakeCommand(
+            command: <String>['git', 'push', FrameworkRepository.defaultUpstream, '$revision1:dev'],
+          ),
         ]);
         writeStateToFile(
           fileSystem.file(stateFile),