| // 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 'dart:convert'; |
| import 'dart:io' as io; |
| |
| import 'package:process/process.dart'; |
| |
| import 'git.dart'; |
| import 'globals.dart'; |
| import 'repository.dart'; |
| |
| /// A service for rolling the SDK's pub packages to latest and open a PR upstream. |
| class PackageAutoroller { |
| PackageAutoroller({ |
| required this.githubClient, |
| required this.token, |
| required this.framework, |
| required this.orgName, |
| required this.processManager, |
| }) { |
| if (token.trim().isEmpty) { |
| throw Exception('empty token!'); |
| } |
| if (githubClient.trim().isEmpty) { |
| throw Exception('Must provide path to GitHub client!'); |
| } |
| if (orgName.trim().isEmpty) { |
| throw Exception('Must provide an orgName!'); |
| } |
| } |
| |
| final FrameworkRepository framework; |
| final ProcessManager processManager; |
| |
| /// Path to GitHub CLI client. |
| final String githubClient; |
| |
| /// GitHub API access token. |
| final String token; |
| |
| static const String hostname = 'github.com'; |
| |
| static const String prBody = ''' |
| This PR was generated by `flutter update-packages --force-upgrade`. |
| '''; |
| |
| /// Name of the feature branch to be opened on against the mirror repo. |
| /// |
| /// We never re-use a previous branch, so the branch name ends in an index |
| /// number, which gets incremented for each roll. |
| late final Future<String> featureBranchName = (() async { |
| final List<String> remoteBranches = await framework.listRemoteBranches(framework.mirrorRemote!.name); |
| |
| int x = 1; |
| String name(int index) => 'packages-autoroller-branch-$index'; |
| |
| while (remoteBranches.contains(name(x))) { |
| x += 1; |
| } |
| |
| return name(x); |
| })(); |
| |
| /// 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(); |
| } |
| |
| Future<void> updatePackages({ |
| bool verbose = true, |
| String author = 'flutter-packages-autoroller <flutter-packages-autoroller@google.com>' |
| }) async { |
| await framework.newBranch(await featureBranchName); |
| final io.Process flutterProcess = await framework.streamFlutter(<String>[ |
| if (verbose) '--verbose', |
| 'update-packages', |
| '--force-upgrade', |
| ]); |
| final int exitCode = await flutterProcess.exitCode; |
| if (exitCode != 0) { |
| throw ConductorException('Failed to update packages with exit code $exitCode'); |
| } |
| await framework.commit( |
| 'roll packages', |
| addFirst: true, |
| author: author, |
| ); |
| } |
| |
| Future<void> pushBranch() async { |
| await framework.pushRef( |
| fromRef: await featureBranchName, |
| toRef: await featureBranchName, |
| remote: framework.mirrorRemote!.url, |
| ); |
| } |
| |
| Future<void> authLogout() { |
| return cli( |
| <String>['auth', 'logout', '--hostname', hostname], |
| allowFailure: true, |
| ); |
| } |
| |
| Future<void> authLogin() { |
| return cli( |
| <String>[ |
| 'auth', |
| 'login', |
| '--hostname', |
| hostname, |
| '--git-protocol', |
| 'https', |
| '--with-token', |
| ], |
| stdin: token, |
| ); |
| } |
| |
| /// Create a pull request on GitHub. |
| /// |
| /// Depends on the gh cli tool. |
| Future<void> createPr({ |
| required io.Directory repository, |
| String title = 'Roll pub packages', |
| String body = 'This PR was generated by `flutter update-packages --force-upgrade`.', |
| String base = FrameworkRepository.defaultBranch, |
| bool draft = false, |
| }) async { |
| // We will wrap title and body in double quotes before delegating to gh |
| // binary |
| await cli( |
| <String>[ |
| 'pr', |
| 'create', |
| '--title', |
| title.trim(), |
| '--body', |
| body.trim(), |
| '--head', |
| '$orgName:${await featureBranchName}', |
| '--base', |
| base, |
| if (draft) |
| '--draft', |
| ], |
| workingDirectory: repository.path, |
| ); |
| } |
| |
| Future<void> help([List<String>? args]) { |
| return cli(<String>[ |
| 'help', |
| ...?args, |
| ]); |
| } |
| |
| Future<void> cli( |
| List<String> args, { |
| bool allowFailure = false, |
| String? stdin, |
| String? workingDirectory, |
| }) async { |
| print('Executing "$githubClient ${args.join(' ')}" in $workingDirectory'); |
| final io.Process process = await processManager.start( |
| <String>[githubClient, ...args], |
| workingDirectory: workingDirectory, |
| environment: <String, String>{}, |
| ); |
| final List<String> stderrStrings = <String>[]; |
| final List<String> stdoutStrings = <String>[]; |
| final Future<void> stdoutFuture = process.stdout |
| .transform(utf8.decoder) |
| .forEach(stdoutStrings.add); |
| final Future<void> stderrFuture = process.stderr |
| .transform(utf8.decoder) |
| .forEach(stderrStrings.add); |
| if (stdin != null) { |
| process.stdin.write(stdin); |
| await process.stdin.flush(); |
| await process.stdin.close(); |
| } |
| final int exitCode = await process.exitCode; |
| await Future.wait(<Future<Object?>>[ |
| stdoutFuture, |
| stderrFuture, |
| ]); |
| final String stderr = stderrStrings.join(); |
| final String stdout = stdoutStrings.join(); |
| if (!allowFailure && exitCode != 0) { |
| throw GitException( |
| '$stderr\n$stdout', |
| args, |
| ); |
| } |
| print(stdout); |
| } |
| } |