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;
}
......@@ -25,7 +25,7 @@ class Remote {
const Remote({
required RemoteName name,
required this.url,
}) : _name = name;
}) : _name = name, assert(url != null), assert (url != '');
final RemoteName _name;
......@@ -47,7 +47,7 @@ class Remote {
abstract class Repository {
Repository({
required this.name,
required this.fetchRemote,
required this.upstreamRemote,
required this.processManager,
required this.stdio,
required this.platform,
......@@ -55,20 +55,34 @@ abstract class Repository {
required this.parentDirectory,
this.initialRef,
this.localUpstream = false,
this.useExistingCheckout = false,
this.pushRemote,
String? previousCheckoutLocation,
this.mirrorRemote,
}) : git = Git(processManager),
assert(localUpstream != null),
assert(useExistingCheckout != null);
assert(upstreamRemote.url.isNotEmpty) {
if (previousCheckoutLocation != null) {
_checkoutDirectory = fileSystem.directory(previousCheckoutLocation);
if (!_checkoutDirectory!.existsSync()) {
throw ConductorException('Provided previousCheckoutLocation $previousCheckoutLocation does not exist on disk!');
}
if (initialRef != null) {
git.run(
<String>['checkout', '${upstreamRemote.name}/$initialRef'],
'Checking out initialRef $initialRef',
workingDirectory: _checkoutDirectory!.path,
);
}
}
}
final String name;
final Remote fetchRemote;
final Remote upstreamRemote;
/// Remote to publish tags and commits to.
/// Remote for user's mirror.
///
/// This value can be null, in which case attempting to publish will lead to
/// This value can be null, in which case attempting to access it will lead to
/// a [ConductorException].
final Remote? pushRemote;
final Remote? mirrorRemote;
/// The initial ref (branch or commit name) to check out.
final String? initialRef;
......@@ -78,7 +92,6 @@ abstract class Repository {
final Platform platform;
final FileSystem fileSystem;
final Directory parentDirectory;
final bool useExistingCheckout;
/// If the repository will be used as an upstream for a test repo.
final bool localUpstream;
......@@ -100,55 +113,53 @@ abstract class Repository {
/// Ensure the repository is cloned to disk and initialized with proper state.
void lazilyInitialize(Directory checkoutDirectory) {
if (!useExistingCheckout && checkoutDirectory.existsSync()) {
if (checkoutDirectory.existsSync()) {
stdio.printTrace('Deleting $name from ${checkoutDirectory.path}...');
checkoutDirectory.deleteSync(recursive: true);
}
if (!checkoutDirectory.existsSync()) {
stdio.printTrace(
'Cloning $name from ${fetchRemote.url} to ${checkoutDirectory.path}...',
stdio.printTrace(
'Cloning $name from ${upstreamRemote.url} to ${checkoutDirectory.path}...',
);
git.run(
<String>[
'clone',
'--origin',
upstreamRemote.name,
'--',
upstreamRemote.url,
checkoutDirectory.path
],
'Cloning $name repo',
workingDirectory: parentDirectory.path,
);
if (mirrorRemote != null) {
git.run(
<String>['remote', 'add', mirrorRemote!.name, mirrorRemote!.url],
'Adding remote ${mirrorRemote!.url} as ${mirrorRemote!.name}',
workingDirectory: checkoutDirectory.path,
);
git.run(
<String>[
'clone',
'--origin',
fetchRemote.name,
'--',
fetchRemote.url,
checkoutDirectory.path
],
'Cloning $name repo',
workingDirectory: parentDirectory.path,
<String>['fetch', mirrorRemote!.name],
'Fetching git remote ${mirrorRemote!.name}',
workingDirectory: checkoutDirectory.path,
);
if (pushRemote != null) {
git.run(
<String>['remote', 'add', pushRemote!.name, pushRemote!.url],
'Adding remote ${pushRemote!.url} as ${pushRemote!.name}',
workingDirectory: checkoutDirectory.path,
);
}
if (localUpstream) {
// These branches must exist locally for the repo that depends on it
// to fetch and push to.
for (final String channel in kReleaseChannels) {
git.run(
<String>['fetch', pushRemote!.name],
'Fetching git remote ${pushRemote!.name}',
<String>['checkout', channel, '--'],
'check out branch $channel locally',
workingDirectory: checkoutDirectory.path,
);
}
if (localUpstream) {
// These branches must exist locally for the repo that depends on it
// to fetch and push to.
for (final String channel in kReleaseChannels) {
git.run(
<String>['checkout', channel, '--'],
'check out branch $channel locally',
workingDirectory: checkoutDirectory.path,
);
}
}
}
if (initialRef != null) {
git.run(
<String>['checkout', '${fetchRemote.name}/$initialRef'],
<String>['checkout', '${upstreamRemote.name}/$initialRef'],
'Checking out initialRef $initialRef',
workingDirectory: checkoutDirectory.path,
);
......@@ -217,13 +228,25 @@ abstract class Repository {
);
}
/// Obtain the version tag of the previous dev release.
String getFullTag(String remoteName) {
const String glob = '*.*.*-*.*.pre';
/// Obtain the version tag at the tip of a release branch.
String getFullTag(
String remoteName,
String branchName, {
bool exact = true,
}) {
// includes both stable (e.g. 1.2.3) and dev tags (e.g. 1.2.3-4.5.pre)
const String glob = '*.*.*';
// describe the latest dev release
final String ref = 'refs/remotes/$remoteName/dev';
final String ref = 'refs/remotes/$remoteName/$branchName';
return git.getOutput(
<String>['describe', '--match', glob, '--exact-match', '--tags', ref],
<String>[
'describe',
'--match',
glob,
if (exact) '--exact-match',
'--tags',
ref,
],
'obtain last released version number',
workingDirectory: checkoutDirectory.path,
);
......@@ -235,7 +258,7 @@ abstract class Repository {
.getOutput(
<String>['rev-list', ...args],
'rev-list with args ${args.join(' ')}',
workingDirectory: checkoutDirectory.path,
workingDirectory: checkoutDirectory.path
)
.trim()
.split('\n');
......@@ -332,20 +355,6 @@ abstract class Repository {
);
}
/// Tag [commit] and push the tag to the remote.
void tag(String commit, String tagName, String remote) {
git.run(
<String>['tag', tagName, commit],
'tag the commit with the version label',
workingDirectory: checkoutDirectory.path,
);
git.run(
<String>['push', remote, tagName],
'publish the tag to the repo',
workingDirectory: checkoutDirectory.path,
);
}
/// Push [commit] to the release channel [branch].
void updateChannel(
String commit,
......@@ -419,16 +428,16 @@ class FrameworkRepository extends Repository {
FrameworkRepository(
this.checkouts, {
String name = 'framework',
Remote fetchRemote = const Remote(
Remote upstreamRemote = const Remote(
name: RemoteName.upstream, url: FrameworkRepository.defaultUpstream),
bool localUpstream = false,
bool useExistingCheckout = false,
String? previousCheckoutLocation,
String? initialRef,
Remote? pushRemote,
Remote? mirrorRemote,
}) : super(
name: name,
fetchRemote: fetchRemote,
pushRemote: pushRemote,
upstreamRemote: upstreamRemote,
mirrorRemote: mirrorRemote,
initialRef: initialRef,
fileSystem: checkouts.fileSystem,
localUpstream: localUpstream,
......@@ -436,7 +445,7 @@ class FrameworkRepository extends Repository {
platform: checkouts.platform,
processManager: checkouts.processManager,
stdio: checkouts.stdio,
useExistingCheckout: useExistingCheckout,
previousCheckoutLocation: previousCheckoutLocation,
);
/// A [FrameworkRepository] with the host conductor's repo set as upstream.
......@@ -446,18 +455,18 @@ class FrameworkRepository extends Repository {
factory FrameworkRepository.localRepoAsUpstream(
Checkouts checkouts, {
String name = 'framework',
bool useExistingCheckout = false,
String? previousCheckoutLocation,
required String upstreamPath,
}) {
return FrameworkRepository(
checkouts,
name: name,
fetchRemote: Remote(
upstreamRemote: Remote(
name: RemoteName.upstream,
url: 'file://$upstreamPath/',
),
localUpstream: false,
useExistingCheckout: useExistingCheckout,
previousCheckoutLocation: previousCheckoutLocation,
);
}
......@@ -473,6 +482,27 @@ class FrameworkRepository extends Repository {
'cache',
);
/// Tag [commit] and push the tag to the remote.
void tag(String commit, String tagName, String remote) {
assert(commit.isNotEmpty);
assert(tagName.isNotEmpty);
assert(remote.isNotEmpty);
stdio.printStatus('About to tag commit $commit as $tagName...');
git.run(
<String>['tag', tagName, commit],
'tag the commit with the version label',
workingDirectory: checkoutDirectory.path,
);
stdio.printStatus('Tagging successful.');
stdio.printStatus('About to push $tagName to remote $remote...');
git.run(
<String>['push', remote, tagName],
'publish the tag to the repo',
workingDirectory: checkoutDirectory.path,
);
stdio.printStatus('Tag push successful.');
}
@override
Repository cloneRepository(String? cloneName) {
assert(localUpstream);
......@@ -480,9 +510,8 @@ class FrameworkRepository extends Repository {
return FrameworkRepository(
checkouts,
name: cloneName,
fetchRemote: Remote(
upstreamRemote: Remote(
name: RemoteName.upstream, url: 'file://${checkoutDirectory.path}/'),
useExistingCheckout: useExistingCheckout,
);
}
......@@ -547,17 +576,15 @@ class HostFrameworkRepository extends FrameworkRepository {
HostFrameworkRepository({
required Checkouts checkouts,
String name = 'host-framework',
bool useExistingCheckout = false,
required String upstreamPath,
}) : super(
checkouts,
name: name,
fetchRemote: Remote(
upstreamRemote: Remote(
name: RemoteName.upstream,
url: 'file://$upstreamPath/',
),
localUpstream: false,
useExistingCheckout: useExistingCheckout,
) {
_checkoutDirectory = checkouts.fileSystem.directory(upstreamPath);
}
......@@ -613,15 +640,15 @@ class EngineRepository extends Repository {
this.checkouts, {
String name = 'engine',
String initialRef = EngineRepository.defaultBranch,
Remote fetchRemote = const Remote(
Remote upstreamRemote = const Remote(
name: RemoteName.upstream, url: EngineRepository.defaultUpstream),
bool localUpstream = false,
bool useExistingCheckout = false,
Remote? pushRemote,
String? previousCheckoutLocation,
Remote? mirrorRemote,
}) : super(
name: name,
fetchRemote: fetchRemote,
pushRemote: pushRemote,
upstreamRemote: upstreamRemote,
mirrorRemote: mirrorRemote,
initialRef: initialRef,
fileSystem: checkouts.fileSystem,
localUpstream: localUpstream,
......@@ -629,7 +656,7 @@ class EngineRepository extends Repository {
platform: checkouts.platform,
processManager: checkouts.processManager,
stdio: checkouts.stdio,
useExistingCheckout: useExistingCheckout,
previousCheckoutLocation: previousCheckoutLocation,
);
final Checkouts checkouts;
......@@ -669,9 +696,8 @@ class EngineRepository extends Repository {
return EngineRepository(
checkouts,
name: cloneName,
fetchRemote: Remote(
upstreamRemote: Remote(
name: RemoteName.upstream, url: 'file://${checkoutDirectory.path}/'),
useExistingCheckout: useExistingCheckout,
);
}
}
......
......@@ -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,
......
// 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:conductor/next.dart';
import 'package:conductor/proto/conductor_state.pb.dart' as pb;
import 'package:conductor/proto/conductor_state.pbenum.dart' show ReleasePhase;
import 'package:conductor/repository.dart';
import 'package:conductor/state.dart';
import 'package:file/memory.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import './common.dart';
import '../../../packages/flutter_tools/test/src/fake_process_manager.dart';
void main() {
group('next command', () {
const String flutterRoot = '/flutter';
const String checkoutsParentDirectory = '$flutterRoot/dev/tools/';
const String candidateBranch = 'flutter-1.2-candidate.3';
final String localPathSeparator = const LocalPlatform().pathSeparator;
final String localOperatingSystem = const LocalPlatform().pathSeparator;
const String revision1 = 'abc123';
MemoryFileSystem fileSystem;
TestStdio stdio;
const String stateFile = '/state-file.json';
setUp(() {
stdio = TestStdio();
fileSystem = MemoryFileSystem.test();
});
CommandRunner<void> createRunner({
@required Checkouts checkouts,
}) {
final NextCommand command = NextCommand(
checkouts: checkouts,
);
return CommandRunner<void>('codesign-test', '')..addCommand(command);
}
test('throws if no state file found', () async {
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
expect(
() async => runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]),
throwsExceptionWith('No persistent state file found at $stateFile'),
);
});
test('does not prompt user and updates state.currentPhase from APPLY_ENGINE_CHERRYPICKS to CODESIGN_ENGINE_BINARIES if there are no engine cherrypicks', () async {
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
currentPhase: ReleasePhase.APPLY_ENGINE_CHERRYPICKS,
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]);
final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile),
);
expect(finalState.currentPhase, ReleasePhase.CODESIGN_ENGINE_BINARIES);
expect(stdio.error, isEmpty);
});
test('updates state.lastPhase from APPLY_ENGINE_CHERRYPICKS to CODESIGN_ENGINE_BINARIES if user responds yes', () async {
const String remoteUrl = 'https://githost.com/org/repo.git';
stdio.stdin.add('y');
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
engine: pb.Repository(
cherrypicks: <pb.Cherrypick>[
pb.Cherrypick(
trunkRevision: 'abc123',
state: pb.CherrypickState.PENDING,
),
],
mirror: pb.Remote(url: remoteUrl),
),
currentPhase: ReleasePhase.APPLY_ENGINE_CHERRYPICKS,
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]);
final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile),
);
expect(stdio.stdout, contains(
'Are you ready to push your engine branch to the repository $remoteUrl? (y/n) '));
expect(finalState.currentPhase, ReleasePhase.CODESIGN_ENGINE_BINARIES);
expect(stdio.error, isEmpty);
});
test('does not update state.currentPhase from CODESIGN_ENGINE_BINARIES if user responds no', () async {
stdio.stdin.add('n');
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
engine: pb.Repository(
cherrypicks: <pb.Cherrypick>[
pb.Cherrypick(
trunkRevision: 'abc123',
state: pb.CherrypickState.PENDING,
),
],
),
currentPhase: ReleasePhase.CODESIGN_ENGINE_BINARIES,
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]);
final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile),
);
expect(stdio.stdout, contains('Has CI passed for the engine PR and binaries been codesigned? (y/n) '));
expect(finalState.currentPhase, ReleasePhase.CODESIGN_ENGINE_BINARIES);
expect(stdio.error.contains('Aborting command.'), true);
});
test('updates state.currentPhase from CODESIGN_ENGINE_BINARIES to APPLY_FRAMEWORK_CHERRYPICKS if user responds yes', () async {
stdio.stdin.add('y');
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
currentPhase: ReleasePhase.CODESIGN_ENGINE_BINARIES,
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]);
final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile),
);
expect(stdio.stdout, contains('Has CI passed for the engine PR and binaries been codesigned? (y/n) '));
expect(finalState.currentPhase, ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS);
});
test('does not prompt user and updates state.currentPhase from APPLY_FRAMEWORK_CHERRYPICKS to PUBLISH_VERSION if there are no framework cherrypicks', () async {
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]);
final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile),
);
expect(stdio.stdout, isNot(contains('Did you apply all framework cherrypicks? (y/n) ')));
expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION);
expect(stdio.error, isEmpty);
});
test('does not update state.currentPhase from APPLY_FRAMEWORK_CHERRYPICKS if user responds no', () async {
const String remoteUrl = 'https://githost.com/org/repo.git';
stdio.stdin.add('n');
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
framework: pb.Repository(
cherrypicks: <pb.Cherrypick>[
pb.Cherrypick(
trunkRevision: 'abc123',
state: pb.CherrypickState.PENDING,
),
],
mirror: pb.Remote(url: remoteUrl),
),
currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]);
final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile),
);
expect(stdio.stdout, contains('Are you ready to push your framework branch to the repository $remoteUrl? (y/n) '));
expect(stdio.error, contains('Aborting command.'));
expect(finalState.currentPhase, ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS);
});
test('updates state.currentPhase from APPLY_FRAMEWORK_CHERRYPICKS to PUBLISH_VERSION if user responds yes', () async {
const String remoteUrl = 'https://githost.com/org/repo.git';
stdio.stdin.add('y');
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
framework: pb.Repository(
cherrypicks: <pb.Cherrypick>[
pb.Cherrypick(
trunkRevision: 'abc123',
state: pb.CherrypickState.PENDING,
),
],
mirror: pb.Remote(url: remoteUrl),
),
currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]);
final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile),
);
expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION);
expect(stdio.stdout, contains('Are you ready to push your framework branch to the repository $remoteUrl? (y/n)'));
});
test('does not update state.currentPhase from PUBLISH_VERSION if user responds no', () async {
const String remoteName = 'upstream';
stdio.stdin.add('n');
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>['git', 'checkout', '$remoteName/$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision1,
),
],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
currentPhase: ReleasePhase.PUBLISH_VERSION,
framework: pb.Repository(
candidateBranch: candidateBranch,
upstream: pb.Remote(url: FrameworkRepository.defaultUpstream),
),
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]);
final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile),
);
expect(stdio.stdout, contains('Has CI passed for the framework PR?'));
expect(stdio.error, contains('Aborting command.'));
expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION);
expect(finalState.logs, stdio.logs);
expect(processManager.hasRemainingExpectations, false);
});
test('updates state.currentPhase from PUBLISH_VERSION to PUBLISH_CHANNEL if user responds yes', () async {
const String remoteName = 'upstream';
const String releaseVersion = '1.2.0-3.0.pre';
stdio.stdin.add('y');
final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
const FakeCommand(
command: <String>['git', 'checkout', '$remoteName/$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision1,
),
const FakeCommand(
command: <String>['git', 'tag', releaseVersion, revision1],
),
const FakeCommand(
command: <String>['git', 'push', remoteName, releaseVersion],
),
]);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
currentPhase: ReleasePhase.PUBLISH_VERSION,
framework: pb.Repository(
candidateBranch: candidateBranch,
upstream: pb.Remote(url: FrameworkRepository.defaultUpstream),
),
releaseVersion: releaseVersion,
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]);
final pb.ConductorState finalState = readStateFromFile(
fileSystem.file(stateFile),
);
expect(finalState.currentPhase, ReleasePhase.PUBLISH_CHANNEL);
expect(stdio.stdout, contains('Has CI passed for the framework PR?'));
expect(finalState.logs, stdio.logs);
expect(processManager.hasRemainingExpectations, false);
});
test('throws exception if state.currentPhase is RELEASE_COMPLETED', () async {
final FakeProcessManager processManager = FakeProcessManager.list(
<FakeCommand>[],
);
final FakePlatform platform = FakePlatform(
environment: <String, String>{
'HOME': <String>['path', 'to', 'home'].join(localPathSeparator),
},
operatingSystem: localOperatingSystem,
pathSeparator: localPathSeparator,
);
final pb.ConductorState state = pb.ConductorState(
currentPhase: ReleasePhase.RELEASE_COMPLETED,
);
writeStateToFile(
fileSystem.file(stateFile),
state,
<String>[],
);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
expect(
() async => runner.run(<String>[
'next',
'--$kStateOption',
stateFile,
]),
throwsExceptionWith('This release is finished.'),
);
});
}, onPlatform: <String, dynamic>{
'windows': const Skip('Flutter Conductor only supported on macos/linux'),
});
}
// 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