| // 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. |
| |
| import 'package:args/args.dart'; |
| import 'package:args/command_runner.dart'; |
| import 'package:file/file.dart'; |
| import 'package:fixnum/fixnum.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:platform/platform.dart'; |
| import 'package:process/process.dart'; |
| |
| import 'context.dart'; |
| import 'git.dart'; |
| import 'globals.dart'; |
| import 'proto/conductor_state.pb.dart' as pb; |
| import 'proto/conductor_state.pbenum.dart'; |
| import 'repository.dart'; |
| import 'state.dart' as state_import; |
| import 'stdio.dart'; |
| import 'version.dart'; |
| |
| const String kCandidateOption = 'candidate-branch'; |
| const String kDartRevisionOption = 'dart-revision'; |
| const String kEngineUpstreamOption = 'engine-upstream'; |
| const String kFrameworkMirrorOption = 'framework-mirror'; |
| const String kFrameworkUpstreamOption = 'framework-upstream'; |
| const String kEngineMirrorOption = 'engine-mirror'; |
| const String kReleaseOption = 'release-channel'; |
| const String kStateOption = 'state-file'; |
| const String kVersionOverrideOption = 'version-override'; |
| const String kGithubUsernameOption = 'github-username'; |
| |
| /// Command to print the status of the current Flutter release. |
| /// |
| /// This command has many required options which the user must provide |
| /// via command line arguments (or optionally environment variables). |
| /// |
| /// This command is the one with the worst user experience (as the user has to |
| /// carefully type out many different options into their terminal) and the one |
| /// that would benefit the most from a GUI frontend. This command will |
| /// optionally read its options from an environment variable to facilitate a workflow |
| /// in which configuration is provided by editing a bash script that sets environment |
| /// variables and then invokes the conductor tool. |
| class StartCommand extends Command<void> { |
| StartCommand({ |
| required this.checkouts, |
| required this.conductorVersion, |
| }) : platform = checkouts.platform, |
| processManager = checkouts.processManager, |
| fileSystem = checkouts.fileSystem, |
| stdio = checkouts.stdio { |
| final String defaultPath = state_import.defaultStateFilePath(platform); |
| argParser.addOption( |
| kCandidateOption, |
| help: 'The candidate branch the release will be based on.', |
| ); |
| argParser.addOption( |
| kReleaseOption, |
| help: 'The target release channel for the release.', |
| allowed: kBaseReleaseChannels, |
| ); |
| argParser.addOption( |
| kFrameworkUpstreamOption, |
| defaultsTo: FrameworkRepository.defaultUpstream, |
| help: |
| 'Configurable Framework repo upstream remote. Primarily for testing.', |
| hide: true, |
| ); |
| argParser.addOption( |
| kEngineUpstreamOption, |
| defaultsTo: EngineRepository.defaultUpstream, |
| help: 'Configurable Engine repo upstream remote. Primarily for testing.', |
| hide: true, |
| ); |
| argParser.addOption( |
| kStateOption, |
| defaultsTo: defaultPath, |
| help: 'Path to persistent state file. Defaults to $defaultPath', |
| ); |
| argParser.addOption( |
| kDartRevisionOption, |
| help: 'New Dart revision to cherrypick.', |
| ); |
| argParser.addFlag( |
| kForceFlag, |
| abbr: 'f', |
| help: 'Override all validations of the command line inputs.', |
| ); |
| argParser.addOption( |
| kVersionOverrideOption, |
| help: 'Explicitly set the desired version. This should only be used if ' |
| 'the version computed by the tool is not correct.', |
| ); |
| argParser.addOption( |
| kGithubUsernameOption, |
| help: 'Github username', |
| ); |
| } |
| |
| final Checkouts checkouts; |
| |
| final String conductorVersion; |
| final FileSystem fileSystem; |
| final Platform platform; |
| final ProcessManager processManager; |
| final Stdio stdio; |
| |
| @override |
| String get name => 'start'; |
| |
| @override |
| String get description => 'Initialize a new Flutter release.'; |
| |
| @override |
| Future<void> run() async { |
| final ArgResults argumentResults = argResults!; |
| if (!platform.isMacOS && !platform.isLinux) { |
| throw ConductorException( |
| 'Error! This tool is only supported on macOS and Linux', |
| ); |
| } |
| |
| final String frameworkUpstream = getValueFromEnvOrArgs( |
| kFrameworkUpstreamOption, |
| argumentResults, |
| platform.environment, |
| )!; |
| final String githubUsername = getValueFromEnvOrArgs( |
| kGithubUsernameOption, |
| argumentResults, |
| platform.environment, |
| )!; |
| final String frameworkMirror = |
| 'git@github.com:$githubUsername/flutter.git'; |
| final String engineUpstream = getValueFromEnvOrArgs( |
| kEngineUpstreamOption, |
| argumentResults, |
| platform.environment, |
| )!; |
| final String engineMirror = 'git@github.com:$githubUsername/engine.git'; |
| final String candidateBranch = getValueFromEnvOrArgs( |
| kCandidateOption, |
| argumentResults, |
| platform.environment, |
| )!; |
| final String releaseChannel = getValueFromEnvOrArgs( |
| kReleaseOption, |
| argumentResults, |
| platform.environment, |
| )!; |
| final String? dartRevision = getValueFromEnvOrArgs( |
| kDartRevisionOption, |
| argumentResults, |
| platform.environment, |
| allowNull: true, |
| ); |
| final bool force = getBoolFromEnvOrArgs( |
| kForceFlag, |
| argumentResults, |
| platform.environment, |
| ); |
| final File stateFile = checkouts.fileSystem.file( |
| getValueFromEnvOrArgs( |
| kStateOption, argumentResults, platform.environment), |
| ); |
| final String? versionOverrideString = getValueFromEnvOrArgs( |
| kVersionOverrideOption, |
| argumentResults, |
| platform.environment, |
| allowNull: true, |
| ); |
| Version? versionOverride; |
| if (versionOverrideString != null) { |
| versionOverride = Version.fromString(versionOverrideString); |
| } |
| |
| final StartContext context = StartContext( |
| candidateBranch: candidateBranch, |
| checkouts: checkouts, |
| dartRevision: dartRevision, |
| engineMirror: engineMirror, |
| engineUpstream: engineUpstream, |
| conductorVersion: conductorVersion, |
| frameworkMirror: frameworkMirror, |
| frameworkUpstream: frameworkUpstream, |
| processManager: processManager, |
| releaseChannel: releaseChannel, |
| stateFile: stateFile, |
| force: force, |
| versionOverride: versionOverride, |
| githubUsername: githubUsername, |
| ); |
| return context.run(); |
| } |
| } |
| |
| /// Context for starting a new release. |
| /// |
| /// This is a frontend-agnostic implementation. |
| class StartContext extends Context { |
| StartContext({ |
| required this.candidateBranch, |
| required this.dartRevision, |
| required this.engineMirror, |
| required this.engineUpstream, |
| required this.frameworkMirror, |
| required this.frameworkUpstream, |
| required this.conductorVersion, |
| required this.processManager, |
| required this.releaseChannel, |
| required this.githubUsername, |
| required super.checkouts, |
| required super.stateFile, |
| this.force = false, |
| this.versionOverride, |
| }) : git = Git(processManager), |
| engine = EngineRepository( |
| checkouts, |
| initialRef: 'upstream/$candidateBranch', |
| upstreamRemote: Remote( |
| name: RemoteName.upstream, |
| url: engineUpstream, |
| ), |
| mirrorRemote: Remote( |
| name: RemoteName.mirror, |
| url: engineMirror, |
| ), |
| ), |
| framework = FrameworkRepository( |
| checkouts, |
| initialRef: 'upstream/$candidateBranch', |
| upstreamRemote: Remote( |
| name: RemoteName.upstream, |
| url: frameworkUpstream, |
| ), |
| mirrorRemote: Remote( |
| name: RemoteName.mirror, |
| url: frameworkMirror, |
| ), |
| ); |
| |
| final String candidateBranch; |
| final String? dartRevision; |
| final String engineMirror; |
| final String engineUpstream; |
| final String frameworkMirror; |
| final String frameworkUpstream; |
| final String conductorVersion; |
| final Git git; |
| final ProcessManager processManager; |
| final String releaseChannel; |
| final Version? versionOverride; |
| final String githubUsername; |
| |
| /// If validations should be overridden. |
| final bool force; |
| |
| final EngineRepository engine; |
| final FrameworkRepository framework; |
| |
| /// Determine which part of the version to increment in the next release. |
| /// |
| /// If [atBranchPoint] is true, then this is a [ReleaseType.BETA_INITIAL]. |
| @visibleForTesting |
| ReleaseType computeReleaseType(Version lastVersion, bool atBranchPoint) { |
| if (atBranchPoint) { |
| return ReleaseType.BETA_INITIAL; |
| } |
| |
| if (releaseChannel == 'stable') { |
| if (lastVersion.type == VersionType.stable) { |
| return ReleaseType.STABLE_HOTFIX; |
| } else { |
| return ReleaseType.STABLE_INITIAL; |
| } |
| } |
| |
| return ReleaseType.BETA_HOTFIX; |
| } |
| |
| Future<void> run() async { |
| if (stateFile.existsSync()) { |
| throw ConductorException( |
| 'Error! A persistent state file already found at ${stateFile.path}.\n\n' |
| 'Run `conductor clean` to cancel a previous release.'); |
| } |
| if (!releaseCandidateBranchRegex.hasMatch(candidateBranch)) { |
| throw ConductorException( |
| 'Invalid release candidate branch "$candidateBranch". Text should ' |
| 'match the regex pattern /${releaseCandidateBranchRegex.pattern}/.', |
| ); |
| } |
| |
| final Int64 unixDate = Int64(DateTime.now().millisecondsSinceEpoch); |
| final pb.ConductorState state = pb.ConductorState(); |
| |
| state.releaseChannel = releaseChannel; |
| state.createdDate = unixDate; |
| state.lastUpdatedDate = unixDate; |
| |
| // Create a new branch so that we don't accidentally push to upstream |
| // candidateBranch. |
| final String workingBranchName = 'cherrypicks-$candidateBranch'; |
| await engine.newBranch(workingBranchName); |
| |
| if (dartRevision != null && dartRevision!.isNotEmpty) { |
| await engine.updateDartRevision(dartRevision!); |
| await engine.commit('Update Dart SDK to $dartRevision', addFirst: true); |
| } |
| |
| final String engineHead = await engine.reverseParse('HEAD'); |
| state.engine = pb.Repository( |
| candidateBranch: candidateBranch, |
| workingBranch: workingBranchName, |
| startingGitHead: engineHead, |
| currentGitHead: engineHead, |
| checkoutPath: (await engine.checkoutDirectory).path, |
| dartRevision: dartRevision, |
| upstream: pb.Remote(name: 'upstream', url: engine.upstreamRemote.url), |
| mirror: pb.Remote(name: 'mirror', url: engine.mirrorRemote!.url), |
| ); |
| |
| await framework.newBranch(workingBranchName); |
| |
| // Get framework version |
| final Version lastVersion = Version.fromString(await framework.getFullTag( |
| framework.upstreamRemote.name, |
| candidateBranch, |
| exact: false, |
| )); |
| |
| final String frameworkHead = await framework.reverseParse('HEAD'); |
| final String branchPoint = await framework.branchPoint( |
| '${framework.upstreamRemote.name}/$candidateBranch', |
| '${framework.upstreamRemote.name}/${FrameworkRepository.defaultBranch}', |
| ); |
| final bool atBranchPoint = branchPoint == frameworkHead; |
| |
| final ReleaseType releaseType = |
| computeReleaseType(lastVersion, atBranchPoint); |
| state.releaseType = releaseType; |
| |
| try { |
| lastVersion.ensureValid(candidateBranch, releaseType); |
| } on ConductorException catch (e) { |
| // Let the user know, but resume execution |
| stdio.printError(e.message); |
| } |
| |
| Version nextVersion; |
| if (versionOverride != null) { |
| nextVersion = versionOverride!; |
| } else { |
| nextVersion = calculateNextVersion(lastVersion, releaseType); |
| nextVersion = await ensureBranchPointTagged( |
| branchPoint: branchPoint, |
| requestedVersion: nextVersion, |
| framework: framework, |
| ); |
| } |
| |
| state.releaseVersion = nextVersion.toString(); |
| |
| state.framework = pb.Repository( |
| candidateBranch: candidateBranch, |
| workingBranch: workingBranchName, |
| startingGitHead: frameworkHead, |
| currentGitHead: frameworkHead, |
| checkoutPath: (await framework.checkoutDirectory).path, |
| upstream: pb.Remote(name: 'upstream', url: framework.upstreamRemote.url), |
| mirror: pb.Remote(name: 'mirror', url: framework.mirrorRemote!.url), |
| ); |
| |
| state.currentPhase = ReleasePhase.APPLY_ENGINE_CHERRYPICKS; |
| |
| state.conductorVersion = conductorVersion; |
| |
| stdio.printTrace('Writing state to file ${stateFile.path}...'); |
| |
| updateState(state, stdio.logs); |
| |
| stdio.printStatus(state_import.presentState(state)); |
| } |
| |
| /// Determine this release's version number from the [lastVersion] and the [incrementLetter]. |
| Version calculateNextVersion(Version lastVersion, ReleaseType releaseType) { |
| late final Version nextVersion; |
| switch (releaseType) { |
| case ReleaseType.STABLE_INITIAL: |
| nextVersion = Version( |
| x: lastVersion.x, |
| y: lastVersion.y, |
| z: 0, |
| type: VersionType.stable, |
| ); |
| break; |
| case ReleaseType.STABLE_HOTFIX: |
| nextVersion = Version.increment(lastVersion, 'z'); |
| break; |
| case ReleaseType.BETA_INITIAL: |
| nextVersion = Version.fromCandidateBranch(candidateBranch); |
| break; |
| case ReleaseType.BETA_HOTFIX: |
| nextVersion = Version.increment(lastVersion, 'n'); |
| break; |
| } |
| return nextVersion; |
| } |
| |
| /// Ensures the branch point [candidateBranch] and `master` has a version tag. |
| /// |
| /// This is necessary for version reporting for users on the `master` channel |
| /// to be correct. |
| Future<Version> ensureBranchPointTagged({ |
| required Version requestedVersion, |
| required String branchPoint, |
| required FrameworkRepository framework, |
| }) async { |
| if (await framework.isCommitTagged(branchPoint)) { |
| // The branch point is tagged, no work to be done |
| return requestedVersion; |
| } |
| if (requestedVersion.n != 0) { |
| stdio.printError( |
| 'Tried to tag the branch point, however the target version is ' |
| '$requestedVersion, which does not have n == 0!', |
| ); |
| return requestedVersion; |
| } |
| |
| final bool response = await prompt( |
| 'About to tag the release candidate branch branchpoint of $branchPoint ' |
| 'as $requestedVersion and push it to ${framework.upstreamRemote.url}. ' |
| 'Is this correct?', |
| ); |
| |
| if (!response) { |
| throw ConductorException('Aborting command.'); |
| } |
| |
| stdio.printStatus( |
| 'Applying the tag $requestedVersion at the branch point $branchPoint'); |
| |
| await framework.tag( |
| branchPoint, |
| requestedVersion.toString(), |
| frameworkUpstream, |
| ); |
| final Version nextVersion = Version.increment(requestedVersion, 'n'); |
| stdio.printStatus('The actual release will be version $nextVersion.'); |
| return nextVersion; |
| } |
| } |