Unverified Commit 764577f8 authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

[flutter_conductor] allow --force to override validations on start sub-command (#93514)

parent e95d669e
...@@ -9,6 +9,7 @@ import 'proto/conductor_state.pb.dart' as pb; ...@@ -9,6 +9,7 @@ import 'proto/conductor_state.pb.dart' as pb;
const String gsutilBinary = 'gsutil.py'; const String gsutilBinary = 'gsutil.py';
const String kFrameworkDefaultBranch = 'master'; const String kFrameworkDefaultBranch = 'master';
const String kForceFlag = 'force';
const List<String> kReleaseChannels = <String>[ const List<String> kReleaseChannels = <String>[
'stable', 'stable',
...@@ -81,6 +82,18 @@ String? getValueFromEnvOrArgs( ...@@ -81,6 +82,18 @@ String? getValueFromEnvOrArgs(
'to be provided!'); 'to be provided!');
} }
bool getBoolFromEnvOrArgs(
String name,
ArgResults argResults,
Map<String, String> env,
) {
final String envName = fromArgToEnvName(name);
if (env[envName] != null) {
return (env[envName]?.toUpperCase()) == 'TRUE';
}
return argResults[name] as bool;
}
/// Return multiple values from the environment or fall back to [argResults]. /// Return multiple values from the environment or fall back to [argResults].
/// ///
/// Values read from an environment variable are assumed to be comma-delimited. /// Values read from an environment variable are assumed to be comma-delimited.
......
...@@ -14,7 +14,6 @@ import './stdio.dart'; ...@@ -14,7 +14,6 @@ import './stdio.dart';
const String kStateOption = 'state-file'; const String kStateOption = 'state-file';
const String kYesFlag = 'yes'; const String kYesFlag = 'yes';
const String kForceFlag = 'force';
/// Command to proceed from one [pb.ReleasePhase] to the next. /// Command to proceed from one [pb.ReleasePhase] to the next.
class NextCommand extends Command<void> { class NextCommand extends Command<void> {
......
...@@ -102,6 +102,11 @@ class StartCommand extends Command<void> { ...@@ -102,6 +102,11 @@ class StartCommand extends Command<void> {
'n': 'Indicates a hotfix to a dev or beta release.', 'n': 'Indicates a hotfix to a dev or beta release.',
}, },
); );
argParser.addFlag(
kForceFlag,
abbr: 'f',
help: 'Override all validations of the command line inputs.',
);
} }
final Checkouts checkouts; final Checkouts checkouts;
...@@ -178,6 +183,11 @@ class StartCommand extends Command<void> { ...@@ -178,6 +183,11 @@ class StartCommand extends Command<void> {
argumentResults, argumentResults,
platform.environment, platform.environment,
)!; )!;
final bool force = getBoolFromEnvOrArgs(
kForceFlag,
argumentResults,
platform.environment,
);
final File stateFile = checkouts.fileSystem.file( final File stateFile = checkouts.fileSystem.file(
getValueFromEnvOrArgs(kStateOption, argumentResults, platform.environment), getValueFromEnvOrArgs(kStateOption, argumentResults, platform.environment),
); );
...@@ -198,6 +208,7 @@ class StartCommand extends Command<void> { ...@@ -198,6 +208,7 @@ class StartCommand extends Command<void> {
releaseChannel: releaseChannel, releaseChannel: releaseChannel,
stateFile: stateFile, stateFile: stateFile,
stdio: stdio, stdio: stdio,
force: force,
); );
return context.run(); return context.run();
} }
...@@ -223,6 +234,7 @@ class StartContext { ...@@ -223,6 +234,7 @@ class StartContext {
required this.releaseChannel, required this.releaseChannel,
required this.stateFile, required this.stateFile,
required this.stdio, required this.stdio,
this.force = false,
}) : git = Git(processManager); }) : git = Git(processManager);
final String candidateBranch; final String candidateBranch;
...@@ -242,6 +254,9 @@ class StartContext { ...@@ -242,6 +254,9 @@ class StartContext {
final File stateFile; final File stateFile;
final Stdio stdio; final Stdio stdio;
/// If validations should be overridden.
final bool force;
Future<void> run() async { Future<void> run() async {
if (stateFile.existsSync()) { if (stateFile.existsSync()) {
throw ConductorException( throw ConductorException(
...@@ -361,7 +376,12 @@ class StartContext { ...@@ -361,7 +376,12 @@ class StartContext {
final Version lastVersion = Version.fromString(await framework.getFullTag( final Version lastVersion = Version.fromString(await framework.getFullTag(
framework.upstreamRemote.name, candidateBranch, framework.upstreamRemote.name, candidateBranch,
exact: false, exact: false,
))..ensureValid(candidateBranch, incrementLetter); ));
// [force] means we know this would fail but need to publish anyway
if (!force) {
lastVersion.ensureValid(candidateBranch, incrementLetter);
}
Version nextVersion = calculateNextVersion(lastVersion); Version nextVersion = calculateNextVersion(lastVersion);
nextVersion = await ensureBranchPointTagged(nextVersion, framework); nextVersion = await ensureBranchPointTagged(nextVersion, framework);
......
...@@ -238,12 +238,18 @@ class Version { ...@@ -238,12 +238,18 @@ class Version {
final int y; final int y;
/// Number of hotfix releases after a stable release. /// Number of hotfix releases after a stable release.
///
/// For non-stable releases, this will be 0.
final int z; final int z;
/// Zero-indexed count of dev releases after a beta release. /// Zero-indexed count of dev releases after a beta release.
///
/// For stable releases, this will be null.
final int? m; final int? m;
/// Number of hotfixes required to make a dev release. /// Number of hotfixes required to make a dev release.
///
/// For stable releases, this will be null.
final int? n; final int? n;
/// Number of commits past last tagged dev release. /// Number of commits past last tagged dev release.
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:args/args.dart';
import 'package:conductor_core/src/globals.dart'; import 'package:conductor_core/src/globals.dart';
import 'package:conductor_core/src/proto/conductor_state.pb.dart' as pb; import 'package:conductor_core/src/proto/conductor_state.pb.dart' as pb;
...@@ -129,4 +130,69 @@ void main() { ...@@ -129,4 +130,69 @@ void main() {
); );
}); });
}); });
group('getBoolFromEnvOrArgs', () {
const String flagName = 'a-cli-flag';
test('prefers env over argResults', () {
final ArgResults argResults = FakeArgs(results: <String, Object>{
flagName: false,
});
final Map<String, String> env = <String, String>{'A_CLI_FLAG': 'TRUE'};
final bool result = getBoolFromEnvOrArgs(
flagName,
argResults,
env,
);
expect(result, true);
});
test('falls back to argResults if env is empty', () {
final ArgResults argResults = FakeArgs(results: <String, Object>{
flagName: false,
});
final Map<String, String> env = <String, String>{};
final bool result = getBoolFromEnvOrArgs(
flagName,
argResults,
env,
);
expect(result, false);
});
});
}
class FakeArgs implements ArgResults {
FakeArgs({
this.arguments = const <String>[],
this.name = 'fake-command',
this.results = const <String, Object>{},
});
final Map<String, Object> results;
@override
final List<String> arguments;
@override
final String name;
@override
ArgResults? get command => throw Exception('Unimplemented');
@override
List<String> get rest => throw Exception('Unimplemented');
@override
Iterable<String> get options => throw Exception('Unimplemented');
@override
bool wasParsed(String name) {
return results[name] != null;
}
@override
Object? operator[](String name) {
return results[name];
}
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:convert' show jsonDecode; import 'dart:convert' show jsonDecode;
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:conductor_core/src/globals.dart';
import 'package:conductor_core/src/proto/conductor_state.pb.dart' as pb; import 'package:conductor_core/src/proto/conductor_state.pb.dart' as pb;
import 'package:conductor_core/src/proto/conductor_state.pbenum.dart' show ReleasePhase; import 'package:conductor_core/src/proto/conductor_state.pbenum.dart' show ReleasePhase;
import 'package:conductor_core/src/repository.dart'; import 'package:conductor_core/src/repository.dart';
...@@ -122,6 +123,188 @@ void main() { ...@@ -122,6 +123,188 @@ void main() {
); );
}); });
test('does not fail if version wrong but --force provided', () async {
const String revision2 = 'def789';
const String revision3 = '123abc';
const String previousDartRevision = '171876a4e6cf56ee6da1f97d203926bd7afda7ef';
const String nextDartRevision = 'f6c91128be6b77aef8351e1e3a9d07c85bc2e46e';
const String previousVersion = '1.2.0-1.0.pre';
// This is what this release will be
const String nextVersion = '1.2.0-1.1.pre';
const String incrementLevel = 'n';
final Directory engine = fileSystem.directory(checkoutsParentDirectory)
.childDirectory('flutter_conductor_checkouts')
.childDirectory('engine');
final File depsFile = engine.childFile('DEPS');
final List<FakeCommand> engineCommands = <FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
EngineRepository.defaultUpstream,
engine.path,
],
onRun: () {
// Create the DEPS file which the tool will update
engine.createSync(recursive: true);
depsFile.writeAsStringSync(generateMockDeps(previousDartRevision));
}
),
const FakeCommand(
command: <String>['git', 'remote', 'add', 'mirror', engineMirror],
),
const FakeCommand(
command: <String>['git', 'fetch', 'mirror'],
),
const FakeCommand(
command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision2,
),
const FakeCommand(
command: <String>[
'git',
'checkout',
'-b',
'cherrypicks-$candidateBranch',
],
),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM path/to/DEPS',
),
const FakeCommand(
command: <String>['git', 'add', '--all'],
),
const FakeCommand(
command: <String>['git', 'commit', "--message='Update Dart SDK to $nextDartRevision'"],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision2,
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision2,
),
];
final List<FakeCommand> frameworkCommands = <FakeCommand>[
FakeCommand(
command: <String>[
'git',
'clone',
'--origin',
'upstream',
'--',
FrameworkRepository.defaultUpstream,
fileSystem.path.join(
checkoutsParentDirectory,
'flutter_conductor_checkouts',
'framework',
),
],
),
const FakeCommand(
command: <String>['git', 'remote', 'add', 'mirror', frameworkMirror],
),
const FakeCommand(
command: <String>['git', 'fetch', 'mirror'],
),
const FakeCommand(
command: <String>['git', 'checkout', 'upstream/$candidateBranch'],
),
const FakeCommand(
command: <String>['git', 'rev-parse', 'HEAD'],
stdout: revision3,
),
const FakeCommand(
command: <String>[
'git',
'checkout',
'-b',
'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>[
...engineCommands,
...frameworkCommands,
],
);
final String stateFilePath = fileSystem.path.join(
platform.environment['HOME']!,
kStateFileName,
);
await runner.run(<String>[
'start',
'--$kFrameworkMirrorOption',
frameworkMirror,
'--$kEngineMirrorOption',
engineMirror,
'--$kCandidateOption',
candidateBranch,
'--$kReleaseOption',
releaseChannel,
'--$kStateOption',
stateFilePath,
'--$kDartRevisionOption',
nextDartRevision,
'--$kIncrementOption',
incrementLevel,
'--$kForceFlag',
]);
final File stateFile = fileSystem.file(stateFilePath);
final pb.ConductorState state = pb.ConductorState();
state.mergeFromProto3Json(
jsonDecode(stateFile.readAsStringSync()),
);
expect(processManager.hasRemainingExpectations, false);
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.engine.upstream.url, 'git@github.com:flutter/engine.git');
expect(state.framework.candidateBranch, candidateBranch);
expect(state.framework.startingGitHead, revision3);
expect(state.framework.upstream.url, 'git@github.com:flutter/flutter.git');
expect(state.currentPhase, ReleasePhase.APPLY_ENGINE_CHERRYPICKS);
expect(state.conductorVersion, conductorVersion);
expect(state.incrementLevel, incrementLevel);
});
test('creates state file if provided correct inputs', () async { test('creates state file if provided correct inputs', () async {
const String revision2 = 'def789'; const String revision2 = 'def789';
const String revision3 = '123abc'; const String revision3 = '123abc';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment