mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Conductor add next (#84354)
This commit is contained in:
parent
567586b407
commit
c3822edbe8
@ -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+)',
|
||||
);
|
||||
|
288
dev/conductor/lib/next.dart
Normal file
288
dev/conductor/lib/next.dart
Normal file
@ -0,0 +1,288 @@
|
||||
// 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) {
|
||||
}
|
||||
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>['remote', 'add', pushRemote!.name, pushRemote!.url],
|
||||
'Adding remote ${pushRemote!.url} as ${pushRemote!.name}',
|
||||
<String>['checkout', channel, '--'],
|
||||
'check out branch $channel locally',
|
||||
workingDirectory: checkoutDirectory.path,
|
||||
);
|
||||
git.run(
|
||||
<String>['fetch', pushRemote!.name],
|
||||
'Fetching git remote ${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>['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,
|
||||
|
572
dev/conductor/test/next_test.dart
Normal file
572
dev/conductor/test/next_test.dart
Normal file
@ -0,0 +1,572 @@
|
||||
// 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'),
|
||||
});
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
// 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(
|
||||
() => 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,
|
||||
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', () {
|
||||
|
Loading…
Reference in New Issue
Block a user