// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // @dart = 2.8 import 'package:platform/platform.dart'; import './globals.dart'; import './proto/conductor_state.pb.dart' as pb; import './proto/conductor_state.pbenum.dart' show ReleasePhase; const String kStateFileName = '.flutter_conductor_state.json'; String luciConsoleLink(String channel, String groupName) { assert( ['stable', 'beta', 'dev', 'master'].contains(channel), 'channel $channel not recognized', ); assert( ['framework', 'engine', 'devicelab'].contains(groupName), 'group named $groupName not recognized', ); final String consoleName = channel == 'master' ? groupName : '${channel}_$groupName'; return 'https://ci.chromium.org/p/flutter/g/$consoleName/console'; } String defaultStateFilePath(Platform platform) { assert(platform.environment['HOME'] != null); return [ platform.environment['HOME'], kStateFileName, ].join(platform.pathSeparator); } String presentState(pb.ConductorState state) { final StringBuffer buffer = StringBuffer(); buffer.writeln('Conductor version: ${state.conductorVersion}'); buffer.writeln('Release channel: ${state.releaseChannel}'); buffer.writeln(''); buffer.writeln( 'Release started at: ${DateTime.fromMillisecondsSinceEpoch(state.createdDate.toInt())}'); buffer.writeln( 'Last updated at: ${DateTime.fromMillisecondsSinceEpoch(state.lastUpdatedDate.toInt())}'); buffer.writeln(''); buffer.writeln('Engine Repo'); buffer.writeln('\tCandidate branch: ${state.engine.candidateBranch}'); buffer.writeln('\tStarting git HEAD: ${state.engine.startingGitHead}'); buffer.writeln('\tCurrent git HEAD: ${state.engine.currentGitHead}'); buffer.writeln('\tPath to checkout: ${state.engine.checkoutPath}'); buffer.writeln('\tPost-submit LUCI dashboard: ${luciConsoleLink(state.releaseChannel, 'engine')}'); if (state.engine.cherrypicks.isNotEmpty) { buffer.writeln('${state.engine.cherrypicks.length} Engine Cherrypicks:'); for (final pb.Cherrypick cherrypick in state.engine.cherrypicks) { buffer.writeln('\t${cherrypick.trunkRevision} - ${cherrypick.state}'); } } else { buffer.writeln('0 Engine cherrypicks.'); } buffer.writeln('Framework Repo'); buffer.writeln('\tCandidate branch: ${state.framework.candidateBranch}'); buffer.writeln('\tStarting git HEAD: ${state.framework.startingGitHead}'); buffer.writeln('\tCurrent git HEAD: ${state.framework.currentGitHead}'); buffer.writeln('\tPath to checkout: ${state.framework.checkoutPath}'); buffer.writeln('\tPost-submit LUCI dashboard: ${luciConsoleLink(state.releaseChannel, 'framework')}'); buffer.writeln('\tDevicelab LUCI dashboard: ${luciConsoleLink(state.releaseChannel, 'devicelab')}'); if (state.framework.cherrypicks.isNotEmpty) { buffer.writeln('${state.framework.cherrypicks.length} Framework Cherrypicks:'); for (final pb.Cherrypick cherrypick in state.framework.cherrypicks) { buffer.writeln('\t${cherrypick.trunkRevision} - ${cherrypick.state}'); } } else { buffer.writeln('0 Framework cherrypicks.'); } buffer.writeln(''); if (state.lastPhase == ReleasePhase.VERIFY_RELEASE) { buffer.writeln( '${state.releaseChannel} release ${state.releaseVersion} has been published and verified.\n', ); return buffer.toString(); } buffer.writeln('The next step is:'); buffer.writeln(presentPhases(state.lastPhase)); buffer.writeln(phaseInstructions(state)); buffer.writeln(''); buffer.writeln('Issue `conductor next` when you are ready to proceed.'); return buffer.toString(); } String presentPhases(ReleasePhase lastPhase) { final ReleasePhase nextPhase = getNextPhase(lastPhase); final StringBuffer buffer = StringBuffer(); bool phaseCompleted = true; for (final ReleasePhase phase in ReleasePhase.values) { if (phase == nextPhase) { // This phase will execute the next time `conductor next` is run. buffer.writeln('> ${phase.name} (next)'); phaseCompleted = false; } else if (phaseCompleted) { // This phase was already completed. buffer.writeln('✓ ${phase.name}'); } else { // This phase has not been completed yet. buffer.writeln(' ${phase.name}'); } } return buffer.toString(); } String phaseInstructions(pb.ConductorState state) { switch (state.lastPhase) { case ReleasePhase.INITIALIZE: if (state.engine.cherrypicks.isEmpty) { return [ 'There are no engine cherrypicks, so issue `conductor next` to continue', 'to the next step.', ].join('\n'); } return [ 'You must now manually apply the following engine cherrypicks to the checkout', 'at ${state.engine.checkoutPath} in order:', for (final pb.Cherrypick cherrypick in state.engine.cherrypicks) '\t${cherrypick.trunkRevision}', 'See $kReleaseDocumentationUrl for more information.', ].join('\n'); case ReleasePhase.APPLY_ENGINE_CHERRYPICKS: return [ 'You must verify Engine CI builds are successful and then codesign the', 'binaries at revision ${state.engine.currentGitHead}.', ].join('\n'); case ReleasePhase.CODESIGN_ENGINE_BINARIES: return [ 'You must now manually apply the following framework cherrypicks to the checkout', 'at ${state.framework.checkoutPath} in order:', for (final pb.Cherrypick cherrypick in state.framework.cherrypicks) '\t${cherrypick.trunkRevision}', ].join('\n'); case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS: return [ 'You must verify Framework CI builds are successful.', 'See $kReleaseDocumentationUrl for more information.', ].join('\n'); case ReleasePhase.PUBLISH_VERSION: return 'Issue `conductor next` to publish your release to the release branch.'; case ReleasePhase.PUBLISH_CHANNEL: return [ 'Release archive packages must be verified on cloud storage. Issue', '`conductor next` to check if they are ready.', ].join('\n'); case ReleasePhase.VERIFY_RELEASE: return 'This release has been completed.'; } assert(false); return ''; // For analyzer } /// Returns the next phase in the ReleasePhase enum. /// /// Will throw a [ConductorException] if [ReleasePhase.RELEASE_VERIFIED] is /// passed as an argument, as there is no next phase. ReleasePhase getNextPhase(ReleasePhase previousPhase) { assert(previousPhase != null); if (previousPhase == ReleasePhase.VERIFY_RELEASE) { throw ConductorException('There is no next ReleasePhase!'); } return ReleasePhase.valueOf(previousPhase.value + 1); }