| // 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. |
| |
| // Rolls the dev channel. |
| // Only tested on Linux. |
| // |
| // See: https://github.com/flutter/flutter/wiki/Release-process |
| |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| import 'package:meta/meta.dart'; |
| |
| const String kIncrement = 'increment'; |
| const String kX = 'x'; |
| const String kY = 'y'; |
| const String kZ = 'z'; |
| const String kCommit = 'commit'; |
| const String kOrigin = 'origin'; |
| const String kJustPrint = 'just-print'; |
| const String kYes = 'yes'; |
| const String kHelp = 'help'; |
| const String kForce = 'force'; |
| const String kSkipTagging = 'skip-tagging'; |
| |
| const String kUpstreamRemote = 'git@github.com:flutter/flutter.git'; |
| |
| void main(List<String> args) { |
| final ArgParser argParser = ArgParser(allowTrailingOptions: false); |
| |
| ArgResults argResults; |
| try { |
| argResults = parseArguments(argParser, args); |
| } on ArgParserException catch (error) { |
| print(error.message); |
| print(argParser.usage); |
| exit(1); |
| } |
| |
| try { |
| run( |
| usage: argParser.usage, |
| argResults: argResults, |
| git: const Git(), |
| ); |
| } on Exception catch (e) { |
| print(e.toString()); |
| exit(1); |
| } |
| } |
| |
| /// Main script execution. |
| /// |
| /// Returns true if publishing was successful, else false. |
| bool run({ |
| @required String usage, |
| @required ArgResults argResults, |
| @required Git git, |
| }) { |
| final String level = argResults[kIncrement] as String; |
| final String commit = argResults[kCommit] as String; |
| final String origin = argResults[kOrigin] as String; |
| final bool justPrint = argResults[kJustPrint] as bool; |
| final bool autoApprove = argResults[kYes] as bool; |
| final bool help = argResults[kHelp] as bool; |
| final bool force = argResults[kForce] as bool; |
| final bool skipTagging = argResults[kSkipTagging] as bool; |
| |
| if (help || level == null || commit == null) { |
| print( |
| 'roll_dev.dart --increment=level --commit=hash • update the version tags ' |
| 'and roll a new dev build.\n$usage' |
| ); |
| return false; |
| } |
| |
| final String remote = git.getOutput( |
| 'remote get-url $origin', |
| 'check whether this is a flutter checkout', |
| ); |
| if (remote != kUpstreamRemote) { |
| throw Exception( |
| 'The remote named $origin is set to $remote, when $kUpstreamRemote was ' |
| 'expected.\nFor more details see: ' |
| 'https://github.com/flutter/flutter/wiki/Release-process' |
| ); |
| } |
| |
| if (git.getOutput('status --porcelain', 'check status of your local checkout') != '') { |
| throw Exception( |
| 'Your git repository is not clean. Try running "git clean -fd". Warning, ' |
| 'this will delete files! Run with -n to find out which ones.' |
| ); |
| } |
| |
| git.run('fetch $origin', 'fetch $origin'); |
| |
| final String lastVersion = getFullTag(git, origin); |
| |
| final String version = skipTagging |
| ? lastVersion |
| : incrementLevel(lastVersion, level); |
| |
| if (git.getOutput( |
| 'rev-parse $lastVersion', |
| 'check if commit is already on dev', |
| ).contains(commit.trim())) { |
| throw Exception('Commit $commit is already on the dev branch as $lastVersion.'); |
| } |
| |
| if (justPrint) { |
| print(version); |
| return false; |
| } |
| |
| if (skipTagging) { |
| git.run( |
| 'describe --exact-match --tags $commit', |
| 'verify $commit is already tagged. You can only use the flag ' |
| '`$kSkipTagging` if the commit has already been tagged.' |
| ); |
| } |
| |
| if (!force) { |
| git.run( |
| 'merge-base --is-ancestor $lastVersion $commit', |
| 'verify $lastVersion is a direct ancestor of $commit. The flag `$kForce`' |
| 'is required to force push a new release past a cherry-pick', |
| ); |
| } |
| |
| git.run('reset $commit --hard', 'reset to the release commit'); |
| |
| final String hash = git.getOutput('rev-parse HEAD', 'Get git hash for $commit'); |
| |
| // PROMPT |
| |
| if (autoApprove) { |
| print('Publishing Flutter $version (${hash.substring(0, 10)}) to the "dev" channel.'); |
| } else { |
| print('Your tree is ready to publish Flutter $version (${hash.substring(0, 10)}) ' |
| 'to the "dev" channel.'); |
| stdout.write('Are you? [yes/no] '); |
| if (stdin.readLineSync() != 'yes') { |
| print('The dev roll has been aborted.'); |
| return false; |
| } |
| } |
| |
| if (!skipTagging) { |
| git.run('tag $version', 'tag the commit with the version label'); |
| git.run('push $origin $version', 'publish the version'); |
| } |
| git.run( |
| 'push ${force ? "--force " : ""}$origin HEAD:dev', |
| 'land the new version on the "dev" branch', |
| ); |
| print('Flutter version $version has been rolled to the "dev" channel!'); |
| return true; |
| } |
| |
| ArgResults parseArguments(ArgParser argParser, List<String> args) { |
| argParser.addOption( |
| kIncrement, |
| help: 'Specifies which part of the x.y.z version number to increment. Required.', |
| valueHelp: 'level', |
| allowed: <String>[kX, kY, kZ], |
| allowedHelp: <String, String>{ |
| kX: 'Indicates a major development, e.g. typically changed after a big press event.', |
| kY: 'Indicates a minor development, e.g. typically changed after a beta release.', |
| kZ: 'Indicates the least notable level of change. You normally want this.', |
| }, |
| ); |
| argParser.addOption( |
| kCommit, |
| help: 'Specifies which git commit to roll to the dev branch. Required.', |
| valueHelp: 'hash', |
| defaultsTo: null, // This option is required |
| ); |
| argParser.addOption( |
| kOrigin, |
| help: 'Specifies the name of the upstream repository', |
| valueHelp: 'repository', |
| defaultsTo: 'upstream', |
| ); |
| argParser.addFlag( |
| kForce, |
| abbr: 'f', |
| help: 'Force push. Necessary when the previous release had cherry-picks.', |
| negatable: false, |
| ); |
| argParser.addFlag( |
| kJustPrint, |
| negatable: false, |
| help: |
| "Don't actually roll the dev channel; " |
| 'just print the would-be version and quit.', |
| ); |
| argParser.addFlag( |
| kSkipTagging, |
| negatable: false, |
| help: 'Do not create tag and push to remote, only update release branch. ' |
| 'For recovering when the script fails trying to git push to the release branch.' |
| ); |
| argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.'); |
| argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.', hide: true); |
| |
| return argParser.parse(args); |
| } |
| |
| /// Obtain the version tag of the previous dev release. |
| String getFullTag(Git git, String remote) { |
| const String glob = '*.*.*-*.*.pre'; |
| // describe the latest dev release |
| final String ref = 'refs/remotes/$remote/dev'; |
| return git.getOutput( |
| 'describe --match $glob --exact-match --tags $ref', |
| 'obtain last released version number', |
| ); |
| } |
| |
| Match parseFullTag(String version) { |
| // of the form: x.y.z-m.n.pre |
| final RegExp versionPattern = RegExp( |
| r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre$'); |
| return versionPattern.matchAsPrefix(version); |
| } |
| |
| String getVersionFromParts(List<int> parts) { |
| // where parts correspond to [x, y, z, m, n] from tag |
| assert(parts.length == 5); |
| final StringBuffer buf = StringBuffer() |
| // take x, y, and z |
| ..write(parts.take(3).join('.')) |
| ..write('-') |
| // skip x, y, and z, take m and n |
| ..write(parts.skip(3).take(2).join('.')) |
| ..write('.pre'); |
| // return a string that looks like: '1.2.3-4.5.pre' |
| return buf.toString(); |
| } |
| |
| /// A wrapper around git process calls that can be mocked for unit testing. |
| class Git { |
| const Git(); |
| |
| String getOutput(String command, String explanation) { |
| final ProcessResult result = _run(command); |
| if ((result.stderr as String).isEmpty && result.exitCode == 0) |
| return (result.stdout as String).trim(); |
| _reportFailureAndExit(result, explanation); |
| return null; // for the analyzer's sake |
| } |
| |
| void run(String command, String explanation) { |
| final ProcessResult result = _run(command); |
| if (result.exitCode != 0) |
| _reportFailureAndExit(result, explanation); |
| } |
| |
| ProcessResult _run(String command) { |
| return Process.runSync('git', command.split(' ')); |
| } |
| |
| void _reportFailureAndExit(ProcessResult result, String explanation) { |
| final StringBuffer message = StringBuffer(); |
| if (result.exitCode != 0) { |
| message.writeln('Failed to $explanation. Git exited with error code ${result.exitCode}.'); |
| } else { |
| message.writeln('Failed to $explanation.'); |
| } |
| if ((result.stdout as String).isNotEmpty) |
| message.writeln('stdout from git:\n${result.stdout}\n'); |
| if ((result.stderr as String).isNotEmpty) |
| message.writeln('stderr from git:\n${result.stderr}\n'); |
| throw Exception(message); |
| } |
| } |
| |
| /// Return a copy of the [version] with [level] incremented by one. |
| String incrementLevel(String version, String level) { |
| final Match match = parseFullTag(version); |
| if (match == null) { |
| String errorMessage; |
| if (version.isEmpty) { |
| errorMessage = 'Could not determine the version for this build.'; |
| } else { |
| errorMessage = 'Git reported the latest version as "$version", which ' |
| 'does not fit the expected pattern.'; |
| } |
| throw Exception(errorMessage); |
| } |
| |
| final List<int> parts = match.groups(<int>[1, 2, 3, 4, 5]).map<int>(int.parse).toList(); |
| |
| switch (level) { |
| case kX: |
| parts[0] += 1; |
| parts[1] = 0; |
| parts[2] = 0; |
| parts[3] = 0; |
| parts[4] = 0; |
| break; |
| case kY: |
| parts[1] += 1; |
| parts[2] = 0; |
| parts[3] = 0; |
| parts[4] = 0; |
| break; |
| case kZ: |
| parts[2] = 0; |
| parts[3] += 1; |
| parts[4] = 0; |
| break; |
| default: |
| throw Exception('Unknown increment level. The valid values are "$kX", "$kY", and "$kZ".'); |
| } |
| return getVersionFromParts(parts); |
| } |