@@ -10,6 +10,7 @@ import 'package:process/process.dart';
10
10
import 'git.dart' ;
11
11
import 'globals.dart' ;
12
12
import 'repository.dart' ;
13
+ import 'stdio.dart' ;
13
14
14
15
/// A service for rolling the SDK's pub packages to latest and open a PR upstream.
15
16
class PackageAutoroller {
@@ -19,7 +20,10 @@ class PackageAutoroller {
19
20
required this .framework,
20
21
required this .orgName,
21
22
required this .processManager,
23
+ this .githubUsername = 'fluttergithubbot' ,
24
+ Stdio ? stdio,
22
25
}) {
26
+ this .stdio = stdio ?? VerboseStdio .local ();
23
27
if (token.trim ().isEmpty) {
24
28
throw Exception ('empty token!' );
25
29
}
@@ -31,12 +35,16 @@ class PackageAutoroller {
31
35
}
32
36
}
33
37
38
+ late final Stdio stdio;
39
+
34
40
final FrameworkRepository framework;
35
41
final ProcessManager processManager;
36
42
37
43
/// Path to GitHub CLI client.
38
44
final String githubClient;
39
45
46
+ final String githubUsername;
47
+
40
48
/// GitHub API access token.
41
49
final String token;
42
50
@@ -63,23 +71,46 @@ This PR was generated by `flutter update-packages --force-upgrade`.
63
71
return name (x);
64
72
})();
65
73
74
+ void log (String message) {
75
+ stdio.printStatus (_redactToken (message));
76
+ }
77
+
66
78
/// Name of the GitHub organization to push the feature branch to.
67
79
final String orgName;
68
80
69
81
Future <void > roll () async {
70
- await authLogin ();
71
- await updatePackages ();
72
- await pushBranch ();
73
- await createPr (
74
- repository: await framework.checkoutDirectory,
75
- );
76
- await authLogout ();
82
+ try {
83
+ await authLogin ();
84
+ final bool openPrAlready = await hasOpenPrs ();
85
+ if (openPrAlready) {
86
+ // Don't open multiple roll PRs.
87
+ return ;
88
+ }
89
+ final bool didUpdate = await updatePackages ();
90
+ if (! didUpdate) {
91
+ log ('Packages are already at latest.' );
92
+ return ;
93
+ }
94
+ await pushBranch ();
95
+ await createPr (repository: await framework.checkoutDirectory);
96
+ await authLogout ();
97
+ } on Exception catch (exception) {
98
+ final String message = _redactToken (exception.toString ());
99
+ throw Exception ('${exception .runtimeType }: $message ' );
100
+ }
77
101
}
78
102
79
- Future <void > updatePackages ({
103
+ // Ensure we don't leak the GitHub token in exception messages
104
+ String _redactToken (String message) => message.replaceAll (token, '[GitHub TOKEN]' );
105
+
106
+ /// Attempt to update all pub packages.
107
+ ///
108
+ /// Will return whether or not any changes were made.
109
+ Future <bool > updatePackages ({
80
110
bool verbose = true ,
81
- String author
= 'flutter-packages-autoroller <[email protected] >'
82
111
}) async {
112
+ final String author = '$githubUsername <$githubUsername @gmail.com>' ;
113
+
83
114
await framework.newBranch (await featureBranchName);
84
115
final io.Process flutterProcess = await framework.streamFlutter (< String > [
85
116
if (verbose) '--verbose' ,
@@ -90,18 +121,26 @@ This PR was generated by `flutter update-packages --force-upgrade`.
90
121
if (exitCode != 0 ) {
91
122
throw ConductorException ('Failed to update packages with exit code $exitCode ' );
92
123
}
124
+ // If the git checkout is clean, then pub packages are already at latest that cleanly resolve.
125
+ if (await framework.gitCheckoutClean ()) {
126
+ return false ;
127
+ }
93
128
await framework.commit (
94
129
'roll packages' ,
95
130
addFirst: true ,
96
131
author: author,
97
132
);
133
+ return true ;
98
134
}
99
135
100
136
Future <void > pushBranch () async {
137
+ final String projectName = framework.mirrorRemote! .url.split (r'/' ).last;
138
+ // Encode the token into the remote URL for authentication to work
139
+ final String remote = 'https://$token @$hostname /$orgName /$projectName ' ;
101
140
await framework.pushRef (
102
141
fromRef: await featureBranchName,
103
142
toRef: await featureBranchName,
104
- remote: framework.mirrorRemote ! .url ,
143
+ remote: remote ,
105
144
);
106
145
}
107
146
@@ -123,7 +162,7 @@ This PR was generated by `flutter update-packages --force-upgrade`.
123
162
'https' ,
124
163
'--with-token' ,
125
164
],
126
- stdin: token,
165
+ stdin: '$ token \n ' ,
127
166
);
128
167
}
129
168
@@ -151,6 +190,8 @@ This PR was generated by `flutter update-packages --force-upgrade`.
151
190
'$orgName :${await featureBranchName }' ,
152
191
'--base' ,
153
192
base ,
193
+ '--label' ,
194
+ 'tool' ,
154
195
if (draft)
155
196
'--draft' ,
156
197
],
@@ -165,13 +206,16 @@ This PR was generated by `flutter update-packages --force-upgrade`.
165
206
]);
166
207
}
167
208
168
- Future <void > cli (
209
+ /// Run a sub-process with the GitHub CLI client.
210
+ ///
211
+ /// Will return STDOUT of the sub-process.
212
+ Future <String > cli (
169
213
List <String > args, {
170
214
bool allowFailure = false ,
171
215
String ? stdin,
172
216
String ? workingDirectory,
173
217
}) async {
174
- print ('Executing "$githubClient ${args .join (' ' )}" in $workingDirectory ' );
218
+ log ('Executing "$githubClient ${args .join (' ' )}" in $workingDirectory ' );
175
219
final io.Process process = await processManager.start (
176
220
< String > [githubClient, ...args],
177
221
workingDirectory: workingDirectory,
@@ -203,6 +247,36 @@ This PR was generated by `flutter update-packages --force-upgrade`.
203
247
args,
204
248
);
205
249
}
206
- print (stdout);
250
+ log (stdout);
251
+ return stdout;
252
+ }
253
+
254
+ Future <bool > hasOpenPrs () async {
255
+ // gh pr list --author christopherfujino --repo flutter/flutter --state open --json number
256
+ final String openPrString = await cli (< String > [
257
+ 'pr' ,
258
+ 'list' ,
259
+ '--author' ,
260
+ githubUsername,
261
+ '--repo' ,
262
+ 'flutter/flutter' ,
263
+ '--state' ,
264
+ 'open' ,
265
+ // We are only interested in pub rolls, not devicelab flaky PRs
266
+ '--label' ,
267
+ 'tool' ,
268
+ // Return structured JSON with the PR numbers of open PRs
269
+ '--json' ,
270
+ 'number' ,
271
+ ]);
272
+
273
+ // This will be an array of objects, one for each open PR.
274
+ final List <Object ?> openPrs = json.decode (openPrString) as List <Object ?>;
275
+
276
+ if (openPrs.isNotEmpty) {
277
+ log ('$githubUsername already has open tool PRs:\n $openPrs ' );
278
+ return true ;
279
+ }
280
+ return false ;
207
281
}
208
282
}
0 commit comments