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

Conductor add next (#84354)

parent 567586b4
......@@ -29,8 +29,8 @@ Releases are initialized with the `start` sub-command, like:
conductor start \
--candidate-branch=flutter-2.2-candidate.10 \
--release-channel=beta \
--framework-mirror=git@github.com:flutter-contributor/flutter.git \
--engine-mirror=git@github.com:flutter-contributor/engine.git \
--framework-mirror=git@github.com:username/flutter.git \
--engine-mirror=git@github.com:username/engine.git \
--engine-cherrypicks=72114dafe28c8700f1d5d629c6ae9d34172ba395 \
--framework-cherrypicks=a3e66b396746f6581b2b7efd1b0d0f0074215128,d8d853436206e86f416236b930e97779b143a100 \
--dart-revision=4511eb2a779a612d9d6b2012123575013e0aef12
......@@ -54,3 +54,51 @@ Upon successful completion of the release, the following command will remove the
persistent state file:
`conductor clean`
## Steps
Once the user has finished manual steps for each step, they proceed to the next
step with the command:
`conductor next`
### Apply Engine Cherrypicks
The tool will attempt to auto-apply all engine cherrypicks. However, any
cherrypicks that result in a merge conflict will be reverted and it is left to
the user to manually cherry-pick them (with the command `git cherry-pick
$REVISION`) and resolve the merge conflict in their checkout.
Once a PR is opened, the user must validate CI builds. If there are regressions
(or if the `licenses_check` fails, then
`//engine/ci/licenses_golden/licenses_third_party` must be updated to match the
output of the failing test), then the user must fix these tests in their local
checkout and push their changes again.
### Codesign Engine Binaries
The user must validate post-submit CI builds for their merged engine PR have
passed. A link to the web dashboard is available via `conductor status`. Once
the post-submit CI builds have all passed, the user must codesign engine
binaries for the **merged** engine commit.
### Apply Framework Cherrypicks
The tool will attempt to auto-apply all framework cherrypicks. However, any
cherrypicks that result in a merge conflict will be reverted and it is left to
the user to manually cherry-pick them (with the command `git cherry-pick
$REVISION`) and resolve the merge conflict in their checkout.
### Publish Version
This step will add a version git tag to the final Framework commit and push it
to the upstream repository.
### Publish Channel
This step will update the upstream release branch.
### Verify Release
For the final step, the user must manually verify that packaging builds have
finished successfully.
......@@ -13,6 +13,7 @@ import 'package:conductor/candidates.dart';
import 'package:conductor/clean.dart';
import 'package:conductor/codesign.dart';
import 'package:conductor/globals.dart';
import 'package:conductor/next.dart';
import 'package:conductor/repository.dart';
import 'package:conductor/roll_dev.dart';
import 'package:conductor/start.dart';
......@@ -74,6 +75,9 @@ Future<void> main(List<String> args) async {
checkouts: checkouts,
flutterRoot: localFlutterRoot,
),
NextCommand(
checkouts: checkouts,
),
].forEach(runner.addCommand);
if (!assertsEnabled()) {
......
......@@ -115,8 +115,7 @@ class CodesignCommand extends Command<void> {
revision = (processManager.runSync(
<String>['git', 'rev-parse', 'HEAD'],
workingDirectory: framework.checkoutDirectory.path,
).stdout as String)
.trim();
).stdout as String).trim();
assert(revision.isNotEmpty);
}
......@@ -291,7 +290,7 @@ class CodesignCommand extends Command<void> {
if (wrongEntitlementBinaries.isNotEmpty) {
stdio.printError(
'Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:');
wrongEntitlementBinaries.forEach(print);
wrongEntitlementBinaries.forEach(stdio.printError);
}
if (unexpectedBinaries.isNotEmpty) {
......
......@@ -20,6 +20,8 @@ const List<String> kReleaseChannels = <String>[
const String kReleaseDocumentationUrl = 'https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process';
const String kLuciPackagingConsoleLink = 'https://ci.chromium.org/p/flutter/g/packaging/console';
final RegExp releaseCandidateBranchRegex = RegExp(
r'flutter-(\d+)\.(\d+)-candidate\.(\d+)',
);
......
// 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:args/command_runner.dart';
import 'package:file/file.dart' show File;
import 'package:meta/meta.dart' show required, visibleForTesting;
import './globals.dart';
import './proto/conductor_state.pb.dart' as pb;
import './proto/conductor_state.pbenum.dart';
import './repository.dart';
import './state.dart';
import './stdio.dart';
const String kStateOption = 'state-file';
const String kYesFlag = 'yes';
const String kForceFlag = 'force';
/// Command to proceed from one [pb.ReleasePhase] to the next.
class NextCommand extends Command<void> {
NextCommand({
@required this.checkouts,
}) {
final String defaultPath = defaultStateFilePath(checkouts.platform);
argParser.addOption(
kStateOption,
defaultsTo: defaultPath,
help: 'Path to persistent state file. Defaults to $defaultPath',
);
argParser.addFlag(
kYesFlag,
help: 'Auto-accept any confirmation prompts.',
hide: true, // primarily for integration testing
);
argParser.addFlag(
kForceFlag,
help: 'Force push when updating remote git branches.',
);
}
final Checkouts checkouts;
@override
String get name => 'next';
@override
String get description => 'Proceed to the next release phase.';
@override
void run() {
runNext(
autoAccept: argResults[kYesFlag] as bool,
checkouts: checkouts,
force: argResults[kForceFlag] as bool,
stateFile: checkouts.fileSystem.file(argResults[kStateOption]),
);
}
}
@visibleForTesting
bool prompt(String message, Stdio stdio) {
stdio.write('${message.trim()} (y/n) ');
final String response = stdio.readLineSync().trim();
final String firstChar = response[0].toUpperCase();
if (firstChar == 'Y') {
return true;
}
if (firstChar == 'N') {
return false;
}
throw ConductorException(
'Unknown user input (expected "y" or "n"): $response',
);
}
@visibleForTesting
void runNext({
@required bool autoAccept,
@required bool force,
@required Checkouts checkouts,
@required File stateFile,
}) {
final Stdio stdio = checkouts.stdio;
const List<CherrypickState> finishedStates = <CherrypickState>[
CherrypickState.COMPLETED,
CherrypickState.ABANDONED,
];
if (!stateFile.existsSync()) {
throw ConductorException(
'No persistent state file found at ${stateFile.path}.',
);
}
final pb.ConductorState state = readStateFromFile(stateFile);
switch (state.currentPhase) {
case pb.ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
final List<pb.Cherrypick> unappliedCherrypicks = <pb.Cherrypick>[];
for (final pb.Cherrypick cherrypick in state.engine.cherrypicks) {
if (!finishedStates.contains(cherrypick.state)) {
unappliedCherrypicks.add(cherrypick);
}
}
if (state.engine.cherrypicks.isEmpty) {
stdio.printStatus('This release has no engine cherrypicks.');
break;
} else if (unappliedCherrypicks.isEmpty) {
stdio.printStatus('All engine cherrypicks have been auto-applied by '
'the conductor.\n');
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to push your changes to the repository '
'${state.engine.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
} else {
stdio.printStatus(
'There were ${unappliedCherrypicks.length} cherrypicks that were not auto-applied.');
stdio.printStatus('These must be applied manually in the directory '
'${state.engine.checkoutPath} before proceeding.\n');
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to push your engine branch to the repository '
'${state.engine.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
}
break;
case pb.ReleasePhase.CODESIGN_ENGINE_BINARIES:
if (autoAccept == false) {
// TODO(fujino): actually test if binaries have been codesigned on macOS
final bool response = prompt(
'Has CI passed for the engine PR and binaries been codesigned?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
break;
case pb.ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
final List<pb.Cherrypick> unappliedCherrypicks = <pb.Cherrypick>[];
for (final pb.Cherrypick cherrypick in state.framework.cherrypicks) {
if (!finishedStates.contains(cherrypick.state)) {
unappliedCherrypicks.add(cherrypick);
}
}
if (state.framework.cherrypicks.isEmpty) {
stdio.printStatus('This release has no framework cherrypicks.');
break;
} else if (unappliedCherrypicks.isEmpty) {
stdio.printStatus('All framework cherrypicks have been auto-applied by '
'the conductor.\n');
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to push your changes to the repository '
'${state.framework.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
} else {
stdio.printStatus(
'There were ${unappliedCherrypicks.length} cherrypicks that were not auto-applied.');
stdio.printStatus('These must be applied manually in the directory '
'${state.framework.checkoutPath} before proceeding.\n');
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to push your framework branch to the repository '
'${state.framework.mirror.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
}
break;
case pb.ReleasePhase.PUBLISH_VERSION:
stdio.printStatus('Please ensure that you have merged your framework PR and that');
stdio.printStatus('post-submit CI has finished successfully.\n');
final Remote upstream = Remote(
name: RemoteName.upstream,
url: state.framework.upstream.url,
);
final FrameworkRepository framework = FrameworkRepository(
checkouts,
initialRef: state.framework.candidateBranch,
upstreamRemote: upstream,
previousCheckoutLocation: state.framework.checkoutPath,
);
final String headRevision = framework.reverseParse('HEAD');
if (autoAccept == false) {
final bool response = prompt(
'Has CI passed for the framework PR?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
framework.tag(headRevision, state.releaseVersion, upstream.name);
break;
case pb.ReleasePhase.PUBLISH_CHANNEL:
final Remote upstream = Remote(
name: RemoteName.upstream,
url: state.framework.upstream.url,
);
final FrameworkRepository framework = FrameworkRepository(
checkouts,
initialRef: state.framework.candidateBranch,
upstreamRemote: upstream,
previousCheckoutLocation: state.framework.checkoutPath,
);
final String headRevision = framework.reverseParse('HEAD');
if (autoAccept == false) {
final bool response = prompt(
'Are you ready to publish release ${state.releaseVersion} to '
'channel ${state.releaseChannel} at ${state.framework.upstream.url}?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
framework.updateChannel(
headRevision,
state.framework.upstream.url,
state.releaseChannel,
force: force,
);
break;
case pb.ReleasePhase.VERIFY_RELEASE:
stdio.printStatus(
'The current status of packaging builds can be seen at:\n'
'\t$kLuciPackagingConsoleLink',
);
if (autoAccept == false) {
final bool response = prompt(
'Have all packaging builds finished successfully?',
stdio,
);
if (!response) {
stdio.printError('Aborting command.');
writeStateToFile(stateFile, state, stdio.logs);
return;
}
}
break;
case pb.ReleasePhase.RELEASE_COMPLETED:
throw ConductorException('This release is finished.');
break;
}
final ReleasePhase nextPhase = getNextPhase(state.currentPhase);
stdio.printStatus('\nUpdating phase from ${state.currentPhase} to $nextPhase...\n');
state.currentPhase = nextPhase;
stdio.printStatus(phaseInstructions(state));
writeStateToFile(stateFile, state, stdio.logs);
}
......@@ -32,7 +32,7 @@ for SOURCE_FILE in $(ls "$DIR"/*.pb*.dart); do
"$DARTFMT" --overwrite --line-length 120 "$SOURCE_FILE"
# Create temp copy with the license header prepended
cp license_header.txt "${SOURCE_FILE}.tmp"
cp "$DIR/license_header.txt" "${SOURCE_FILE}.tmp"
# Add an extra newline required by analysis (analysis also prevents
# license_header.txt from having the trailing newline)
......
......@@ -378,12 +378,13 @@ class ConductorState extends $pb.GeneratedMessage {
protoName: 'lastUpdatedDate')
..pPS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'logs')
..e<ReleasePhase>(
9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'lastPhase', $pb.PbFieldType.OE,
protoName: 'lastPhase',
defaultOrMaker: ReleasePhase.INITIALIZE,
9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'currentPhase', $pb.PbFieldType.OE,
protoName: 'currentPhase',
defaultOrMaker: ReleasePhase.APPLY_ENGINE_CHERRYPICKS,
valueOf: ReleasePhase.valueOf,
enumValues: ReleasePhase.values)
..aOS(10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'conductorVersion')
..aOS(10, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'conductorVersion',
protoName: 'conductorVersion')
..hasRequiredFields = false;
ConductorState._() : super();
......@@ -395,7 +396,7 @@ class ConductorState extends $pb.GeneratedMessage {
$fixnum.Int64 createdDate,
$fixnum.Int64 lastUpdatedDate,
$core.Iterable<$core.String> logs,
ReleasePhase lastPhase,
ReleasePhase currentPhase,
$core.String conductorVersion,
}) {
final _result = create();
......@@ -420,8 +421,8 @@ class ConductorState extends $pb.GeneratedMessage {
if (logs != null) {
_result.logs.addAll(logs);
}
if (lastPhase != null) {
_result.lastPhase = lastPhase;
if (currentPhase != null) {
_result.currentPhase = currentPhase;
}
if (conductorVersion != null) {
_result.conductorVersion = conductorVersion;
......@@ -531,16 +532,16 @@ class ConductorState extends $pb.GeneratedMessage {
$core.List<$core.String> get logs => $_getList(6);
@$pb.TagNumber(9)
ReleasePhase get lastPhase => $_getN(7);
ReleasePhase get currentPhase => $_getN(7);
@$pb.TagNumber(9)
set lastPhase(ReleasePhase v) {
set currentPhase(ReleasePhase v) {
setField(9, v);
}
@$pb.TagNumber(9)
$core.bool hasLastPhase() => $_has(7);
$core.bool hasCurrentPhase() => $_has(7);
@$pb.TagNumber(9)
void clearLastPhase() => clearField(9);
void clearCurrentPhase() => clearField(9);
@$pb.TagNumber(10)
$core.String get conductorVersion => $_getSZ(8);
......
......@@ -14,29 +14,29 @@ import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class ReleasePhase extends $pb.ProtobufEnum {
static const ReleasePhase INITIALIZE =
ReleasePhase._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'INITIALIZE');
static const ReleasePhase APPLY_ENGINE_CHERRYPICKS =
ReleasePhase._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'APPLY_ENGINE_CHERRYPICKS');
ReleasePhase._(0, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'APPLY_ENGINE_CHERRYPICKS');
static const ReleasePhase CODESIGN_ENGINE_BINARIES =
ReleasePhase._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CODESIGN_ENGINE_BINARIES');
ReleasePhase._(1, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CODESIGN_ENGINE_BINARIES');
static const ReleasePhase APPLY_FRAMEWORK_CHERRYPICKS = ReleasePhase._(
3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'APPLY_FRAMEWORK_CHERRYPICKS');
2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'APPLY_FRAMEWORK_CHERRYPICKS');
static const ReleasePhase PUBLISH_VERSION =
ReleasePhase._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PUBLISH_VERSION');
ReleasePhase._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PUBLISH_VERSION');
static const ReleasePhase PUBLISH_CHANNEL =
ReleasePhase._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PUBLISH_CHANNEL');
ReleasePhase._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'PUBLISH_CHANNEL');
static const ReleasePhase VERIFY_RELEASE =
ReleasePhase._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'VERIFY_RELEASE');
ReleasePhase._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'VERIFY_RELEASE');
static const ReleasePhase RELEASE_COMPLETED =
ReleasePhase._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'RELEASE_COMPLETED');
static const $core.List<ReleasePhase> values = <ReleasePhase>[
INITIALIZE,
APPLY_ENGINE_CHERRYPICKS,
CODESIGN_ENGINE_BINARIES,
APPLY_FRAMEWORK_CHERRYPICKS,
PUBLISH_VERSION,
PUBLISH_CHANNEL,
VERIFY_RELEASE,
RELEASE_COMPLETED,
];
static final $core.Map<$core.int, ReleasePhase> _byValue = $pb.ProtobufEnum.initByValue(values);
......
......@@ -17,19 +17,19 @@ import 'dart:typed_data' as $typed_data;
const ReleasePhase$json = const {
'1': 'ReleasePhase',
'2': const [
const {'1': 'INITIALIZE', '2': 0},
const {'1': 'APPLY_ENGINE_CHERRYPICKS', '2': 1},
const {'1': 'CODESIGN_ENGINE_BINARIES', '2': 2},
const {'1': 'APPLY_FRAMEWORK_CHERRYPICKS', '2': 3},
const {'1': 'PUBLISH_VERSION', '2': 4},
const {'1': 'PUBLISH_CHANNEL', '2': 5},
const {'1': 'VERIFY_RELEASE', '2': 6},
const {'1': 'APPLY_ENGINE_CHERRYPICKS', '2': 0},
const {'1': 'CODESIGN_ENGINE_BINARIES', '2': 1},
const {'1': 'APPLY_FRAMEWORK_CHERRYPICKS', '2': 2},
const {'1': 'PUBLISH_VERSION', '2': 3},
const {'1': 'PUBLISH_CHANNEL', '2': 4},
const {'1': 'VERIFY_RELEASE', '2': 5},
const {'1': 'RELEASE_COMPLETED', '2': 6},
],
};
/// Descriptor for `ReleasePhase`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List releasePhaseDescriptor = $convert.base64Decode(
'CgxSZWxlYXNlUGhhc2USDgoKSU5JVElBTElaRRAAEhwKGEFQUExZX0VOR0lORV9DSEVSUllQSUNLUxABEhwKGENPREVTSUdOX0VOR0lORV9CSU5BUklFUxACEh8KG0FQUExZX0ZSQU1FV09SS19DSEVSUllQSUNLUxADEhMKD1BVQkxJU0hfVkVSU0lPThAEEhMKD1BVQkxJU0hfQ0hBTk5FTBAFEhIKDlZFUklGWV9SRUxFQVNFEAY=');
'CgxSZWxlYXNlUGhhc2USHAoYQVBQTFlfRU5HSU5FX0NIRVJSWVBJQ0tTEAASHAoYQ09ERVNJR05fRU5HSU5FX0JJTkFSSUVTEAESHwobQVBQTFlfRlJBTUVXT1JLX0NIRVJSWVBJQ0tTEAISEwoPUFVCTElTSF9WRVJTSU9OEAMSEwoPUFVCTElTSF9DSEFOTkVMEAQSEgoOVkVSSUZZX1JFTEVBU0UQBRIVChFSRUxFQVNFX0NPTVBMRVRFRBAG');
@$core.Deprecated('Use cherrypickStateDescriptor instead')
const CherrypickState$json = const {
'1': 'CherrypickState',
......@@ -98,11 +98,11 @@ const ConductorState$json = const {
const {'1': 'createdDate', '3': 6, '4': 1, '5': 3, '10': 'createdDate'},
const {'1': 'lastUpdatedDate', '3': 7, '4': 1, '5': 3, '10': 'lastUpdatedDate'},
const {'1': 'logs', '3': 8, '4': 3, '5': 9, '10': 'logs'},
const {'1': 'lastPhase', '3': 9, '4': 1, '5': 14, '6': '.conductor_state.ReleasePhase', '10': 'lastPhase'},
const {'1': 'conductor_version', '3': 10, '4': 1, '5': 9, '10': 'conductorVersion'},
const {'1': 'currentPhase', '3': 9, '4': 1, '5': 14, '6': '.conductor_state.ReleasePhase', '10': 'currentPhase'},
const {'1': 'conductorVersion', '3': 10, '4': 1, '5': 9, '10': 'conductorVersion'},
],
};
/// Descriptor for `ConductorState`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List conductorStateDescriptor = $convert.base64Decode(
'Cg5Db25kdWN0b3JTdGF0ZRImCg5yZWxlYXNlQ2hhbm5lbBgBIAEoCVIOcmVsZWFzZUNoYW5uZWwSJgoOcmVsZWFzZVZlcnNpb24YAiABKAlSDnJlbGVhc2VWZXJzaW9uEjMKBmVuZ2luZRgEIAEoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5SZXBvc2l0b3J5UgZlbmdpbmUSOQoJZnJhbWV3b3JrGAUgASgLMhsuY29uZHVjdG9yX3N0YXRlLlJlcG9zaXRvcnlSCWZyYW1ld29yaxIgCgtjcmVhdGVkRGF0ZRgGIAEoA1ILY3JlYXRlZERhdGUSKAoPbGFzdFVwZGF0ZWREYXRlGAcgASgDUg9sYXN0VXBkYXRlZERhdGUSEgoEbG9ncxgIIAMoCVIEbG9ncxI7CglsYXN0UGhhc2UYCSABKA4yHS5jb25kdWN0b3Jfc3RhdGUuUmVsZWFzZVBoYXNlUglsYXN0UGhhc2USKwoRY29uZHVjdG9yX3ZlcnNpb24YCiABKAlSEGNvbmR1Y3RvclZlcnNpb24=');
'Cg5Db25kdWN0b3JTdGF0ZRImCg5yZWxlYXNlQ2hhbm5lbBgBIAEoCVIOcmVsZWFzZUNoYW5uZWwSJgoOcmVsZWFzZVZlcnNpb24YAiABKAlSDnJlbGVhc2VWZXJzaW9uEjMKBmVuZ2luZRgEIAEoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5SZXBvc2l0b3J5UgZlbmdpbmUSOQoJZnJhbWV3b3JrGAUgASgLMhsuY29uZHVjdG9yX3N0YXRlLlJlcG9zaXRvcnlSCWZyYW1ld29yaxIgCgtjcmVhdGVkRGF0ZRgGIAEoA1ILY3JlYXRlZERhdGUSKAoPbGFzdFVwZGF0ZWREYXRlGAcgASgDUg9sYXN0VXBkYXRlZERhdGUSEgoEbG9ncxgIIAMoCVIEbG9ncxJBCgxjdXJyZW50UGhhc2UYCSABKA4yHS5jb25kdWN0b3Jfc3RhdGUuUmVsZWFzZVBoYXNlUgxjdXJyZW50UGhhc2USKgoQY29uZHVjdG9yVmVyc2lvbhgKIAEoCVIQY29uZHVjdG9yVmVyc2lvbg==');
......@@ -10,21 +10,23 @@ message Remote {
enum ReleasePhase {
// Release was started with `conductor start` and repositories cloned.
INITIALIZE = 0;
APPLY_ENGINE_CHERRYPICKS = 1;
CODESIGN_ENGINE_BINARIES = 2;
APPLY_FRAMEWORK_CHERRYPICKS = 3;
APPLY_ENGINE_CHERRYPICKS = 0;
CODESIGN_ENGINE_BINARIES = 1;
APPLY_FRAMEWORK_CHERRYPICKS = 2;
// Git tag applied to framework RC branch HEAD and pushed upstream.
PUBLISH_VERSION = 4;
PUBLISH_VERSION = 3;
// RC branch HEAD pushed to upstream release branch.
//
// For example, flutter-1.2-candidate.3 -> upstream/beta
PUBLISH_CHANNEL = 5;
PUBLISH_CHANNEL = 4;
// Package artifacts verified to exist on cloud storage.
VERIFY_RELEASE = 6;
VERIFY_RELEASE = 5;
// There is no further work to be done.
RELEASE_COMPLETED = 6;
}
enum CherrypickState {
......@@ -98,9 +100,9 @@ message ConductorState {
repeated string logs = 8;
// The last [ReleasePhase] that was successfully completed.
ReleasePhase lastPhase = 9;
// The current [ReleasePhase] that has yet to be completed.
ReleasePhase currentPhase = 9;
// Commit hash of the Conductor tool.
string conductor_version = 10;
string conductorVersion = 10;
}
This diff is collapsed.
......@@ -13,7 +13,7 @@ import './stdio.dart';
import './version.dart';
const String kIncrement = 'increment';
const String kCommit = 'commit';
const String kCandidateBranch = 'candidate-branch';
const String kRemoteName = 'remote';
const String kJustPrint = 'just-print';
const String kYes = 'yes';
......@@ -40,9 +40,9 @@ class RollDevCommand extends Command<void> {
},
);
argParser.addOption(
kCommit,
help: 'Specifies which git commit to roll to the dev branch. Required.',
valueHelp: 'hash',
kCandidateBranch,
help: 'Specifies which git branch to roll to the dev branch. Required.',
valueHelp: 'branch',
defaultsTo: null, // This option is required
);
argParser.addFlag(
......@@ -112,17 +112,16 @@ bool rollDev({
}) {
final String remoteName = argResults[kRemoteName] as String;
final String level = argResults[kIncrement] as String;
final String commit = argResults[kCommit] as String;
final String candidateBranch = argResults[kCandidateBranch] as String;
final bool justPrint = argResults[kJustPrint] as bool;
final bool autoApprove = argResults[kYes] as bool;
final bool force = argResults[kForce] as bool;
final bool skipTagging = argResults[kSkipTagging] as bool;
if (level == null || commit == null) {
stdio.printStatus(
'roll_dev.dart --increment=level --commit=hash • update the version tags '
if (level == null || candidateBranch == null) {
throw Exception(
'roll_dev.dart --$kIncrement=level --$kCandidateBranch=branch • update the version tags '
'and roll a new dev build.\n$usage');
return false;
}
final String remoteUrl = repository.remoteUrl(remoteName);
......@@ -136,14 +135,16 @@ bool rollDev({
repository.fetch(remoteName);
// Verify [commit] is valid
repository.reverseParse(commit);
final String commit = repository.reverseParse(candidateBranch);
stdio.printStatus('remoteName is $remoteName');
final Version lastVersion =
Version.fromString(repository.getFullTag(remoteName));
// Get the name of the last dev release
final Version lastVersion = Version.fromString(
repository.getFullTag(remoteName, 'dev'),
);
final Version version =
skipTagging ? lastVersion : Version.increment(lastVersion, level);
skipTagging ? lastVersion : Version.fromCandidateBranch(candidateBranch);
final String tagName = version.toString();
if (repository.reverseParse(lastVersion.toString()).contains(commit.trim())) {
......
......@@ -4,8 +4,6 @@
// @dart = 2.8
import 'dart:convert' show jsonEncode;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:fixnum/fixnum.dart';
......@@ -20,6 +18,7 @@ import './proto/conductor_state.pbenum.dart' show ReleasePhase;
import './repository.dart';
import './state.dart';
import './stdio.dart';
import './version.dart';
const String kCandidateOption = 'candidate-branch';
const String kDartRevisionOption = 'dart-revision';
......@@ -28,6 +27,7 @@ const String kEngineUpstreamOption = 'engine-upstream';
const String kFrameworkCherrypicksOption = 'framework-cherrypicks';
const String kFrameworkMirrorOption = 'framework-mirror';
const String kFrameworkUpstreamOption = 'framework-upstream';
const String kIncrementOption = 'increment';
const String kEngineMirrorOption = 'engine-mirror';
const String kReleaseOption = 'release-channel';
const String kStateOption = 'state-file';
......@@ -91,6 +91,18 @@ class StartCommand extends Command<void> {
kDartRevisionOption,
help: 'New Dart revision to cherrypick.',
);
argParser.addOption(
kIncrementOption,
help: 'Specifies which part of the x.y.z version number to increment. Required.',
valueHelp: 'level',
allowed: <String>['y', 'z', 'm', 'n'],
allowedHelp: <String, String>{
'y': 'Indicates the first dev release after a beta release.',
'z': 'Indicates a hotfix to a stable release.',
'm': 'Indicates a standard dev release.',
'n': 'Indicates a hotfix to a dev release.',
},
);
final Git git = Git(processManager);
conductorVersion = git.getOutput(
<String>['rev-parse', 'HEAD'],
......@@ -183,6 +195,12 @@ class StartCommand extends Command<void> {
platform.environment,
allowNull: true,
);
final String incrementLetter = getValueFromEnvOrArgs(
kIncrementOption,
argResults,
platform.environment,
);
if (!releaseCandidateBranchRegex.hasMatch(candidateBranch)) {
throw ConductorException(
'Invalid release candidate branch "$candidateBranch". Text should '
......@@ -200,11 +218,11 @@ class StartCommand extends Command<void> {
final EngineRepository engine = EngineRepository(
checkouts,
initialRef: candidateBranch,
fetchRemote: Remote(
upstreamRemote: Remote(
name: RemoteName.upstream,
url: engineUpstream,
),
pushRemote: Remote(
mirrorRemote: Remote(
name: RemoteName.mirror,
url: engineMirror,
),
......@@ -249,15 +267,17 @@ class StartCommand extends Command<void> {
checkoutPath: engine.checkoutDirectory.path,
cherrypicks: engineCherrypicks,
dartRevision: dartRevision,
upstream: pb.Remote(name: 'upstream', url: engine.upstreamRemote.url),
mirror: pb.Remote(name: 'mirror', url: engine.mirrorRemote.url),
);
final FrameworkRepository framework = FrameworkRepository(
checkouts,
initialRef: candidateBranch,
fetchRemote: Remote(
upstreamRemote: Remote(
name: RemoteName.upstream,
url: frameworkUpstream,
),
pushRemote: Remote(
mirrorRemote: Remote(
name: RemoteName.mirror,
url: frameworkMirror,
),
......@@ -287,6 +307,16 @@ class StartCommand extends Command<void> {
}
}
// Get framework version
final Version lastVersion = Version.fromString(framework.getFullTag(framework.upstreamRemote.name, candidateBranch, exact: false));
Version nextVersion;
if (incrementLetter == 'm') {
nextVersion = Version.fromCandidateBranch(candidateBranch);
} else {
nextVersion = Version.increment(lastVersion, incrementLetter);
}
state.releaseVersion = nextVersion.toString();
final String frameworkHead = framework.reverseParse('HEAD');
state.framework = pb.Repository(
candidateBranch: candidateBranch,
......@@ -294,20 +324,17 @@ class StartCommand extends Command<void> {
currentGitHead: frameworkHead,
checkoutPath: framework.checkoutDirectory.path,
cherrypicks: frameworkCherrypicks,
upstream: pb.Remote(name: 'upstream', url: framework.upstreamRemote.url),
mirror: pb.Remote(name: 'mirror', url: framework.mirrorRemote.url),
);
state.lastPhase = ReleasePhase.INITIALIZE;
state.currentPhase = ReleasePhase.APPLY_ENGINE_CHERRYPICKS;
state.conductorVersion = conductorVersion;
stdio.printTrace('Writing state to file ${stateFile.path}...');
state.logs.addAll(stdio.logs);
stateFile.writeAsStringSync(
jsonEncode(state.toProto3Json()),
flush: true,
);
writeStateToFile(stateFile, state, stdio.logs);
stdio.printStatus(presentState(state));
}
......@@ -340,8 +367,8 @@ class StartCommand extends Command<void> {
}
final String branchPoint = repository.branchPoint(
'${repository.fetchRemote.name}/$upstreamRef',
'${repository.fetchRemote.name}/$releaseRef',
'${repository.upstreamRemote.name}/$upstreamRef',
'${repository.upstreamRemote.name}/$releaseRef',
);
// `git rev-list` returns newest first, so reverse this list
......
......@@ -4,6 +4,9 @@
// @dart = 2.8
import 'dart:convert' show jsonDecode, jsonEncode;
import 'package:file/file.dart' show File;
import 'package:platform/platform.dart';
import './globals.dart';
......@@ -37,6 +40,7 @@ String presentState(pb.ConductorState state) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('Conductor version: ${state.conductorVersion}');
buffer.writeln('Release channel: ${state.releaseChannel}');
buffer.writeln('Release version: ${state.releaseVersion}');
buffer.writeln('');
buffer.writeln(
'Release started at: ${DateTime.fromMillisecondsSinceEpoch(state.createdDate.toInt())}');
......@@ -76,14 +80,14 @@ String presentState(pb.ConductorState state) {
buffer.writeln('0 Framework cherrypicks.');
}
buffer.writeln('');
if (state.lastPhase == ReleasePhase.VERIFY_RELEASE) {
if (state.currentPhase == 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('The current phase is:');
buffer.writeln(presentPhases(state.currentPhase));
buffer.writeln(phaseInstructions(state));
buffer.writeln('');
......@@ -91,15 +95,14 @@ String presentState(pb.ConductorState state) {
return buffer.toString();
}
String presentPhases(ReleasePhase lastPhase) {
final ReleasePhase nextPhase = getNextPhase(lastPhase);
String presentPhases(ReleasePhase currentPhase) {
final StringBuffer buffer = StringBuffer();
bool phaseCompleted = true;
for (final ReleasePhase phase in ReleasePhase.values) {
if (phase == nextPhase) {
if (phase == currentPhase) {
// This phase will execute the next time `conductor next` is run.
buffer.writeln('> ${phase.name} (next)');
buffer.writeln('> ${phase.name} (current)');
phaseCompleted = false;
} else if (phaseCompleted) {
// This phase was already completed.
......@@ -113,8 +116,8 @@ String presentPhases(ReleasePhase lastPhase) {
}
String phaseInstructions(pb.ConductorState state) {
switch (state.lastPhase) {
case ReleasePhase.INITIALIZE:
switch (state.currentPhase) {
case ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
if (state.engine.cherrypicks.isEmpty) {
return <String>[
'There are no engine cherrypicks, so issue `conductor next` to continue',
......@@ -128,31 +131,33 @@ String phaseInstructions(pb.ConductorState state) {
'\t${cherrypick.trunkRevision}',
'See $kReleaseDocumentationUrl for more information.',
].join('\n');
case ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
case ReleasePhase.CODESIGN_ENGINE_BINARIES:
return <String>[
'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:
case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
final List<pb.Cherrypick> outstandingCherrypicks = state.framework.cherrypicks.where(
(pb.Cherrypick cp) {
return cp.state == pb.CherrypickState.PENDING || cp.state == pb.CherrypickState.PENDING_WITH_CONFLICT;
},
).toList();
return <String>[
'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)
for (final pb.Cherrypick cherrypick in outstandingCherrypicks)
'\t${cherrypick.trunkRevision}',
].join('\n');
case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS:
case ReleasePhase.PUBLISH_VERSION:
return <String>[
'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 <String>[
'Release archive packages must be verified on cloud storage. Issue',
'`conductor next` to check if they are ready.',
].join('\n');
return 'Issue `conductor next` to publish your release to the release branch.';
case ReleasePhase.VERIFY_RELEASE:
return 'Release archive packages must be verified on cloud storage.';
case ReleasePhase.RELEASE_COMPLETED:
return 'This release has been completed.';
}
assert(false);
......@@ -161,12 +166,29 @@ String phaseInstructions(pb.ConductorState state) {
/// Returns the next phase in the ReleasePhase enum.
///
/// Will throw a [ConductorException] if [ReleasePhase.RELEASE_VERIFIED] is
/// Will throw a [ConductorException] if [ReleasePhase.RELEASE_COMPLETED] is
/// passed as an argument, as there is no next phase.
ReleasePhase getNextPhase(ReleasePhase previousPhase) {
assert(previousPhase != null);
if (previousPhase == ReleasePhase.VERIFY_RELEASE) {
if (previousPhase == ReleasePhase.RELEASE_COMPLETED) {
throw ConductorException('There is no next ReleasePhase!');
}
return ReleasePhase.valueOf(previousPhase.value + 1);
}
void writeStateToFile(File file, pb.ConductorState state, List<String> logs) {
state.logs.addAll(logs);
file.writeAsStringSync(
jsonEncode(state.toProto3Json()),
flush: true,
);
}
pb.ConductorState readStateFromFile(File file) {
final pb.ConductorState state = pb.ConductorState();
final String stateAsString = file.readAsStringSync();
state.mergeFromProto3Json(
jsonDecode(stateAsString),
);
return state;
}
......@@ -4,8 +4,6 @@
// @dart = 2.8
import 'dart:convert' show jsonDecode;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
......@@ -59,8 +57,8 @@ class StatusCommand extends Command<void> {
'No persistent state file found at ${argResults[kStateOption]}.');
return;
}
final pb.ConductorState state = pb.ConductorState();
state.mergeFromProto3Json(jsonDecode(stateFile.readAsStringSync()));
final pb.ConductorState state = readStateFromFile(stateFile);
stdio.printStatus(presentState(state));
if (argResults[kVerboseFlag] as bool) {
stdio.printStatus('\nLogs:');
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './globals.dart' show ConductorException;
/// Possible string formats that `flutter --version` can return.
enum VersionType {
/// A stable flutter release.
......@@ -20,12 +22,18 @@ enum VersionType {
///
/// The last number is the number of commits past the last tagged version.
latest,
/// A master channel flutter version from git describe.
///
/// Example: '1.2.3-4.0.pre-10-gabc123'.
gitDescribe,
}
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+)$'),
VersionType.gitDescribe: RegExp(r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre-(\d+)-g[a-f0-9]+$'),
};
class Version {
......@@ -54,6 +62,10 @@ class Version {
assert(n != null);
assert(commits != null);
break;
case VersionType.gitDescribe:
throw ConductorException(
'VersionType.gitDescribe not supported! Use VersionType.latest instead.',
);
}
}
......@@ -115,6 +127,24 @@ class Version {
type: VersionType.latest,
);
}
match = versionPatterns[VersionType.gitDescribe]!.firstMatch(versionString);
if (match != null) {
// parse latest
final List<int> parts = match.groups(
<int>[1, 2, 3, 4, 5, 6],
).map(
(String? s) => int.parse(s!),
).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');
}
......@@ -131,7 +161,7 @@ class Version {
int? nextM = previousVersion.m;
int? nextN = previousVersion.n;
if (nextVersionType == null) {
if (previousVersion.type == VersionType.latest) {
if (previousVersion.type == VersionType.latest || previousVersion.type == VersionType.gitDescribe) {
nextVersionType = VersionType.development;
} else {
nextVersionType = previousVersion.type;
......@@ -157,10 +187,7 @@ class Version {
nextZ += 1;
break;
case 'm':
// Regular dev release.
assert(previousVersion.type == VersionType.development);
nextM = nextM! + 1;
nextN = 0;
assert(false, "Do not increment 'm' via Version.increment, use instead Version.fromCandidateBranch()");
break;
case 'n':
// Hotfix to internal roll.
......@@ -179,6 +206,31 @@ class Version {
);
}
factory Version.fromCandidateBranch(String branchName) {
// Regular dev release.
final RegExp pattern = RegExp(r'flutter-(\d+)\.(\d+)-candidate.(\d+)');
final RegExpMatch? match = pattern.firstMatch(branchName);
late final int x;
late final int y;
late final int m;
try {
x = int.parse(match!.group(1)!);
y = int.parse(match.group(2)!);
m = int.parse(match.group(3)!);
} on Exception {
throw ConductorException('branch named $branchName not recognized as a valid candidate branch');
}
return Version(
type: VersionType.development,
x: x,
y: y,
z: 0,
m: m,
n: 0,
);
}
/// Major version.
final int x;
......@@ -208,6 +260,8 @@ class Version {
return '$x.$y.$z-$m.$n.pre';
case VersionType.latest:
return '$x.$y.$z-$m.$n.pre.$commits';
case VersionType.gitDescribe:
return '$x.$y.$z-$m.$n.pre.$commits';
}
}
}
......@@ -4,7 +4,6 @@
import 'package:args/command_runner.dart';
import 'package:conductor/codesign.dart';
import 'package:conductor/globals.dart';
import 'package:conductor/repository.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
......@@ -16,7 +15,7 @@ import '../../../packages/flutter_tools/test/src/fake_process_manager.dart';
void main() {
group('codesign command', () {
const String flutterRoot = '/flutter';
const String checkoutsParentDirectory = '$flutterRoot/dev/tools/';
const String checkoutsParentDirectory = '$flutterRoot/dev/conductor/';
const String flutterCache =
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache';
const String flutterBin =
......@@ -387,18 +386,13 @@ void main() {
stdout: 'application/x-mach-binary',
),
]);
try {
await runner.run(<String>[
'codesign',
'--$kVerify',
'--no-$kSignatures',
'--$kRevision',
revision,
]);
} on ConductorException {
//print(stdio.error);
rethrow;
}
await runner.run(<String>[
'codesign',
'--$kVerify',
'--no-$kSignatures',
'--$kRevision',
revision,
]);
expect(
processManager.hasRemainingExpectations,
false,
......
......@@ -9,6 +9,16 @@ import 'package:test/test.dart';
export 'package:test/test.dart' hide isInstanceOf;
Matcher throwsAssertionWith(String messageSubString) {
return throwsA(
isA<AssertionError>().having(
(AssertionError e) => e.toString(),
'description',
contains(messageSubString),
),
);
}
Matcher throwsExceptionWith(String messageSubString) {
return throwsA(
isA<Exception>().having(
......@@ -28,11 +38,12 @@ class TestStdio extends Stdio {
String get error => logs.where((String log) => log.startsWith(r'[error] ')).join('\n');
String get stdout => logs.where((String log) {
return log.startsWith(r'[status] ') || log.startsWith(r'[trace] ');
return log.startsWith(r'[status] ') || log.startsWith(r'[trace] ') || log.startsWith(r'[write] ');
}).join('\n');
final bool verbose;
late final List<String> _stdin;
List<String> get stdin => _stdin;
@override
String readLineSync() {
......@@ -46,7 +57,7 @@ class TestStdio extends Stdio {
class FakeArgResults implements ArgResults {
FakeArgResults({
required String level,
required String commit,
required String candidateBranch,
String remote = 'upstream',
bool justPrint = false,
bool autoApprove = true, // so we don't have to mock stdin
......@@ -55,7 +66,7 @@ class FakeArgResults implements ArgResults {
bool skipTagging = false,
}) : _parsedArgs = <String, dynamic>{
'increment': level,
'commit': commit,
'candidate-branch': candidateBranch,
'remote': remote,
'just-print': justPrint,
'yes': autoApprove,
......
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:conductor/repository.dart';
import 'package:conductor/roll_dev.dart' show rollDev;
import 'package:conductor/version.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import './common.dart';
void main() {
group('roll-dev', () {
late TestStdio stdio;
late Platform platform;
late ProcessManager processManager;
late FileSystem fileSystem;
const String usageString = 'Usage: flutter conductor.';
late Checkouts checkouts;
late FrameworkRepository frameworkUpstream;
late FrameworkRepository framework;
late Directory tempDir;
setUp(() {
platform = const LocalPlatform();
fileSystem = const LocalFileSystem();
processManager = const LocalProcessManager();
stdio = TestStdio(verbose: true);
tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_conductor_checkouts.');
checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: tempDir,
platform: platform,
processManager: processManager,
stdio: stdio,
);
frameworkUpstream = FrameworkRepository(checkouts, localUpstream: true);
// This repository has [frameworkUpstream] set as its push/pull remote.
framework = FrameworkRepository(
checkouts,
name: 'test-framework',
fetchRemote: Remote(name: RemoteName.upstream, url: 'file://${frameworkUpstream.checkoutDirectory.path}/'),
);
});
test('increment m', () {
final Version initialVersion = framework.flutterVersion();
final String latestCommit = framework.authorEmptyCommit();
final FakeArgResults fakeArgResults = FakeArgResults(
level: 'm',
commit: latestCommit,
// Ensure this test passes after a dev release with hotfixes
force: true,
remote: 'upstream',
);
expect(
rollDev(
usage: usageString,
argResults: fakeArgResults,
stdio: stdio,
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);
});
}, onPlatform: <String, dynamic>{
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
});
}
......@@ -20,7 +20,8 @@ void main() {
const String commit = 'abcde012345';
const String remote = 'origin';
const String lastVersion = '1.2.0-0.0.pre';
const String nextVersion = '1.2.0-1.0.pre';
const String nextVersion = '1.2.0-2.0.pre';
const String candidateBranch = 'flutter-1.2-candidate.2';
const String checkoutsParentDirectory = '/path/to/directory/';
FakeArgResults fakeArgResults;
MemoryFileSystem fileSystem;
......@@ -45,37 +46,20 @@ void main() {
repo = FrameworkRepository(checkouts);
});
test('returns false if level not provided', () {
test('throws Exception if level not provided', () {
fakeArgResults = FakeArgResults(
level: null,
commit: commit,
candidateBranch: candidateBranch,
remote: remote,
);
expect(
rollDev(
argResults: fakeArgResults,
repository: repo,
stdio: stdio,
usage: usage,
),
false,
);
});
test('returns false if commit not provided', () {
fakeArgResults = FakeArgResults(
level: level,
commit: null,
remote: remote,
);
expect(
rollDev(
() => rollDev(
argResults: fakeArgResults,
repository: repo,
stdio: stdio,
usage: usage,
),
false,
throwsExceptionWith(usage),
);
});
......@@ -109,24 +93,18 @@ void main() {
]);
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
candidateBranch: candidateBranch,
remote: remote,
);
Exception exception;
try {
rollDev(
expect(
() => rollDev(
argResults: fakeArgResults,
repository: repo,
stdio: stdio,
usage: usage,
);
} on Exception catch (e) {
exception = e;
}
const String pattern = r'Your git repository is not clean. Try running '
'"git clean -fd". Warning, this will delete files! Run with -n to find '
'out which ones.';
expect(exception?.toString(), contains(pattern));
),
throwsExceptionWith('Your git repository is not clean.'),
);
});
test('does not reset or tag if --just-print is specified', () {
......@@ -165,13 +143,13 @@ void main() {
const FakeCommand(command: <String>[
'git',
'rev-parse',
commit,
candidateBranch,
], stdout: commit),
const FakeCommand(command: <String>[
'git',
'describe',
'--match',
'*.*.*-*.*.pre',
'*.*.*',
'--exact-match',
'--tags',
'refs/remotes/$remote/dev',
......@@ -185,7 +163,7 @@ void main() {
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
candidateBranch: candidateBranch,
remote: remote,
justPrint: true,
);
......@@ -237,13 +215,13 @@ void main() {
const FakeCommand(command: <String>[
'git',
'rev-parse',
commit,
candidateBranch,
], stdout: commit),
const FakeCommand(command: <String>[
'git',
'describe',
'--match',
'*.*.*-*.*.pre',
'*.*.*',
'--exact-match',
'--tags',
'refs/remotes/$remote/dev',
......@@ -268,7 +246,7 @@ void main() {
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
candidateBranch: candidateBranch,
remote: remote,
skipTagging: true,
);
......@@ -319,13 +297,13 @@ void main() {
const FakeCommand(command: <String>[
'git',
'rev-parse',
commit,
candidateBranch,
], stdout: commit),
const FakeCommand(command: <String>[
'git',
'describe',
'--match',
'*.*.*-*.*.pre',
'*.*.*',
'--exact-match',
'--tags',
'refs/remotes/$remote/dev',
......@@ -339,7 +317,7 @@ void main() {
]);
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
candidateBranch: candidateBranch,
remote: remote,
justPrint: true,
);
......@@ -394,13 +372,13 @@ void main() {
const FakeCommand(command: <String>[
'git',
'rev-parse',
commit,
candidateBranch,
], stdout: commit),
const FakeCommand(command: <String>[
'git',
'describe',
'--match',
'*.*.*-*.*.pre',
'*.*.*',
'--exact-match',
'--tags',
'refs/remotes/$remote/dev',
......@@ -421,7 +399,7 @@ void main() {
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
candidateBranch: candidateBranch,
remote: remote,
);
const String errorMessage = 'The previous dev tag $lastVersion is not a '
......@@ -473,13 +451,13 @@ void main() {
const FakeCommand(command: <String>[
'git',
'rev-parse',
commit,
candidateBranch,
], stdout: commit),
const FakeCommand(command: <String>[
'git',
'describe',
'--match',
'*.*.*-*.*.pre',
'*.*.*',
'--exact-match',
'--tags',
'refs/remotes/$remote/dev',
......@@ -517,7 +495,7 @@ void main() {
]);
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
candidateBranch: candidateBranch,
remote: remote,
skipTagging: true,
);
......@@ -568,13 +546,13 @@ void main() {
const FakeCommand(command: <String>[
'git',
'rev-parse',
commit,
candidateBranch,
], stdout: commit),
const FakeCommand(command: <String>[
'git',
'describe',
'--match',
'*.*.*-*.*.pre',
'*.*.*',
'--exact-match',
'--tags',
'refs/remotes/$remote/dev',
......@@ -617,7 +595,7 @@ void main() {
]);
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
candidateBranch: candidateBranch,
remote: remote,
);
expect(
......@@ -667,13 +645,13 @@ void main() {
const FakeCommand(command: <String>[
'git',
'rev-parse',
commit,
candidateBranch,
], stdout: commit),
const FakeCommand(command: <String>[
'git',
'describe',
'--match',
'*.*.*-*.*.pre',
'*.*.*',
'--exact-match',
'--tags',
'refs/remotes/$remote/dev',
......@@ -711,7 +689,7 @@ void main() {
fakeArgResults = FakeArgResults(
level: level,
commit: commit,
candidateBranch: candidateBranch,
remote: remote,
force: true,
);
......
......@@ -122,6 +122,8 @@ void main() {
const String revision3 = '123abc';
const String previousDartRevision = '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
const String nextDartRevision = 'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e';
const String previousVersion = '1.2.0-1.0.pre';
const String nextVersion = '1.2.0-3.0.pre';
final Directory engine = fileSystem.directory(checkoutsParentDirectory)
.childDirectory('flutter_conductor_checkouts')
......@@ -182,6 +184,7 @@ void main() {
stdout: revision2,
),
];
final List<FakeCommand> frameworkCommands = <FakeCommand>[
FakeCommand(
command: <String>[
......@@ -219,11 +222,23 @@ void main() {
'cherrypicks-$candidateBranch',
],
),
const FakeCommand(
command: <String>[
'git',
'describe',
'--match',
'*.*.*',
'--tags',
'refs/remotes/upstream/$candidateBranch',
],
stdout: '$previousVersion-42-gabc123',
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision3,
),
];
final CommandRunner<void> runner = createRunner(
commands: <FakeCommand>[
const FakeCommand(
......@@ -254,6 +269,8 @@ void main() {
stateFilePath,
'--$kDartRevisionOption',
nextDartRevision,
'--$kIncrementOption',
'm',
]);
final File stateFile = fileSystem.file(stateFilePath);
......@@ -265,12 +282,13 @@ void main() {
expect(state.isInitialized(), true);
expect(state.releaseChannel, releaseChannel);
expect(state.releaseVersion, nextVersion);
expect(state.engine.candidateBranch, candidateBranch);
expect(state.engine.startingGitHead, revision2);
expect(state.engine.dartRevision, nextDartRevision);
expect(state.framework.candidateBranch, candidateBranch);
expect(state.framework.startingGitHead, revision3);
expect(state.lastPhase, ReleasePhase.INITIALIZE);
expect(state.currentPhase, ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
expect(state.conductorVersion, revision);
});
}, onPlatform: <String, dynamic>{
......
......@@ -44,29 +44,26 @@ void main() {
});
test('successfully increments z', () {
const String level = 'm';
const String level = 'z';
Version version = Version.fromString('1.0.0-0.0.pre');
expect(Version.increment(version, level).toString(), '1.0.0-1.0.pre');
Version version = Version.fromString('1.0.0');
expect(Version.increment(version, level).toString(), '1.0.1');
version = Version.fromString('10.20.0-40.50.pre');
expect(Version.increment(version, level).toString(), '10.20.0-41.0.pre');
version = Version.fromString('10.20.0');
expect(Version.increment(version, level).toString(), '10.20.1');
version = Version.fromString('1.18.0-3.0.pre');
expect(Version.increment(version, level).toString(), '1.18.0-4.0.pre');
version = Version.fromString('1.18.3');
expect(Version.increment(version, level).toString(), '1.18.4');
});
test('successfully increments m', () {
test('does not support incrementing 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');
final Version version = Version.fromString('1.0.0-0.0.pre');
expect(
() => Version.increment(version, level).toString(),
throwsAssertionWith("Do not increment 'm' via Version.increment"),
);
});
test('successfully increments n', () {
......
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