1// Copyright 2017 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// Rolls the dev channel. 6// Only tested on Linux. 7// 8// See: https://github.com/flutter/flutter/wiki/Release-process 9 10import 'dart:io'; 11 12import 'package:args/args.dart'; 13 14const String kIncrement = 'increment'; 15const String kX = 'x'; 16const String kY = 'y'; 17const String kZ = 'z'; 18const String kCommit = 'commit'; 19const String kOrigin = 'origin'; 20const String kJustPrint = 'just-print'; 21const String kYes = 'yes'; 22const String kHelp = 'help'; 23 24const String kUpstreamRemote = 'git@github.com:flutter/flutter.git'; 25 26void main(List<String> args) { 27 final ArgParser argParser = ArgParser(allowTrailingOptions: false); 28 argParser.addOption( 29 kIncrement, 30 help: 'Specifies which part of the x.y.z version number to increment. Required.', 31 valueHelp: 'level', 32 allowed: <String>[kX, kY, kZ], 33 allowedHelp: <String, String>{ 34 kX: 'Indicates a major development, e.g. typically changed after a big press event.', 35 kY: 'Indicates a minor development, e.g. typically changed after a beta release.', 36 kZ: 'Indicates the least notable level of change. You normally want this.', 37 }, 38 ); 39 argParser.addOption( 40 kCommit, 41 help: 'Specifies which git commit to roll to the dev branch.', 42 valueHelp: 'hash', 43 defaultsTo: 'upstream/master', 44 ); 45 argParser.addOption( 46 kOrigin, 47 help: 'Specifies the name of the upstream repository', 48 valueHelp: 'repository', 49 defaultsTo: 'upstream', 50 ); 51 argParser.addFlag( 52 kJustPrint, 53 negatable: false, 54 help: 55 'Don\'t actually roll the dev channel; ' 56 'just print the would-be version and quit.', 57 ); 58 argParser.addFlag(kYes, negatable: false, abbr: 'y', help: 'Skip the confirmation prompt.'); 59 argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.', hide: true); 60 ArgResults argResults; 61 try { 62 argResults = argParser.parse(args); 63 } on ArgParserException catch (error) { 64 print(error.message); 65 print(argParser.usage); 66 exit(1); 67 } 68 69 final String level = argResults[kIncrement]; 70 final String commit = argResults[kCommit]; 71 final String origin = argResults[kOrigin]; 72 final bool justPrint = argResults[kJustPrint]; 73 final bool autoApprove = argResults[kYes]; 74 final bool help = argResults[kHelp]; 75 76 if (help || level == null) { 77 print('roll_dev.dart --increment=level --commit=hash • update the version tags and roll a new dev build.\n'); 78 print(argParser.usage); 79 exit(0); 80 } 81 82 if (getGitOutput('remote get-url $origin', 'check whether this is a flutter checkout') != kUpstreamRemote) { 83 print('The current directory is not a Flutter repository checkout with a correctly configured upstream remote.'); 84 print('For more details see: https://github.com/flutter/flutter/wiki/Release-process'); 85 exit(1); 86 } 87 88 runGit('checkout master', 'switch to master branch'); 89 90 if (getGitOutput('status --porcelain', 'check status of your local checkout') != '') { 91 print('Your git repository is not clean. Try running "git clean -fd". Warning, this '); 92 print('will delete files! Run with -n to find out which ones.'); 93 exit(1); 94 } 95 96 runGit('fetch $origin', 'fetch $origin'); 97 runGit('reset $commit --hard', 'check out master branch'); 98 99 String version = getFullTag(); 100 final Match match = parseFullTag(version); 101 if (match == null) { 102 print('Could not determine the version for this build.'); 103 if (version.isNotEmpty) 104 print('Git reported the latest version as "$version", which does not fit the expected pattern.'); 105 exit(1); 106 } 107 108 final List<int> parts = match.groups(<int>[1, 2, 3]).map<int>(int.parse).toList(); 109 110 if (match.group(4) == '0') { 111 print('This commit has already been released, as version ${parts.join(".")}.'); 112 exit(0); 113 } 114 115 switch (level) { 116 case kX: 117 parts[0] += 1; 118 parts[1] = 0; 119 parts[2] = 0; 120 break; 121 case kY: 122 parts[1] += 1; 123 parts[2] = 0; 124 break; 125 case kZ: 126 parts[2] += 1; 127 break; 128 default: 129 print('Unknown increment level. The valid values are "$kX", "$kY", and "$kZ".'); 130 exit(1); 131 } 132 version = parts.join('.'); 133 134 if (justPrint) { 135 print(version); 136 exit(0); 137 } 138 139 final String hash = getGitOutput('rev-parse HEAD', 'Get git hash for $commit'); 140 141 runGit('tag v$version', 'tag the commit with the version label'); 142 143 // PROMPT 144 145 if (autoApprove) { 146 print('Publishing Flutter $version (${hash.substring(0, 10)}) to the "dev" channel.'); 147 } else { 148 print('Your tree is ready to publish Flutter $version (${hash.substring(0, 10)}) ' 149 'to the "dev" channel.'); 150 stdout.write('Are you? [yes/no] '); 151 if (stdin.readLineSync() != 'yes') { 152 runGit('tag -d v$version', 'remove the tag you did not want to publish'); 153 print('The dev roll has been aborted.'); 154 exit(0); 155 } 156 } 157 158 runGit('push $origin v$version', 'publish the version'); 159 runGit('push $origin HEAD:dev', 'land the new version on the "dev" branch'); 160 print('Flutter version $version has been rolled to the "dev" channel!'); 161} 162 163String getFullTag() { 164 return getGitOutput( 165 'describe --match v*.*.* --first-parent --long --tags', 166 'obtain last released version number', 167 ); 168} 169 170Match parseFullTag(String version) { 171 final RegExp versionPattern = RegExp('^v([0-9]+)\.([0-9]+)\.([0-9]+)-([0-9]+)-g([a-f0-9]+)\$'); 172 return versionPattern.matchAsPrefix(version); 173} 174 175String getGitOutput(String command, String explanation) { 176 final ProcessResult result = _runGit(command); 177 if (result.stderr.isEmpty && result.exitCode == 0) 178 return result.stdout.trim(); 179 _reportGitFailureAndExit(result, explanation); 180 return null; // for the analyzer's sake 181} 182 183void runGit(String command, String explanation) { 184 final ProcessResult result = _runGit(command); 185 if (result.exitCode != 0) 186 _reportGitFailureAndExit(result, explanation); 187} 188 189ProcessResult _runGit(String command) { 190 return Process.runSync('git', command.split(' ')); 191} 192 193void _reportGitFailureAndExit(ProcessResult result, String explanation) { 194 if (result.exitCode != 0) { 195 print('Failed to $explanation. Git exited with error code ${result.exitCode}.'); 196 } else { 197 print('Failed to $explanation.'); 198 } 199 if (result.stdout.isNotEmpty) 200 print('stdout from git:\n${result.stdout}\n'); 201 if (result.stderr.isNotEmpty) 202 print('stderr from git:\n${result.stderr}\n'); 203 exit(1); 204} 205