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: ...@@ -29,8 +29,8 @@ Releases are initialized with the `start` sub-command, like:
conductor start \ conductor start \
--candidate-branch=flutter-2.2-candidate.10 \ --candidate-branch=flutter-2.2-candidate.10 \
--release-channel=beta \ --release-channel=beta \
--framework-mirror=git@github.com:flutter-contributor/flutter.git \ --framework-mirror=git@github.com:username/flutter.git \
--engine-mirror=git@github.com:flutter-contributor/engine.git \ --engine-mirror=git@github.com:username/engine.git \
--engine-cherrypicks=72114dafe28c8700f1d5d629c6ae9d34172ba395 \ --engine-cherrypicks=72114dafe28c8700f1d5d629c6ae9d34172ba395 \
--framework-cherrypicks=a3e66b396746f6581b2b7efd1b0d0f0074215128,d8d853436206e86f416236b930e97779b143a100 \ --framework-cherrypicks=a3e66b396746f6581b2b7efd1b0d0f0074215128,d8d853436206e86f416236b930e97779b143a100 \
--dart-revision=4511eb2a779a612d9d6b2012123575013e0aef12 --dart-revision=4511eb2a779a612d9d6b2012123575013e0aef12
...@@ -54,3 +54,51 @@ Upon successful completion of the release, the following command will remove the ...@@ -54,3 +54,51 @@ Upon successful completion of the release, the following command will remove the
persistent state file: persistent state file:
`conductor clean` `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'; ...@@ -13,6 +13,7 @@ import 'package:conductor/candidates.dart';
import 'package:conductor/clean.dart'; import 'package:conductor/clean.dart';
import 'package:conductor/codesign.dart'; import 'package:conductor/codesign.dart';
import 'package:conductor/globals.dart'; import 'package:conductor/globals.dart';
import 'package:conductor/next.dart';
import 'package:conductor/repository.dart'; import 'package:conductor/repository.dart';
import 'package:conductor/roll_dev.dart'; import 'package:conductor/roll_dev.dart';
import 'package:conductor/start.dart'; import 'package:conductor/start.dart';
...@@ -74,6 +75,9 @@ Future<void> main(List<String> args) async { ...@@ -74,6 +75,9 @@ Future<void> main(List<String> args) async {
checkouts: checkouts, checkouts: checkouts,
flutterRoot: localFlutterRoot, flutterRoot: localFlutterRoot,
), ),
NextCommand(
checkouts: checkouts,
),
].forEach(runner.addCommand); ].forEach(runner.addCommand);
if (!assertsEnabled()) { if (!assertsEnabled()) {
......
...@@ -115,8 +115,7 @@ class CodesignCommand extends Command<void> { ...@@ -115,8 +115,7 @@ class CodesignCommand extends Command<void> {
revision = (processManager.runSync( revision = (processManager.runSync(
<String>['git', 'rev-parse', 'HEAD'], <String>['git', 'rev-parse', 'HEAD'],
workingDirectory: framework.checkoutDirectory.path, workingDirectory: framework.checkoutDirectory.path,
).stdout as String) ).stdout as String).trim();
.trim();
assert(revision.isNotEmpty); assert(revision.isNotEmpty);
} }
...@@ -291,7 +290,7 @@ class CodesignCommand extends Command<void> { ...@@ -291,7 +290,7 @@ class CodesignCommand extends Command<void> {
if (wrongEntitlementBinaries.isNotEmpty) { if (wrongEntitlementBinaries.isNotEmpty) {
stdio.printError( stdio.printError(
'Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:'); 'Found ${wrongEntitlementBinaries.length} binaries with unexpected entitlements:');
wrongEntitlementBinaries.forEach(print); wrongEntitlementBinaries.forEach(stdio.printError);
} }
if (unexpectedBinaries.isNotEmpty) { if (unexpectedBinaries.isNotEmpty) {
......
...@@ -20,6 +20,8 @@ const List<String> kReleaseChannels = <String>[ ...@@ -20,6 +20,8 @@ const List<String> kReleaseChannels = <String>[
const String kReleaseDocumentationUrl = 'https://github.com/flutter/flutter/wiki/Flutter-Cherrypick-Process'; 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( final RegExp releaseCandidateBranchRegex = RegExp(
r'flutter-(\d+)\.(\d+)-candidate\.(\d+)', 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 ...@@ -32,7 +32,7 @@ for SOURCE_FILE in $(ls "$DIR"/*.pb*.dart); do
"$DARTFMT" --overwrite --line-length 120 "$SOURCE_FILE" "$DARTFMT" --overwrite --line-length 120 "$SOURCE_FILE"
# Create temp copy with the license header prepended # 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 # Add an extra newline required by analysis (analysis also prevents
# license_header.txt from having the trailing newline) # license_header.txt from having the trailing newline)
......
...@@ -378,12 +378,13 @@ class ConductorState extends $pb.GeneratedMessage { ...@@ -378,12 +378,13 @@ class ConductorState extends $pb.GeneratedMessage {
protoName: 'lastUpdatedDate') protoName: 'lastUpdatedDate')
..pPS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'logs') ..pPS(8, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'logs')
..e<ReleasePhase>( ..e<ReleasePhase>(
9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'lastPhase', $pb.PbFieldType.OE, 9, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'currentPhase', $pb.PbFieldType.OE,
protoName: 'lastPhase', protoName: 'currentPhase',
defaultOrMaker: ReleasePhase.INITIALIZE, defaultOrMaker: ReleasePhase.APPLY_ENGINE_CHERRYPICKS,
valueOf: ReleasePhase.valueOf, valueOf: ReleasePhase.valueOf,
enumValues: ReleasePhase.values) 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; ..hasRequiredFields = false;
ConductorState._() : super(); ConductorState._() : super();
...@@ -395,7 +396,7 @@ class ConductorState extends $pb.GeneratedMessage { ...@@ -395,7 +396,7 @@ class ConductorState extends $pb.GeneratedMessage {
$fixnum.Int64 createdDate, $fixnum.Int64 createdDate,
$fixnum.Int64 lastUpdatedDate, $fixnum.Int64 lastUpdatedDate,
$core.Iterable<$core.String> logs, $core.Iterable<$core.String> logs,
ReleasePhase lastPhase, ReleasePhase currentPhase,
$core.String conductorVersion, $core.String conductorVersion,
}) { }) {
final _result = create(); final _result = create();
...@@ -420,8 +421,8 @@ class ConductorState extends $pb.GeneratedMessage { ...@@ -420,8 +421,8 @@ class ConductorState extends $pb.GeneratedMessage {
if (logs != null) { if (logs != null) {
_result.logs.addAll(logs); _result.logs.addAll(logs);
} }
if (lastPhase != null) { if (currentPhase != null) {
_result.lastPhase = lastPhase; _result.currentPhase = currentPhase;
} }
if (conductorVersion != null) { if (conductorVersion != null) {
_result.conductorVersion = conductorVersion; _result.conductorVersion = conductorVersion;
...@@ -531,16 +532,16 @@ class ConductorState extends $pb.GeneratedMessage { ...@@ -531,16 +532,16 @@ class ConductorState extends $pb.GeneratedMessage {
$core.List<$core.String> get logs => $_getList(6); $core.List<$core.String> get logs => $_getList(6);
@$pb.TagNumber(9) @$pb.TagNumber(9)
ReleasePhase get lastPhase => $_getN(7); ReleasePhase get currentPhase => $_getN(7);
@$pb.TagNumber(9) @$pb.TagNumber(9)
set lastPhase(ReleasePhase v) { set currentPhase(ReleasePhase v) {
setField(9, v); setField(9, v);
} }
@$pb.TagNumber(9) @$pb.TagNumber(9)
$core.bool hasLastPhase() => $_has(7); $core.bool hasCurrentPhase() => $_has(7);
@$pb.TagNumber(9) @$pb.TagNumber(9)
void clearLastPhase() => clearField(9); void clearCurrentPhase() => clearField(9);
@$pb.TagNumber(10) @$pb.TagNumber(10)
$core.String get conductorVersion => $_getSZ(8); $core.String get conductorVersion => $_getSZ(8);
......
...@@ -14,29 +14,29 @@ import 'dart:core' as $core; ...@@ -14,29 +14,29 @@ import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb; import 'package:protobuf/protobuf.dart' as $pb;
class ReleasePhase extends $pb.ProtobufEnum { 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 = 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 = 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._( 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 = 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 = 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 = 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>[ static const $core.List<ReleasePhase> values = <ReleasePhase>[
INITIALIZE,
APPLY_ENGINE_CHERRYPICKS, APPLY_ENGINE_CHERRYPICKS,
CODESIGN_ENGINE_BINARIES, CODESIGN_ENGINE_BINARIES,
APPLY_FRAMEWORK_CHERRYPICKS, APPLY_FRAMEWORK_CHERRYPICKS,
PUBLISH_VERSION, PUBLISH_VERSION,
PUBLISH_CHANNEL, PUBLISH_CHANNEL,
VERIFY_RELEASE, VERIFY_RELEASE,
RELEASE_COMPLETED,
]; ];
static final $core.Map<$core.int, ReleasePhase> _byValue = $pb.ProtobufEnum.initByValue(values); static final $core.Map<$core.int, ReleasePhase> _byValue = $pb.ProtobufEnum.initByValue(values);
......
...@@ -17,19 +17,19 @@ import 'dart:typed_data' as $typed_data; ...@@ -17,19 +17,19 @@ import 'dart:typed_data' as $typed_data;
const ReleasePhase$json = const { const ReleasePhase$json = const {
'1': 'ReleasePhase', '1': 'ReleasePhase',
'2': const [ '2': const [
const {'1': 'INITIALIZE', '2': 0}, const {'1': 'APPLY_ENGINE_CHERRYPICKS', '2': 0},
const {'1': 'APPLY_ENGINE_CHERRYPICKS', '2': 1}, const {'1': 'CODESIGN_ENGINE_BINARIES', '2': 1},
const {'1': 'CODESIGN_ENGINE_BINARIES', '2': 2}, const {'1': 'APPLY_FRAMEWORK_CHERRYPICKS', '2': 2},
const {'1': 'APPLY_FRAMEWORK_CHERRYPICKS', '2': 3}, const {'1': 'PUBLISH_VERSION', '2': 3},
const {'1': 'PUBLISH_VERSION', '2': 4}, const {'1': 'PUBLISH_CHANNEL', '2': 4},
const {'1': 'PUBLISH_CHANNEL', '2': 5}, const {'1': 'VERIFY_RELEASE', '2': 5},
const {'1': 'VERIFY_RELEASE', '2': 6}, const {'1': 'RELEASE_COMPLETED', '2': 6},
], ],
}; };
/// Descriptor for `ReleasePhase`. Decode as a `google.protobuf.EnumDescriptorProto`. /// Descriptor for `ReleasePhase`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List releasePhaseDescriptor = $convert.base64Decode( final $typed_data.Uint8List releasePhaseDescriptor = $convert.base64Decode(
'CgxSZWxlYXNlUGhhc2USDgoKSU5JVElBTElaRRAAEhwKGEFQUExZX0VOR0lORV9DSEVSUllQSUNLUxABEhwKGENPREVTSUdOX0VOR0lORV9CSU5BUklFUxACEh8KG0FQUExZX0ZSQU1FV09SS19DSEVSUllQSUNLUxADEhMKD1BVQkxJU0hfVkVSU0lPThAEEhMKD1BVQkxJU0hfQ0hBTk5FTBAFEhIKDlZFUklGWV9SRUxFQVNFEAY='); 'CgxSZWxlYXNlUGhhc2USHAoYQVBQTFlfRU5HSU5FX0NIRVJSWVBJQ0tTEAASHAoYQ09ERVNJR05fRU5HSU5FX0JJTkFSSUVTEAESHwobQVBQTFlfRlJBTUVXT1JLX0NIRVJSWVBJQ0tTEAISEwoPUFVCTElTSF9WRVJTSU9OEAMSEwoPUFVCTElTSF9DSEFOTkVMEAQSEgoOVkVSSUZZX1JFTEVBU0UQBRIVChFSRUxFQVNFX0NPTVBMRVRFRBAG');
@$core.Deprecated('Use cherrypickStateDescriptor instead') @$core.Deprecated('Use cherrypickStateDescriptor instead')
const CherrypickState$json = const { const CherrypickState$json = const {
'1': 'CherrypickState', '1': 'CherrypickState',
...@@ -98,11 +98,11 @@ const ConductorState$json = const { ...@@ -98,11 +98,11 @@ const ConductorState$json = const {
const {'1': 'createdDate', '3': 6, '4': 1, '5': 3, '10': 'createdDate'}, const {'1': 'createdDate', '3': 6, '4': 1, '5': 3, '10': 'createdDate'},
const {'1': 'lastUpdatedDate', '3': 7, '4': 1, '5': 3, '10': 'lastUpdatedDate'}, const {'1': 'lastUpdatedDate', '3': 7, '4': 1, '5': 3, '10': 'lastUpdatedDate'},
const {'1': 'logs', '3': 8, '4': 3, '5': 9, '10': 'logs'}, 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': 'currentPhase', '3': 9, '4': 1, '5': 14, '6': '.conductor_state.ReleasePhase', '10': 'currentPhase'},
const {'1': 'conductor_version', '3': 10, '4': 1, '5': 9, '10': 'conductorVersion'}, const {'1': 'conductorVersion', '3': 10, '4': 1, '5': 9, '10': 'conductorVersion'},
], ],
}; };
/// Descriptor for `ConductorState`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ConductorState`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List conductorStateDescriptor = $convert.base64Decode( final $typed_data.Uint8List conductorStateDescriptor = $convert.base64Decode(
'Cg5Db25kdWN0b3JTdGF0ZRImCg5yZWxlYXNlQ2hhbm5lbBgBIAEoCVIOcmVsZWFzZUNoYW5uZWwSJgoOcmVsZWFzZVZlcnNpb24YAiABKAlSDnJlbGVhc2VWZXJzaW9uEjMKBmVuZ2luZRgEIAEoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5SZXBvc2l0b3J5UgZlbmdpbmUSOQoJZnJhbWV3b3JrGAUgASgLMhsuY29uZHVjdG9yX3N0YXRlLlJlcG9zaXRvcnlSCWZyYW1ld29yaxIgCgtjcmVhdGVkRGF0ZRgGIAEoA1ILY3JlYXRlZERhdGUSKAoPbGFzdFVwZGF0ZWREYXRlGAcgASgDUg9sYXN0VXBkYXRlZERhdGUSEgoEbG9ncxgIIAMoCVIEbG9ncxI7CglsYXN0UGhhc2UYCSABKA4yHS5jb25kdWN0b3Jfc3RhdGUuUmVsZWFzZVBoYXNlUglsYXN0UGhhc2USKwoRY29uZHVjdG9yX3ZlcnNpb24YCiABKAlSEGNvbmR1Y3RvclZlcnNpb24='); 'Cg5Db25kdWN0b3JTdGF0ZRImCg5yZWxlYXNlQ2hhbm5lbBgBIAEoCVIOcmVsZWFzZUNoYW5uZWwSJgoOcmVsZWFzZVZlcnNpb24YAiABKAlSDnJlbGVhc2VWZXJzaW9uEjMKBmVuZ2luZRgEIAEoCzIbLmNvbmR1Y3Rvcl9zdGF0ZS5SZXBvc2l0b3J5UgZlbmdpbmUSOQoJZnJhbWV3b3JrGAUgASgLMhsuY29uZHVjdG9yX3N0YXRlLlJlcG9zaXRvcnlSCWZyYW1ld29yaxIgCgtjcmVhdGVkRGF0ZRgGIAEoA1ILY3JlYXRlZERhdGUSKAoPbGFzdFVwZGF0ZWREYXRlGAcgASgDUg9sYXN0VXBkYXRlZERhdGUSEgoEbG9ncxgIIAMoCVIEbG9ncxJBCgxjdXJyZW50UGhhc2UYCSABKA4yHS5jb25kdWN0b3Jfc3RhdGUuUmVsZWFzZVBoYXNlUgxjdXJyZW50UGhhc2USKgoQY29uZHVjdG9yVmVyc2lvbhgKIAEoCVIQY29uZHVjdG9yVmVyc2lvbg==');
...@@ -10,21 +10,23 @@ message Remote { ...@@ -10,21 +10,23 @@ message Remote {
enum ReleasePhase { enum ReleasePhase {
// Release was started with `conductor start` and repositories cloned. // Release was started with `conductor start` and repositories cloned.
INITIALIZE = 0; APPLY_ENGINE_CHERRYPICKS = 0;
APPLY_ENGINE_CHERRYPICKS = 1; CODESIGN_ENGINE_BINARIES = 1;
CODESIGN_ENGINE_BINARIES = 2; APPLY_FRAMEWORK_CHERRYPICKS = 2;
APPLY_FRAMEWORK_CHERRYPICKS = 3;
// Git tag applied to framework RC branch HEAD and pushed upstream. // 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. // RC branch HEAD pushed to upstream release branch.
// //
// For example, flutter-1.2-candidate.3 -> upstream/beta // For example, flutter-1.2-candidate.3 -> upstream/beta
PUBLISH_CHANNEL = 5; PUBLISH_CHANNEL = 4;
// Package artifacts verified to exist on cloud storage. // 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 { enum CherrypickState {
...@@ -98,9 +100,9 @@ message ConductorState { ...@@ -98,9 +100,9 @@ message ConductorState {
repeated string logs = 8; repeated string logs = 8;
// The last [ReleasePhase] that was successfully completed. // The current [ReleasePhase] that has yet to be completed.
ReleasePhase lastPhase = 9; ReleasePhase currentPhase = 9;
// Commit hash of the Conductor tool. // Commit hash of the Conductor tool.
string conductor_version = 10; string conductorVersion = 10;
} }
This diff is collapsed.
...@@ -13,7 +13,7 @@ import './stdio.dart'; ...@@ -13,7 +13,7 @@ import './stdio.dart';
import './version.dart'; import './version.dart';
const String kIncrement = 'increment'; const String kIncrement = 'increment';
const String kCommit = 'commit'; const String kCandidateBranch = 'candidate-branch';
const String kRemoteName = 'remote'; const String kRemoteName = 'remote';
const String kJustPrint = 'just-print'; const String kJustPrint = 'just-print';
const String kYes = 'yes'; const String kYes = 'yes';
...@@ -40,9 +40,9 @@ class RollDevCommand extends Command<void> { ...@@ -40,9 +40,9 @@ class RollDevCommand extends Command<void> {
}, },
); );
argParser.addOption( argParser.addOption(
kCommit, kCandidateBranch,
help: 'Specifies which git commit to roll to the dev branch. Required.', help: 'Specifies which git branch to roll to the dev branch. Required.',
valueHelp: 'hash', valueHelp: 'branch',
defaultsTo: null, // This option is required defaultsTo: null, // This option is required
); );
argParser.addFlag( argParser.addFlag(
...@@ -112,17 +112,16 @@ bool rollDev({ ...@@ -112,17 +112,16 @@ bool rollDev({
}) { }) {
final String remoteName = argResults[kRemoteName] as String; final String remoteName = argResults[kRemoteName] as String;
final String level = argResults[kIncrement] 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 justPrint = argResults[kJustPrint] as bool;
final bool autoApprove = argResults[kYes] as bool; final bool autoApprove = argResults[kYes] as bool;
final bool force = argResults[kForce] as bool; final bool force = argResults[kForce] as bool;
final bool skipTagging = argResults[kSkipTagging] as bool; final bool skipTagging = argResults[kSkipTagging] as bool;
if (level == null || commit == null) { if (level == null || candidateBranch == null) {
stdio.printStatus( throw Exception(
'roll_dev.dart --increment=level --commit=hash • update the version tags ' 'roll_dev.dart --$kIncrement=level --$kCandidateBranch=branch • update the version tags '
'and roll a new dev build.\n$usage'); 'and roll a new dev build.\n$usage');
return false;
} }
final String remoteUrl = repository.remoteUrl(remoteName); final String remoteUrl = repository.remoteUrl(remoteName);
...@@ -136,14 +135,16 @@ bool rollDev({ ...@@ -136,14 +135,16 @@ bool rollDev({
repository.fetch(remoteName); repository.fetch(remoteName);
// Verify [commit] is valid // Verify [commit] is valid
repository.reverseParse(commit); final String commit = repository.reverseParse(candidateBranch);
stdio.printStatus('remoteName is $remoteName'); stdio.printStatus('remoteName is $remoteName');
final Version lastVersion = // Get the name of the last dev release
Version.fromString(repository.getFullTag(remoteName)); final Version lastVersion = Version.fromString(
repository.getFullTag(remoteName, 'dev'),
);
final Version version = final Version version =
skipTagging ? lastVersion : Version.increment(lastVersion, level); skipTagging ? lastVersion : Version.fromCandidateBranch(candidateBranch);
final String tagName = version.toString(); final String tagName = version.toString();
if (repository.reverseParse(lastVersion.toString()).contains(commit.trim())) { if (repository.reverseParse(lastVersion.toString()).contains(commit.trim())) {
......
...@@ -4,8 +4,6 @@ ...@@ -4,8 +4,6 @@
// @dart = 2.8 // @dart = 2.8
import 'dart:convert' show jsonEncode;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
...@@ -20,6 +18,7 @@ import './proto/conductor_state.pbenum.dart' show ReleasePhase; ...@@ -20,6 +18,7 @@ import './proto/conductor_state.pbenum.dart' show ReleasePhase;
import './repository.dart'; import './repository.dart';
import './state.dart'; import './state.dart';
import './stdio.dart'; import './stdio.dart';
import './version.dart';
const String kCandidateOption = 'candidate-branch'; const String kCandidateOption = 'candidate-branch';
const String kDartRevisionOption = 'dart-revision'; const String kDartRevisionOption = 'dart-revision';
...@@ -28,6 +27,7 @@ const String kEngineUpstreamOption = 'engine-upstream'; ...@@ -28,6 +27,7 @@ const String kEngineUpstreamOption = 'engine-upstream';
const String kFrameworkCherrypicksOption = 'framework-cherrypicks'; const String kFrameworkCherrypicksOption = 'framework-cherrypicks';
const String kFrameworkMirrorOption = 'framework-mirror'; const String kFrameworkMirrorOption = 'framework-mirror';
const String kFrameworkUpstreamOption = 'framework-upstream'; const String kFrameworkUpstreamOption = 'framework-upstream';
const String kIncrementOption = 'increment';
const String kEngineMirrorOption = 'engine-mirror'; const String kEngineMirrorOption = 'engine-mirror';
const String kReleaseOption = 'release-channel'; const String kReleaseOption = 'release-channel';
const String kStateOption = 'state-file'; const String kStateOption = 'state-file';
...@@ -91,6 +91,18 @@ class StartCommand extends Command<void> { ...@@ -91,6 +91,18 @@ class StartCommand extends Command<void> {
kDartRevisionOption, kDartRevisionOption,
help: 'New Dart revision to cherrypick.', 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); final Git git = Git(processManager);
conductorVersion = git.getOutput( conductorVersion = git.getOutput(
<String>['rev-parse', 'HEAD'], <String>['rev-parse', 'HEAD'],
...@@ -183,6 +195,12 @@ class StartCommand extends Command<void> { ...@@ -183,6 +195,12 @@ class StartCommand extends Command<void> {
platform.environment, platform.environment,
allowNull: true, allowNull: true,
); );
final String incrementLetter = getValueFromEnvOrArgs(
kIncrementOption,
argResults,
platform.environment,
);
if (!releaseCandidateBranchRegex.hasMatch(candidateBranch)) { if (!releaseCandidateBranchRegex.hasMatch(candidateBranch)) {
throw ConductorException( throw ConductorException(
'Invalid release candidate branch "$candidateBranch". Text should ' 'Invalid release candidate branch "$candidateBranch". Text should '
...@@ -200,11 +218,11 @@ class StartCommand extends Command<void> { ...@@ -200,11 +218,11 @@ class StartCommand extends Command<void> {
final EngineRepository engine = EngineRepository( final EngineRepository engine = EngineRepository(
checkouts, checkouts,
initialRef: candidateBranch, initialRef: candidateBranch,
fetchRemote: Remote( upstreamRemote: Remote(
name: RemoteName.upstream, name: RemoteName.upstream,
url: engineUpstream, url: engineUpstream,
), ),
pushRemote: Remote( mirrorRemote: Remote(
name: RemoteName.mirror, name: RemoteName.mirror,
url: engineMirror, url: engineMirror,
), ),
...@@ -249,15 +267,17 @@ class StartCommand extends Command<void> { ...@@ -249,15 +267,17 @@ class StartCommand extends Command<void> {
checkoutPath: engine.checkoutDirectory.path, checkoutPath: engine.checkoutDirectory.path,
cherrypicks: engineCherrypicks, cherrypicks: engineCherrypicks,
dartRevision: dartRevision, dartRevision: dartRevision,
upstream: pb.Remote(name: 'upstream', url: engine.upstreamRemote.url),
mirror: pb.Remote(name: 'mirror', url: engine.mirrorRemote.url),
); );
final FrameworkRepository framework = FrameworkRepository( final FrameworkRepository framework = FrameworkRepository(
checkouts, checkouts,
initialRef: candidateBranch, initialRef: candidateBranch,
fetchRemote: Remote( upstreamRemote: Remote(
name: RemoteName.upstream, name: RemoteName.upstream,
url: frameworkUpstream, url: frameworkUpstream,
), ),
pushRemote: Remote( mirrorRemote: Remote(
name: RemoteName.mirror, name: RemoteName.mirror,
url: frameworkMirror, url: frameworkMirror,
), ),
...@@ -287,6 +307,16 @@ class StartCommand extends Command<void> { ...@@ -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'); final String frameworkHead = framework.reverseParse('HEAD');
state.framework = pb.Repository( state.framework = pb.Repository(
candidateBranch: candidateBranch, candidateBranch: candidateBranch,
...@@ -294,20 +324,17 @@ class StartCommand extends Command<void> { ...@@ -294,20 +324,17 @@ class StartCommand extends Command<void> {
currentGitHead: frameworkHead, currentGitHead: frameworkHead,
checkoutPath: framework.checkoutDirectory.path, checkoutPath: framework.checkoutDirectory.path,
cherrypicks: frameworkCherrypicks, 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; state.conductorVersion = conductorVersion;
stdio.printTrace('Writing state to file ${stateFile.path}...'); stdio.printTrace('Writing state to file ${stateFile.path}...');
state.logs.addAll(stdio.logs); writeStateToFile(stateFile, state, stdio.logs);
stateFile.writeAsStringSync(
jsonEncode(state.toProto3Json()),
flush: true,
);
stdio.printStatus(presentState(state)); stdio.printStatus(presentState(state));
} }
...@@ -340,8 +367,8 @@ class StartCommand extends Command<void> { ...@@ -340,8 +367,8 @@ class StartCommand extends Command<void> {
} }
final String branchPoint = repository.branchPoint( final String branchPoint = repository.branchPoint(
'${repository.fetchRemote.name}/$upstreamRef', '${repository.upstreamRemote.name}/$upstreamRef',
'${repository.fetchRemote.name}/$releaseRef', '${repository.upstreamRemote.name}/$releaseRef',
); );
// `git rev-list` returns newest first, so reverse this list // `git rev-list` returns newest first, so reverse this list
......
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
// @dart = 2.8 // @dart = 2.8
import 'dart:convert' show jsonDecode, jsonEncode;
import 'package:file/file.dart' show File;
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import './globals.dart'; import './globals.dart';
...@@ -37,6 +40,7 @@ String presentState(pb.ConductorState state) { ...@@ -37,6 +40,7 @@ String presentState(pb.ConductorState state) {
final StringBuffer buffer = StringBuffer(); final StringBuffer buffer = StringBuffer();
buffer.writeln('Conductor version: ${state.conductorVersion}'); buffer.writeln('Conductor version: ${state.conductorVersion}');
buffer.writeln('Release channel: ${state.releaseChannel}'); buffer.writeln('Release channel: ${state.releaseChannel}');
buffer.writeln('Release version: ${state.releaseVersion}');
buffer.writeln(''); buffer.writeln('');
buffer.writeln( buffer.writeln(
'Release started at: ${DateTime.fromMillisecondsSinceEpoch(state.createdDate.toInt())}'); 'Release started at: ${DateTime.fromMillisecondsSinceEpoch(state.createdDate.toInt())}');
...@@ -76,14 +80,14 @@ String presentState(pb.ConductorState state) { ...@@ -76,14 +80,14 @@ String presentState(pb.ConductorState state) {
buffer.writeln('0 Framework cherrypicks.'); buffer.writeln('0 Framework cherrypicks.');
} }
buffer.writeln(''); buffer.writeln('');
if (state.lastPhase == ReleasePhase.VERIFY_RELEASE) { if (state.currentPhase == ReleasePhase.VERIFY_RELEASE) {
buffer.writeln( buffer.writeln(
'${state.releaseChannel} release ${state.releaseVersion} has been published and verified.\n', '${state.releaseChannel} release ${state.releaseVersion} has been published and verified.\n',
); );
return buffer.toString(); return buffer.toString();
} }
buffer.writeln('The next step is:'); buffer.writeln('The current phase is:');
buffer.writeln(presentPhases(state.lastPhase)); buffer.writeln(presentPhases(state.currentPhase));
buffer.writeln(phaseInstructions(state)); buffer.writeln(phaseInstructions(state));
buffer.writeln(''); buffer.writeln('');
...@@ -91,15 +95,14 @@ String presentState(pb.ConductorState state) { ...@@ -91,15 +95,14 @@ String presentState(pb.ConductorState state) {
return buffer.toString(); return buffer.toString();
} }
String presentPhases(ReleasePhase lastPhase) { String presentPhases(ReleasePhase currentPhase) {
final ReleasePhase nextPhase = getNextPhase(lastPhase);
final StringBuffer buffer = StringBuffer(); final StringBuffer buffer = StringBuffer();
bool phaseCompleted = true; bool phaseCompleted = true;
for (final ReleasePhase phase in ReleasePhase.values) { for (final ReleasePhase phase in ReleasePhase.values) {
if (phase == nextPhase) { if (phase == currentPhase) {
// This phase will execute the next time `conductor next` is run. // This phase will execute the next time `conductor next` is run.
buffer.writeln('> ${phase.name} (next)'); buffer.writeln('> ${phase.name} (current)');
phaseCompleted = false; phaseCompleted = false;
} else if (phaseCompleted) { } else if (phaseCompleted) {
// This phase was already completed. // This phase was already completed.
...@@ -113,8 +116,8 @@ String presentPhases(ReleasePhase lastPhase) { ...@@ -113,8 +116,8 @@ String presentPhases(ReleasePhase lastPhase) {
} }
String phaseInstructions(pb.ConductorState state) { String phaseInstructions(pb.ConductorState state) {
switch (state.lastPhase) { switch (state.currentPhase) {
case ReleasePhase.INITIALIZE: case ReleasePhase.APPLY_ENGINE_CHERRYPICKS:
if (state.engine.cherrypicks.isEmpty) { if (state.engine.cherrypicks.isEmpty) {
return <String>[ return <String>[
'There are no engine cherrypicks, so issue `conductor next` to continue', 'There are no engine cherrypicks, so issue `conductor next` to continue',
...@@ -128,31 +131,33 @@ String phaseInstructions(pb.ConductorState state) { ...@@ -128,31 +131,33 @@ String phaseInstructions(pb.ConductorState state) {
'\t${cherrypick.trunkRevision}', '\t${cherrypick.trunkRevision}',
'See $kReleaseDocumentationUrl for more information.', 'See $kReleaseDocumentationUrl for more information.',
].join('\n'); ].join('\n');
case ReleasePhase.APPLY_ENGINE_CHERRYPICKS: case ReleasePhase.CODESIGN_ENGINE_BINARIES:
return <String>[ return <String>[
'You must verify Engine CI builds are successful and then codesign the', 'You must verify Engine CI builds are successful and then codesign the',
'binaries at revision ${state.engine.currentGitHead}.', 'binaries at revision ${state.engine.currentGitHead}.',
].join('\n'); ].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>[ return <String>[
'You must now manually apply the following framework cherrypicks to the checkout', 'You must now manually apply the following framework cherrypicks to the checkout',
'at ${state.framework.checkoutPath} in order:', '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}', '\t${cherrypick.trunkRevision}',
].join('\n'); ].join('\n');
case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS: case ReleasePhase.PUBLISH_VERSION:
return <String>[ return <String>[
'You must verify Framework CI builds are successful.', 'You must verify Framework CI builds are successful.',
'See $kReleaseDocumentationUrl for more information.', 'See $kReleaseDocumentationUrl for more information.',
].join('\n'); ].join('\n');
case ReleasePhase.PUBLISH_VERSION:
return 'Issue `conductor next` to publish your release to the release branch.';
case ReleasePhase.PUBLISH_CHANNEL: case ReleasePhase.PUBLISH_CHANNEL:
return <String>[ return 'Issue `conductor next` to publish your release to the release branch.';
'Release archive packages must be verified on cloud storage. Issue',
'`conductor next` to check if they are ready.',
].join('\n');
case ReleasePhase.VERIFY_RELEASE: case ReleasePhase.VERIFY_RELEASE:
return 'Release archive packages must be verified on cloud storage.';
case ReleasePhase.RELEASE_COMPLETED:
return 'This release has been completed.'; return 'This release has been completed.';
} }
assert(false); assert(false);
...@@ -161,12 +166,29 @@ String phaseInstructions(pb.ConductorState state) { ...@@ -161,12 +166,29 @@ String phaseInstructions(pb.ConductorState state) {
/// Returns the next phase in the ReleasePhase enum. /// 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. /// passed as an argument, as there is no next phase.
ReleasePhase getNextPhase(ReleasePhase previousPhase) { ReleasePhase getNextPhase(ReleasePhase previousPhase) {
assert(previousPhase != null); assert(previousPhase != null);
if (previousPhase == ReleasePhase.VERIFY_RELEASE) { if (previousPhase == ReleasePhase.RELEASE_COMPLETED) {
throw ConductorException('There is no next ReleasePhase!'); throw ConductorException('There is no next ReleasePhase!');
} }
return ReleasePhase.valueOf(previousPhase.value + 1); 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 @@ ...@@ -4,8 +4,6 @@
// @dart = 2.8 // @dart = 2.8
import 'dart:convert' show jsonDecode;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
...@@ -59,8 +57,8 @@ class StatusCommand extends Command<void> { ...@@ -59,8 +57,8 @@ class StatusCommand extends Command<void> {
'No persistent state file found at ${argResults[kStateOption]}.'); 'No persistent state file found at ${argResults[kStateOption]}.');
return; return;
} }
final pb.ConductorState state = pb.ConductorState(); final pb.ConductorState state = readStateFromFile(stateFile);
state.mergeFromProto3Json(jsonDecode(stateFile.readAsStringSync()));
stdio.printStatus(presentState(state)); stdio.printStatus(presentState(state));
if (argResults[kVerboseFlag] as bool) { if (argResults[kVerboseFlag] as bool) {
stdio.printStatus('\nLogs:'); stdio.printStatus('\nLogs:');
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import './globals.dart' show ConductorException;
/// Possible string formats that `flutter --version` can return. /// Possible string formats that `flutter --version` can return.
enum VersionType { enum VersionType {
/// A stable flutter release. /// A stable flutter release.
...@@ -20,12 +22,18 @@ enum VersionType { ...@@ -20,12 +22,18 @@ enum VersionType {
/// ///
/// The last number is the number of commits past the last tagged version. /// The last number is the number of commits past the last tagged version.
latest, 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>{ final Map<VersionType, RegExp> versionPatterns = <VersionType, RegExp>{
VersionType.stable: RegExp(r'^(\d+)\.(\d+)\.(\d+)$'), VersionType.stable: RegExp(r'^(\d+)\.(\d+)\.(\d+)$'),
VersionType.development: RegExp(r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre$'), VersionType.development: RegExp(r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre$'),
VersionType.latest: RegExp(r'^(\d+)\.(\d+)\.(\d+)-(\d+)\.(\d+)\.pre\.(\d+)$'), 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 { class Version {
...@@ -54,6 +62,10 @@ class Version { ...@@ -54,6 +62,10 @@ class Version {
assert(n != null); assert(n != null);
assert(commits != null); assert(commits != null);
break; break;
case VersionType.gitDescribe:
throw ConductorException(
'VersionType.gitDescribe not supported! Use VersionType.latest instead.',
);
} }
} }
...@@ -115,6 +127,24 @@ class Version { ...@@ -115,6 +127,24 @@ class Version {
type: VersionType.latest, 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'); throw Exception('${versionString.trim()} cannot be parsed');
} }
...@@ -131,7 +161,7 @@ class Version { ...@@ -131,7 +161,7 @@ class Version {
int? nextM = previousVersion.m; int? nextM = previousVersion.m;
int? nextN = previousVersion.n; int? nextN = previousVersion.n;
if (nextVersionType == null) { if (nextVersionType == null) {
if (previousVersion.type == VersionType.latest) { if (previousVersion.type == VersionType.latest || previousVersion.type == VersionType.gitDescribe) {
nextVersionType = VersionType.development; nextVersionType = VersionType.development;
} else { } else {
nextVersionType = previousVersion.type; nextVersionType = previousVersion.type;
...@@ -157,10 +187,7 @@ class Version { ...@@ -157,10 +187,7 @@ class Version {
nextZ += 1; nextZ += 1;
break; break;
case 'm': case 'm':
// Regular dev release. assert(false, "Do not increment 'm' via Version.increment, use instead Version.fromCandidateBranch()");
assert(previousVersion.type == VersionType.development);
nextM = nextM! + 1;
nextN = 0;
break; break;
case 'n': case 'n':
// Hotfix to internal roll. // Hotfix to internal roll.
...@@ -179,6 +206,31 @@ class Version { ...@@ -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. /// Major version.
final int x; final int x;
...@@ -208,6 +260,8 @@ class Version { ...@@ -208,6 +260,8 @@ class Version {
return '$x.$y.$z-$m.$n.pre'; return '$x.$y.$z-$m.$n.pre';
case VersionType.latest: case VersionType.latest:
return '$x.$y.$z-$m.$n.pre.$commits'; return '$x.$y.$z-$m.$n.pre.$commits';
case VersionType.gitDescribe:
return '$x.$y.$z-$m.$n.pre.$commits';
} }
} }
} }
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:conductor/codesign.dart'; import 'package:conductor/codesign.dart';
import 'package:conductor/globals.dart';
import 'package:conductor/repository.dart'; import 'package:conductor/repository.dart';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
...@@ -16,7 +15,7 @@ import '../../../packages/flutter_tools/test/src/fake_process_manager.dart'; ...@@ -16,7 +15,7 @@ import '../../../packages/flutter_tools/test/src/fake_process_manager.dart';
void main() { void main() {
group('codesign command', () { group('codesign command', () {
const String flutterRoot = '/flutter'; const String flutterRoot = '/flutter';
const String checkoutsParentDirectory = '$flutterRoot/dev/tools/'; const String checkoutsParentDirectory = '$flutterRoot/dev/conductor/';
const String flutterCache = const String flutterCache =
'${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache'; '${checkoutsParentDirectory}flutter_conductor_checkouts/framework/bin/cache';
const String flutterBin = const String flutterBin =
...@@ -387,18 +386,13 @@ void main() { ...@@ -387,18 +386,13 @@ void main() {
stdout: 'application/x-mach-binary', stdout: 'application/x-mach-binary',
), ),
]); ]);
try { await runner.run(<String>[
await runner.run(<String>[ 'codesign',
'codesign', '--$kVerify',
'--$kVerify', '--no-$kSignatures',
'--no-$kSignatures', '--$kRevision',
'--$kRevision', revision,
revision, ]);
]);
} on ConductorException {
//print(stdio.error);
rethrow;
}
expect( expect(
processManager.hasRemainingExpectations, processManager.hasRemainingExpectations,
false, false,
......
...@@ -9,6 +9,16 @@ import 'package:test/test.dart'; ...@@ -9,6 +9,16 @@ import 'package:test/test.dart';
export 'package:test/test.dart' hide isInstanceOf; 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) { Matcher throwsExceptionWith(String messageSubString) {
return throwsA( return throwsA(
isA<Exception>().having( isA<Exception>().having(
...@@ -28,11 +38,12 @@ class TestStdio extends Stdio { ...@@ -28,11 +38,12 @@ class TestStdio extends Stdio {
String get error => logs.where((String log) => log.startsWith(r'[error] ')).join('\n'); String get error => logs.where((String log) => log.startsWith(r'[error] ')).join('\n');
String get stdout => logs.where((String log) { 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'); }).join('\n');
final bool verbose; final bool verbose;
late final List<String> _stdin; late final List<String> _stdin;
List<String> get stdin => _stdin;
@override @override
String readLineSync() { String readLineSync() {
...@@ -46,7 +57,7 @@ class TestStdio extends Stdio { ...@@ -46,7 +57,7 @@ class TestStdio extends Stdio {
class FakeArgResults implements ArgResults { class FakeArgResults implements ArgResults {
FakeArgResults({ FakeArgResults({
required String level, required String level,
required String commit, required String candidateBranch,
String remote = 'upstream', String remote = 'upstream',
bool justPrint = false, bool justPrint = false,
bool autoApprove = true, // so we don't have to mock stdin bool autoApprove = true, // so we don't have to mock stdin
...@@ -55,7 +66,7 @@ class FakeArgResults implements ArgResults { ...@@ -55,7 +66,7 @@ class FakeArgResults implements ArgResults {
bool skipTagging = false, bool skipTagging = false,
}) : _parsedArgs = <String, dynamic>{ }) : _parsedArgs = <String, dynamic>{
'increment': level, 'increment': level,
'commit': commit, 'candidate-branch': candidateBranch,
'remote': remote, 'remote': remote,
'just-print': justPrint, 'just-print': justPrint,
'yes': autoApprove, '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() { ...@@ -20,7 +20,8 @@ void main() {
const String commit = 'abcde012345'; const String commit = 'abcde012345';
const String remote = 'origin'; const String remote = 'origin';
const String lastVersion = '1.2.0-0.0.pre'; 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/'; const String checkoutsParentDirectory = '/path/to/directory/';
FakeArgResults fakeArgResults; FakeArgResults fakeArgResults;
MemoryFileSystem fileSystem; MemoryFileSystem fileSystem;
...@@ -45,37 +46,20 @@ void main() { ...@@ -45,37 +46,20 @@ void main() {
repo = FrameworkRepository(checkouts); repo = FrameworkRepository(checkouts);
}); });
test('returns false if level not provided', () { test('throws Exception if level not provided', () {
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: null, level: null,
commit: commit, candidateBranch: candidateBranch,
remote: remote, remote: remote,
); );
expect( expect(
rollDev( () => 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(
argResults: fakeArgResults, argResults: fakeArgResults,
repository: repo, repository: repo,
stdio: stdio, stdio: stdio,
usage: usage, usage: usage,
), ),
false, throwsExceptionWith(usage),
); );
}); });
...@@ -109,24 +93,18 @@ void main() { ...@@ -109,24 +93,18 @@ void main() {
]); ]);
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, candidateBranch: candidateBranch,
remote: remote, remote: remote,
); );
Exception exception; expect(
try { () => rollDev(
rollDev(
argResults: fakeArgResults, argResults: fakeArgResults,
repository: repo, repository: repo,
stdio: stdio, stdio: stdio,
usage: usage, usage: usage,
); ),
} on Exception catch (e) { throwsExceptionWith('Your git repository is not clean.'),
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));
}); });
test('does not reset or tag if --just-print is specified', () { test('does not reset or tag if --just-print is specified', () {
...@@ -165,13 +143,13 @@ void main() { ...@@ -165,13 +143,13 @@ void main() {
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'rev-parse', 'rev-parse',
commit, candidateBranch,
], stdout: commit), ], stdout: commit),
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'describe', 'describe',
'--match', '--match',
'*.*.*-*.*.pre', '*.*.*',
'--exact-match', '--exact-match',
'--tags', '--tags',
'refs/remotes/$remote/dev', 'refs/remotes/$remote/dev',
...@@ -185,7 +163,7 @@ void main() { ...@@ -185,7 +163,7 @@ void main() {
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, candidateBranch: candidateBranch,
remote: remote, remote: remote,
justPrint: true, justPrint: true,
); );
...@@ -237,13 +215,13 @@ void main() { ...@@ -237,13 +215,13 @@ void main() {
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'rev-parse', 'rev-parse',
commit, candidateBranch,
], stdout: commit), ], stdout: commit),
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'describe', 'describe',
'--match', '--match',
'*.*.*-*.*.pre', '*.*.*',
'--exact-match', '--exact-match',
'--tags', '--tags',
'refs/remotes/$remote/dev', 'refs/remotes/$remote/dev',
...@@ -268,7 +246,7 @@ void main() { ...@@ -268,7 +246,7 @@ void main() {
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, candidateBranch: candidateBranch,
remote: remote, remote: remote,
skipTagging: true, skipTagging: true,
); );
...@@ -319,13 +297,13 @@ void main() { ...@@ -319,13 +297,13 @@ void main() {
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'rev-parse', 'rev-parse',
commit, candidateBranch,
], stdout: commit), ], stdout: commit),
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'describe', 'describe',
'--match', '--match',
'*.*.*-*.*.pre', '*.*.*',
'--exact-match', '--exact-match',
'--tags', '--tags',
'refs/remotes/$remote/dev', 'refs/remotes/$remote/dev',
...@@ -339,7 +317,7 @@ void main() { ...@@ -339,7 +317,7 @@ void main() {
]); ]);
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, candidateBranch: candidateBranch,
remote: remote, remote: remote,
justPrint: true, justPrint: true,
); );
...@@ -394,13 +372,13 @@ void main() { ...@@ -394,13 +372,13 @@ void main() {
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'rev-parse', 'rev-parse',
commit, candidateBranch,
], stdout: commit), ], stdout: commit),
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'describe', 'describe',
'--match', '--match',
'*.*.*-*.*.pre', '*.*.*',
'--exact-match', '--exact-match',
'--tags', '--tags',
'refs/remotes/$remote/dev', 'refs/remotes/$remote/dev',
...@@ -421,7 +399,7 @@ void main() { ...@@ -421,7 +399,7 @@ void main() {
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, candidateBranch: candidateBranch,
remote: remote, remote: remote,
); );
const String errorMessage = 'The previous dev tag $lastVersion is not a ' const String errorMessage = 'The previous dev tag $lastVersion is not a '
...@@ -473,13 +451,13 @@ void main() { ...@@ -473,13 +451,13 @@ void main() {
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'rev-parse', 'rev-parse',
commit, candidateBranch,
], stdout: commit), ], stdout: commit),
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'describe', 'describe',
'--match', '--match',
'*.*.*-*.*.pre', '*.*.*',
'--exact-match', '--exact-match',
'--tags', '--tags',
'refs/remotes/$remote/dev', 'refs/remotes/$remote/dev',
...@@ -517,7 +495,7 @@ void main() { ...@@ -517,7 +495,7 @@ void main() {
]); ]);
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, candidateBranch: candidateBranch,
remote: remote, remote: remote,
skipTagging: true, skipTagging: true,
); );
...@@ -568,13 +546,13 @@ void main() { ...@@ -568,13 +546,13 @@ void main() {
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'rev-parse', 'rev-parse',
commit, candidateBranch,
], stdout: commit), ], stdout: commit),
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'describe', 'describe',
'--match', '--match',
'*.*.*-*.*.pre', '*.*.*',
'--exact-match', '--exact-match',
'--tags', '--tags',
'refs/remotes/$remote/dev', 'refs/remotes/$remote/dev',
...@@ -617,7 +595,7 @@ void main() { ...@@ -617,7 +595,7 @@ void main() {
]); ]);
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, candidateBranch: candidateBranch,
remote: remote, remote: remote,
); );
expect( expect(
...@@ -667,13 +645,13 @@ void main() { ...@@ -667,13 +645,13 @@ void main() {
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'rev-parse', 'rev-parse',
commit, candidateBranch,
], stdout: commit), ], stdout: commit),
const FakeCommand(command: <String>[ const FakeCommand(command: <String>[
'git', 'git',
'describe', 'describe',
'--match', '--match',
'*.*.*-*.*.pre', '*.*.*',
'--exact-match', '--exact-match',
'--tags', '--tags',
'refs/remotes/$remote/dev', 'refs/remotes/$remote/dev',
...@@ -711,7 +689,7 @@ void main() { ...@@ -711,7 +689,7 @@ void main() {
fakeArgResults = FakeArgResults( fakeArgResults = FakeArgResults(
level: level, level: level,
commit: commit, candidateBranch: candidateBranch,
remote: remote, remote: remote,
force: true, force: true,
); );
......
...@@ -122,6 +122,8 @@ void main() { ...@@ -122,6 +122,8 @@ void main() {
const String revision3 = '123abc'; const String revision3 = '123abc';
const String previousDartRevision = '171876a4e6cf56ee6da1f97d203926bd7afda7ef'; const String previousDartRevision = '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
const String nextDartRevision = 'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e'; 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) final Directory engine = fileSystem.directory(checkoutsParentDirectory)
.childDirectory('flutter_conductor_checkouts') .childDirectory('flutter_conductor_checkouts')
...@@ -182,6 +184,7 @@ void main() { ...@@ -182,6 +184,7 @@ void main() {
stdout: revision2, stdout: revision2,
), ),
]; ];
final List<FakeCommand> frameworkCommands = <FakeCommand>[ final List<FakeCommand> frameworkCommands = <FakeCommand>[
FakeCommand( FakeCommand(
command: <String>[ command: <String>[
...@@ -219,11 +222,23 @@ void main() { ...@@ -219,11 +222,23 @@ void main() {
'cherrypicks-$candidateBranch', 'cherrypicks-$candidateBranch',
], ],
), ),
const FakeCommand(
command: <String>[
'git',
'describe',
'--match',
'*.*.*',
'--tags',
'refs/remotes/upstream/$candidateBranch',
],
stdout: '$previousVersion-42-gabc123',
),
const FakeCommand( const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'], command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision3, stdout: revision3,
), ),
]; ];
final CommandRunner<void> runner = createRunner( final CommandRunner<void> runner = createRunner(
commands: <FakeCommand>[ commands: <FakeCommand>[
const FakeCommand( const FakeCommand(
...@@ -254,6 +269,8 @@ void main() { ...@@ -254,6 +269,8 @@ void main() {
stateFilePath, stateFilePath,
'--$kDartRevisionOption', '--$kDartRevisionOption',
nextDartRevision, nextDartRevision,
'--$kIncrementOption',
'm',
]); ]);
final File stateFile = fileSystem.file(stateFilePath); final File stateFile = fileSystem.file(stateFilePath);
...@@ -265,12 +282,13 @@ void main() { ...@@ -265,12 +282,13 @@ void main() {
expect(state.isInitialized(), true); expect(state.isInitialized(), true);
expect(state.releaseChannel, releaseChannel); expect(state.releaseChannel, releaseChannel);
expect(state.releaseVersion, nextVersion);
expect(state.engine.candidateBranch, candidateBranch); expect(state.engine.candidateBranch, candidateBranch);
expect(state.engine.startingGitHead, revision2); expect(state.engine.startingGitHead, revision2);
expect(state.engine.dartRevision, nextDartRevision); expect(state.engine.dartRevision, nextDartRevision);
expect(state.framework.candidateBranch, candidateBranch); expect(state.framework.candidateBranch, candidateBranch);
expect(state.framework.startingGitHead, revision3); expect(state.framework.startingGitHead, revision3);
expect(state.lastPhase, ReleasePhase.INITIALIZE); expect(state.currentPhase, ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
expect(state.conductorVersion, revision); expect(state.conductorVersion, revision);
}); });
}, onPlatform: <String, dynamic>{ }, onPlatform: <String, dynamic>{
......
...@@ -44,29 +44,26 @@ void main() { ...@@ -44,29 +44,26 @@ void main() {
}); });
test('successfully increments z', () { test('successfully increments z', () {
const String level = 'm'; const String level = 'z';
Version version = Version.fromString('1.0.0-0.0.pre'); Version version = Version.fromString('1.0.0');
expect(Version.increment(version, level).toString(), '1.0.0-1.0.pre'); expect(Version.increment(version, level).toString(), '1.0.1');
version = Version.fromString('10.20.0-40.50.pre'); version = Version.fromString('10.20.0');
expect(Version.increment(version, level).toString(), '10.20.0-41.0.pre'); expect(Version.increment(version, level).toString(), '10.20.1');
version = Version.fromString('1.18.0-3.0.pre'); version = Version.fromString('1.18.3');
expect(Version.increment(version, level).toString(), '1.18.0-4.0.pre'); expect(Version.increment(version, level).toString(), '1.18.4');
}); });
test('successfully increments m', () { test('does not support incrementing m', () {
const String level = 'm'; const String level = 'm';
Version version = Version.fromString('1.0.0-0.0.pre'); final Version version = Version.fromString('1.0.0-0.0.pre');
expect(Version.increment(version, level).toString(), '1.0.0-1.0.pre'); expect(
() => Version.increment(version, level).toString(),
version = Version.fromString('10.20.0-40.50.pre'); throwsAssertionWith("Do not increment 'm' via Version.increment"),
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', () { 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