Unverified Commit 77243762 authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

[flutter_conductor] update dev/tools with release tool (#69791)

parent 1f210275
#!/usr/bin/env bash
set -euo pipefail
# Needed because if it is set, cd may print the path it changed to.
unset CDPATH
# On Mac OS, readlink -f doesn't work, so follow_links traverses the path one
# link at a time, and then cds into the link destination and find out where it
# ends up.
#
# The returned filesystem path must be a format usable by Dart's URI parser,
# since the Dart command line tool treats its argument as a file URI, not a
# filename. For instance, multiple consecutive slashes should be reduced to a
# single slash, since double-slashes indicate a URI "authority", and these are
# supposed to be filenames. There is an edge case where this will return
# multiple slashes: when the input resolves to the root directory. However, if
# that were the case, we wouldn't be running this shell, so we don't do anything
# about it.
#
# The function is enclosed in a subshell to avoid changing the working directory
# of the caller.
function follow_links() (
cd -P "$(dirname -- "$1")"
file="$PWD/$(basename -- "$1")"
while [[ -h "$file" ]]; do
cd -P "$(dirname -- "$file")"
file="$(readlink -- "$file")"
cd -P "$(dirname -- "$file")"
file="$PWD/$(basename -- "$file")"
done
echo "$file"
)
PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")"
BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
DART_BIN="$BIN_DIR/../../../bin/dart"
"$DART_BIN" --enable-asserts "$BIN_DIR/conductor.dart" "$@"
// 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' as io;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'package:dev_tools/repository.dart';
import 'package:dev_tools/roll_dev.dart';
import 'package:dev_tools/stdio.dart';
void main(List<String> args) {
const FileSystem fileSystem = LocalFileSystem();
const ProcessManager processManager = LocalProcessManager();
const Platform platform = LocalPlatform();
final Stdio stdio = VerboseStdio(
stdout: io.stdout,
stderr: io.stderr,
stdin: io.stdin,
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
);
final CommandRunner<void> runner = CommandRunner<void>(
'conductor',
'A tool for coordinating Flutter releases.',
usageLineLength: 80,
);
<Command<void>>[
RollDev(
fileSystem: fileSystem,
platform: platform,
repository: checkouts.addRepo(
fileSystem: fileSystem,
platform: platform,
repoType: RepositoryType.framework,
stdio: stdio,
),
stdio: stdio,
),
].forEach(runner.addCommand);
if (!assertsEnabled()) {
stdio.printError('The conductor tool must be run with --enable-asserts.');
io.exit(1);
}
try {
runner.run(args);
} on Exception catch (e) {
stdio.printError(e.toString());
io.exit(1);
}
}
bool assertsEnabled() {
// Verify asserts enabled
bool assertsEnabled = false;
assert(() {
assertsEnabled = true;
return true;
}());
return assertsEnabled;
}
// 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 'dart:io';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import './globals.dart';
/// A wrapper around git process calls that can be mocked for unit testing.
class Git {
Git(this.processManager) : assert(processManager != null);
final ProcessManager processManager;
String getOutput(
List<String> args,
String explanation, {
@required String workingDirectory,
}) {
final ProcessResult result = _run(args, workingDirectory);
if (result.exitCode == 0) {
return stdoutToString(result.stdout);
}
_reportFailureAndExit(args, workingDirectory, result, explanation);
return null; // for the analyzer's sake
}
int run(
List<String> args,
String explanation, {
bool allowNonZeroExitCode = false,
@required String workingDirectory,
}) {
final ProcessResult result = _run(args, workingDirectory);
if (result.exitCode != 0 && !allowNonZeroExitCode) {
_reportFailureAndExit(args, workingDirectory, result, explanation);
}
return result.exitCode;
}
ProcessResult _run(List<String> args, String workingDirectory) {
return processManager.runSync(
<String>['git', ...args],
workingDirectory: workingDirectory,
);
}
void _reportFailureAndExit(
List<String> args,
String workingDirectory,
ProcessResult result,
String explanation,
) {
final StringBuffer message = StringBuffer();
if (result.exitCode != 0) {
message.writeln(
'Command "git ${args.join(' ')}" failed in directory "$workingDirectory" to '
'$explanation. Git exited with error code ${result.exitCode}.',
);
} else {
message.writeln('Command "git ${args.join(' ')}" 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);
}
}
// 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.
const String kIncrement = 'increment';
const String kCommit = 'commit';
const String kRemoteName = 'remote';
const String kJustPrint = 'just-print';
const String kYes = 'yes';
const String kForce = 'force';
const String kSkipTagging = 'skip-tagging';
const String kUpstreamRemote = 'https://github.com/flutter/flutter.git';
const List<String> kReleaseChannels = <String>[
'stable',
'beta',
'dev',
'master',
];
/// Cast a dynamic to String and trim.
String stdoutToString(dynamic input) {
final String str = input as String;
return str.trim();
}
This diff is collapsed.
This diff is collapsed.
// 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 'dart:io';
import 'package:meta/meta.dart';
abstract class Stdio {
/// Error/warning messages printed to STDERR.
void printError(String message);
/// Ordinary STDOUT messages.
void printStatus(String message);
/// Debug messages that are only printed in verbose mode.
void printTrace(String message);
/// Write string to STDOUT without trailing newline.
void write(String message);
/// Read a line of text from STDIN.
String readLineSync();
}
/// A logger that will print out trace messages.
class VerboseStdio extends Stdio {
VerboseStdio({
@required this.stdout,
@required this.stderr,
@required this.stdin,
}) : assert(stdout != null), assert(stderr != null), assert(stdin != null);
final Stdout stdout;
final Stdout stderr;
final Stdin stdin;
@override
void printError(String message) {
stderr.writeln(message);
}
@override
void printStatus(String message) {
stdout.writeln(message);
}
@override
void printTrace(String message) {
stdout.writeln(message);
}
@override
void write(String message) {
stdout.write(message);
}
@override
String readLineSync() {
return stdin.readLineSync();
}
}
// 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:meta/meta.dart';
/// Possible string formats that `flutter --version` can return.
enum VersionType {
/// A stable flutter release.
///
/// Example: '1.2.3'
stable,
/// A pre-stable flutter release.
///
/// Example: '1.2.3-4.5.pre'
development,
/// A master channel flutter version.
///
/// Example: '1.2.3-4.0.pre.10'
///
/// The last number is the number of commits past the last tagged version.
latest,
}
final Map<VersionType, RegExp> versionPatterns = <VersionType, RegExp>{
VersionType.stable: RegExp(r'^(\d+)\.(\d+)\.(\d+)$'),
VersionType.development: RegExp(r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre$'),
VersionType.latest: RegExp(r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre\.(\d+)$'),
};
class Version {
Version({
@required this.x,
@required this.y,
@required this.z,
this.m,
this.n,
this.commits,
@required this.type,
}) {
switch (type) {
case VersionType.stable:
assert(m == null);
assert(n == null);
assert(commits == null);
break;
case VersionType.development:
assert(m != null);
assert(n != null);
assert(commits == null);
break;
case VersionType.latest:
assert(m != null);
assert(n != null);
assert(commits != null);
break;
}
}
/// Create a new [Version] from a version string.
///
/// It is expected that [versionString] will be generated by
/// `flutter --version` and match one of `stablePattern`, `developmentPattern`
/// and `latestPattern`.
factory Version.fromString(String versionString) {
assert(versionString != null);
versionString = versionString.trim();
// stable tag
Match match = versionPatterns[VersionType.stable].firstMatch(versionString);
if (match != null) {
// parse stable
final List<int> parts =
match.groups(<int>[1, 2, 3]).map(int.parse).toList();
return Version(
x: parts[0],
y: parts[1],
z: parts[2],
type: VersionType.stable,
);
}
// development tag
match = versionPatterns[VersionType.development].firstMatch(versionString);
if (match != null) {
// parse development
final List<int> parts =
match.groups(<int>[1, 2, 3, 4, 5]).map(int.parse).toList();
return Version(
x: parts[0],
y: parts[1],
z: parts[2],
m: parts[3],
n: parts[4],
type: VersionType.development,
);
}
// latest tag
match = versionPatterns[VersionType.latest].firstMatch(versionString);
if (match != null) {
// parse latest
final List<int> parts =
match.groups(<int>[1, 2, 3, 4, 5, 6]).map(int.parse).toList();
return Version(
x: parts[0],
y: parts[1],
z: parts[2],
m: parts[3],
n: parts[4],
commits: parts[5],
type: VersionType.latest,
);
}
throw Exception('${versionString.trim()} cannot be parsed');
}
// Returns a new version with the given [increment] part incremented.
// NOTE new version must be of same type as previousVersion.
factory Version.increment(
Version previousVersion,
String increment, {
VersionType nextVersionType,
}) {
final int nextX = previousVersion.x;
int nextY = previousVersion.y;
int nextZ = previousVersion.z;
int nextM = previousVersion.m;
int nextN = previousVersion.n;
if (nextVersionType == null) {
if (previousVersion.type == VersionType.latest) {
nextVersionType = VersionType.development;
} else {
nextVersionType = previousVersion.type;
}
}
switch (increment) {
case 'x':
// This was probably a mistake.
throw Exception('Incrementing x is not supported by this tool.');
break;
case 'y':
// Dev release following a beta release.
nextY += 1;
nextZ = 0;
if (previousVersion.type != VersionType.stable) {
nextM = 0;
nextN = 0;
}
break;
case 'z':
// Hotfix to stable release.
assert(previousVersion.type == VersionType.stable);
nextZ += 1;
break;
case 'm':
// Regular dev release.
assert(previousVersion.type == VersionType.development);
assert(nextM != null);
nextM += 1;
nextN = 0;
break;
case 'n':
// Hotfix to internal roll.
nextN += 1;
break;
default:
throw Exception('Unknown increment level $increment.');
}
return Version(
x: nextX,
y: nextY,
z: nextZ,
m: nextM,
n: nextN,
type: nextVersionType,
);
}
/// Major version.
final int x;
/// Zero-indexed count of beta releases after a major release.
final int y;
/// Number of hotfix releases after a stable release.
final int z;
/// Zero-indexed count of dev releases after a beta release.
final int m;
/// Number of hotfixes required to make a dev release.
final int n;
/// Number of commits past last tagged dev release.
final int commits;
final VersionType type;
@override
String toString() {
switch (type) {
case VersionType.stable:
return '$x.$y.$z';
case VersionType.development:
return '$x.$y.$z-$m.$n.pre';
case VersionType.latest:
return '$x.$y.$z-$m.$n.pre.$commits';
}
return null; // For analyzer
}
}
......@@ -8,6 +8,8 @@ environment:
dependencies:
archive: 2.0.13
args: 1.6.0
flutter_tools:
path: '../../packages/flutter_tools'
http: 0.12.2
intl: 0.16.1
meta: 1.3.0-nullsafety.6
......
......@@ -7,6 +7,10 @@ import 'dart:io';
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
import 'package:test/test.dart' as test_package show TypeMatcher;
import 'package:dev_tools/stdio.dart';
import 'package:args/args.dart';
export 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
// Defines a 'package:test' shim.
......@@ -25,3 +29,111 @@ void tryToDelete(Directory directory) {
print('Failed to delete ${directory.path}: $error');
}
}
Matcher throwsExceptionWith(String messageSubString) {
return throwsA(
isA<Exception>().having(
(Exception e) => e.toString(),
'description',
contains(messageSubString),
),
);
}
class TestStdio implements Stdio {
TestStdio({
this.verbose = false,
List<String> stdin,
}) {
_stdin = stdin ?? <String>[];
}
final StringBuffer _error = StringBuffer();
String get error => _error.toString();
final StringBuffer _stdout = StringBuffer();
String get stdout => _stdout.toString();
final bool verbose;
List<String> _stdin;
@override
void printError(String message) {
_error.writeln(message);
}
@override
void printStatus(String message) {
_stdout.writeln(message);
}
@override
void printTrace(String message) {
if (verbose) {
_stdout.writeln(message);
}
}
@override
void write(String message) {
_stdout.write(message);
}
@override
String readLineSync() {
if (_stdin.isEmpty) {
throw Exception('Unexpected call to readLineSync!');
}
return _stdin.removeAt(0);
}
}
class FakeArgResults implements ArgResults {
FakeArgResults({
String level,
String commit,
String remote,
bool justPrint = false,
bool autoApprove = true, // so we don't have to mock stdin
bool help = false,
bool force = false,
bool skipTagging = false,
}) : _parsedArgs = <String, dynamic>{
'increment': level,
'commit': commit,
'remote': remote,
'just-print': justPrint,
'yes': autoApprove,
'help': help,
'force': force,
'skip-tagging': skipTagging,
};
@override
String name;
@override
ArgResults command;
@override
final List<String> rest = <String>[];
@override
List<String> arguments;
final Map<String, dynamic> _parsedArgs;
@override
Iterable<String> get options {
return null;
}
@override
dynamic operator [](String name) {
return _parsedArgs[name];
}
@override
bool wasParsed(String name) {
return null;
}
}
// 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:file/file.dart';
import 'package:file/local.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'package:dev_tools/roll_dev.dart' show rollDev;
import 'package:dev_tools/repository.dart';
import 'package:dev_tools/version.dart';
import './common.dart';
void main() {
group('roll-dev', () {
TestStdio stdio;
Platform platform;
ProcessManager processManager;
FileSystem fileSystem;
const String usageString = 'Usage: flutter conductor.';
Checkouts checkouts;
Repository frameworkUpstream;
Repository framework;
setUp(() {
platform = const LocalPlatform();
fileSystem = const LocalFileSystem();
processManager = const LocalProcessManager();
stdio = TestStdio(verbose: true);
checkouts = Checkouts(
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
);
frameworkUpstream = checkouts.addRepo(
repoType: RepositoryType.framework,
name: 'framework-upstream',
stdio: stdio,
platform: platform,
localUpstream: true,
fileSystem: fileSystem,
useExistingCheckout: false,
);
// This repository has [frameworkUpstream] set as its push/pull remote.
framework = frameworkUpstream.cloneRepository('test-framework');
});
test('increment m', () {
final Version initialVersion = framework.flutterVersion();
final String latestCommit = framework.authorEmptyCommit();
final FakeArgResults fakeArgResults = FakeArgResults(
level: 'm',
commit: latestCommit,
remote: 'origin',
);
expect(
rollDev(
usage: usageString,
argResults: fakeArgResults,
stdio: stdio,
fileSystem: fileSystem,
platform: platform,
repository: framework,
),
true,
);
expect(
stdio.stdout,
contains(RegExp(r'Publishing Flutter \d+\.\d+\.\d+-\d+\.\d+\.pre \(')),
);
final Version finalVersion = framework.flutterVersion();
expect(
initialVersion.toString() != finalVersion.toString(),
true,
reason: 'initialVersion = $initialVersion; finalVersion = $finalVersion',
);
expect(finalVersion.n, 0);
expect(finalVersion.commits, null);
});
test('increment y', () {
final Version initialVersion = framework.flutterVersion();
final String latestCommit = framework.authorEmptyCommit();
final FakeArgResults fakeArgResults = FakeArgResults(
level: 'y',
commit: latestCommit,
remote: 'origin',
);
expect(
rollDev(
usage: usageString,
argResults: fakeArgResults,
stdio: stdio,
fileSystem: fileSystem,
platform: platform,
repository: framework,
),
true,
);
expect(
stdio.stdout,
contains(RegExp(r'Publishing Flutter \d+\.\d+\.\d+-\d+\.\d+\.pre \(')),
);
final Version finalVersion = framework.flutterVersion();
expect(
initialVersion.toString() != finalVersion.toString(),
true,
reason: 'initialVersion = $initialVersion; finalVersion = $finalVersion',
);
expect(finalVersion.y, initialVersion.y + 1);
expect(finalVersion.z, 0);
expect(finalVersion.m, 0);
expect(finalVersion.n, 0);
expect(finalVersion.commits, null);
});
}, onPlatform: <String, dynamic>{
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
});
}
This diff is collapsed.
// 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:dev_tools/version.dart';
import './common.dart';
void main() {
group('Version.increment()', () {
test('throws exception on nonsensical `level`', () {
final List<String> levels = <String>['f', '0', 'xyz'];
for (final String level in levels) {
final Version version = Version.fromString('1.0.0-0.0.pre');
expect(
() => Version.increment(version, level).toString(),
throwsExceptionWith('Unknown increment level $level.'),
);
}
});
test('does not support incrementing x', () {
const String level = 'x';
final Version version = Version.fromString('1.0.0-0.0.pre');
expect(
() => Version.increment(version, level).toString(),
throwsExceptionWith(
'Incrementing $level is not supported by this tool'),
);
});
test('successfully increments y', () {
const String level = 'y';
Version version = Version.fromString('1.0.0-0.0.pre');
expect(Version.increment(version, level).toString(), '1.1.0-0.0.pre');
version = Version.fromString('10.20.0-40.50.pre');
expect(Version.increment(version, level).toString(), '10.21.0-0.0.pre');
version = Version.fromString('1.18.0-3.0.pre');
expect(Version.increment(version, level).toString(), '1.19.0-0.0.pre');
});
test('successfully increments z', () {
const String level = 'm';
Version version = Version.fromString('1.0.0-0.0.pre');
expect(Version.increment(version, level).toString(), '1.0.0-1.0.pre');
version = Version.fromString('10.20.0-40.50.pre');
expect(Version.increment(version, level).toString(), '10.20.0-41.0.pre');
version = Version.fromString('1.18.0-3.0.pre');
expect(Version.increment(version, level).toString(), '1.18.0-4.0.pre');
});
test('successfully increments m', () {
const String level = 'm';
Version version = Version.fromString('1.0.0-0.0.pre');
expect(Version.increment(version, level).toString(), '1.0.0-1.0.pre');
version = Version.fromString('10.20.0-40.50.pre');
expect(Version.increment(version, level).toString(), '10.20.0-41.0.pre');
version = Version.fromString('1.18.0-3.0.pre');
expect(Version.increment(version, level).toString(), '1.18.0-4.0.pre');
});
test('successfully increments n', () {
const String level = 'n';
Version version = Version.fromString('1.0.0-0.0.pre');
expect(Version.increment(version, level).toString(), '1.0.0-0.1.pre');
version = Version.fromString('10.20.0-40.50.pre');
expect(Version.increment(version, level).toString(), '10.20.0-40.51.pre');
version = Version.fromString('1.18.0-3.0.pre');
expect(Version.increment(version, level).toString(), '1.18.0-3.1.pre');
});
}, onPlatform: <String, dynamic>{
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment