Skip to content

Commit

Permalink
[flutter_tools] refactor packages_autoroller.dart script (#106580)
Browse files Browse the repository at this point in the history
  • Loading branch information
christopherfujino authored Jun 27, 2022
1 parent 28d271e commit aac5e95
Show file tree
Hide file tree
Showing 5 changed files with 443 additions and 29 deletions.
2 changes: 1 addition & 1 deletion dev/bots/analyze.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1778,7 +1778,7 @@ const Set<String> kExecutableAllowlist = <String>{
'dev/bots/docs.sh',

'dev/conductor/bin/conductor',
'dev/conductor/bin/roll-packages',
'dev/conductor/bin/packages_autoroller',
'dev/conductor/core/lib/src/proto/compile_proto.sh',

'dev/customer_testing/ci.sh',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ DART_BIN="$REPO_DIR/bin/dart"
# Ensure pub get has been run in the repo before running the conductor
(cd "$REPO_DIR/dev/conductor/core"; "$DART_BIN" pub get 1>&2)

exec "$DART_BIN" --enable-asserts "$REPO_DIR/dev/conductor/core/bin/roll_packages.dart" "$@"
exec "$DART_BIN" --enable-asserts "$REPO_DIR/dev/conductor/core/bin/packages_autoroller.dart" "$@"
40 changes: 30 additions & 10 deletions dev/conductor/core/bin/packages_autoroller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:conductor_core/conductor_core.dart';
import 'package:conductor_core/packages_autoroller.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:meta/meta.dart' show visibleForTesting;
import 'package:platform/platform.dart';
import 'package:process/process.dart';

Expand All @@ -17,12 +18,21 @@ const String kGithubClient = 'github-client';
const String kMirrorRemote = 'mirror-remote';
const String kUpstreamRemote = 'upstream-remote';

Future<void> main(List<String> args) async {
Future<void> main(List<String> args) {
return run(args);
}

@visibleForTesting
Future<void> run(
List<String> args, {
FileSystem fs = const LocalFileSystem(),
ProcessManager processManager = const LocalProcessManager(),
}) async {
final ArgParser parser = ArgParser();
parser.addOption(
kTokenOption,
help: 'GitHub access token env variable name.',
defaultsTo: 'GITHUB_TOKEN',
help: 'Path to GitHub access token file.',
mandatory: true,
);
parser.addOption(
kGithubClient,
Expand Down Expand Up @@ -56,12 +66,17 @@ ${parser.usage}

final String mirrorUrl = results[kMirrorRemote]! as String;
final String upstreamUrl = results[kUpstreamRemote]! as String;
const Platform platform = LocalPlatform();
final String tokenName = results[kTokenOption]! as String;
final String? token = platform.environment[tokenName];
if (token == null || token.isEmpty) {
throw FormatException(
'Tried to read a GitHub access token from env variable \$$tokenName but it was undefined or empty',
final String tokenPath = results[kTokenOption]! as String;
final File tokenFile = fs.file(tokenPath);
if (!tokenFile.existsSync()) {
throw ArgumentError(
'Provided token path $tokenPath but no file exists at ${tokenFile.absolute.path}',
);
}
final String token = tokenFile.readAsStringSync().trim();
if (token.isEmpty) {
throw ArgumentError(
'Tried to read a GitHub access token from file ${tokenFile.path} but it was empty',
);
}

Expand All @@ -76,7 +91,7 @@ ${parser.usage}
githubClient: results[kGithubClient] as String? ?? 'gh',
orgName: _parseOrgName(mirrorUrl),
token: token,
processManager: const LocalProcessManager(),
processManager: processManager,
).roll();
}

Expand Down Expand Up @@ -126,3 +141,8 @@ Directory get _localFlutterRoot {
);
return fileSystem.directory(checkoutsDirname);
}

@visibleForTesting
void validateTokenFile(String filePath, [FileSystem fs = const LocalFileSystem()]) {

}
102 changes: 88 additions & 14 deletions dev/conductor/core/lib/src/packages_autoroller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:process/process.dart';
import 'git.dart';
import 'globals.dart';
import 'repository.dart';
import 'stdio.dart';

/// A service for rolling the SDK's pub packages to latest and open a PR upstream.
class PackageAutoroller {
Expand All @@ -19,7 +20,10 @@ class PackageAutoroller {
required this.framework,
required this.orgName,
required this.processManager,
this.githubUsername = 'fluttergithubbot',
Stdio? stdio,
}) {
this.stdio = stdio ?? VerboseStdio.local();
if (token.trim().isEmpty) {
throw Exception('empty token!');
}
Expand All @@ -31,12 +35,16 @@ class PackageAutoroller {
}
}

late final Stdio stdio;

final FrameworkRepository framework;
final ProcessManager processManager;

/// Path to GitHub CLI client.
final String githubClient;

final String githubUsername;

/// GitHub API access token.
final String token;

Expand All @@ -63,23 +71,46 @@ This PR was generated by `flutter update-packages --force-upgrade`.
return name(x);
})();

void log(String message) {
stdio.printStatus(_redactToken(message));
}

/// Name of the GitHub organization to push the feature branch to.
final String orgName;

Future<void> roll() async {
await authLogin();
await updatePackages();
await pushBranch();
await createPr(
repository: await framework.checkoutDirectory,
);
await authLogout();
try {
await authLogin();
final bool openPrAlready = await hasOpenPrs();
if (openPrAlready) {
// Don't open multiple roll PRs.
return;
}
final bool didUpdate = await updatePackages();
if (!didUpdate) {
log('Packages are already at latest.');
return;
}
await pushBranch();
await createPr(repository: await framework.checkoutDirectory);
await authLogout();
} on Exception catch (exception) {
final String message = _redactToken(exception.toString());
throw Exception('${exception.runtimeType}: $message');
}
}

Future<void> updatePackages({
// Ensure we don't leak the GitHub token in exception messages
String _redactToken(String message) => message.replaceAll(token, '[GitHub TOKEN]');

/// Attempt to update all pub packages.
///
/// Will return whether or not any changes were made.
Future<bool> updatePackages({
bool verbose = true,
String author = 'flutter-packages-autoroller <[email protected]>'
}) async {
final String author = '$githubUsername <$githubUsername@gmail.com>';

await framework.newBranch(await featureBranchName);
final io.Process flutterProcess = await framework.streamFlutter(<String>[
if (verbose) '--verbose',
Expand All @@ -90,18 +121,26 @@ This PR was generated by `flutter update-packages --force-upgrade`.
if (exitCode != 0) {
throw ConductorException('Failed to update packages with exit code $exitCode');
}
// If the git checkout is clean, then pub packages are already at latest that cleanly resolve.
if (await framework.gitCheckoutClean()) {
return false;
}
await framework.commit(
'roll packages',
addFirst: true,
author: author,
);
return true;
}

Future<void> pushBranch() async {
final String projectName = framework.mirrorRemote!.url.split(r'/').last;
// Encode the token into the remote URL for authentication to work
final String remote = 'https://$token@$hostname/$orgName/$projectName';
await framework.pushRef(
fromRef: await featureBranchName,
toRef: await featureBranchName,
remote: framework.mirrorRemote!.url,
remote: remote,
);
}

Expand All @@ -123,7 +162,7 @@ This PR was generated by `flutter update-packages --force-upgrade`.
'https',
'--with-token',
],
stdin: token,
stdin: '$token\n',
);
}

Expand Down Expand Up @@ -151,6 +190,8 @@ This PR was generated by `flutter update-packages --force-upgrade`.
'$orgName:${await featureBranchName}',
'--base',
base,
'--label',
'tool',
if (draft)
'--draft',
],
Expand All @@ -165,13 +206,16 @@ This PR was generated by `flutter update-packages --force-upgrade`.
]);
}

Future<void> cli(
/// Run a sub-process with the GitHub CLI client.
///
/// Will return STDOUT of the sub-process.
Future<String> cli(
List<String> args, {
bool allowFailure = false,
String? stdin,
String? workingDirectory,
}) async {
print('Executing "$githubClient ${args.join(' ')}" in $workingDirectory');
log('Executing "$githubClient ${args.join(' ')}" in $workingDirectory');
final io.Process process = await processManager.start(
<String>[githubClient, ...args],
workingDirectory: workingDirectory,
Expand Down Expand Up @@ -203,6 +247,36 @@ This PR was generated by `flutter update-packages --force-upgrade`.
args,
);
}
print(stdout);
log(stdout);
return stdout;
}

Future<bool> hasOpenPrs() async {
// gh pr list --author christopherfujino --repo flutter/flutter --state open --json number
final String openPrString = await cli(<String>[
'pr',
'list',
'--author',
githubUsername,
'--repo',
'flutter/flutter',
'--state',
'open',
// We are only interested in pub rolls, not devicelab flaky PRs
'--label',
'tool',
// Return structured JSON with the PR numbers of open PRs
'--json',
'number',
]);

// This will be an array of objects, one for each open PR.
final List<Object?> openPrs = json.decode(openPrString) as List<Object?>;

if (openPrs.isNotEmpty) {
log('$githubUsername already has open tool PRs:\n$openPrs');
return true;
}
return false;
}
}
Loading

0 comments on commit aac5e95

Please sign in to comment.