Skip to content

Commit 6a4e2ff

Browse files
[tool] Add features to support GCB auto-publish flow (flutter#6218)
Adds the flowing to the tool: - A new `--exact-match-only` flag to be used with `--packages` to prevent group matching (i.e., a selection like `--packages=path_provider --exact-match-only` would only run on `packages/path_provider/path_provider`, not `packages/path_provider/*`). - Two new `publish` command flags: - `--tag-for-auto-publish`, to do all the steps that `publish` currently does except for the real `pub publish`, so it would dry-run the publish and then create and push the tag if successful. - `--already-tagged`, to skip the step of adding and pushing a tag, and replace it with a check that `HEAD` already has the expected tag. This set of additions supports a workflow where the current `release` step is changed to use `--tag-for-auto-publish`, and then the separate auto-publish system would publish each package with `... publish --already-tagged --packages=<some package> --exact-match-only`. See flutter/packages#5005 (comment) for previous discussion/context. Part of flutter#126827
1 parent 83b72ba commit 6a4e2ff

File tree

4 files changed

+239
-36
lines changed

4 files changed

+239
-36
lines changed

script/tool/lib/src/common/package_command.dart

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ abstract class PackageCommand extends Command<void> {
5050
argParser.addMultiOption(
5151
_packagesArg,
5252
help:
53-
'Specifies which packages the command should run on (before sharding).\n',
53+
'Specifies which packages the command should run on (before sharding).\n'
54+
'If a package name is the name of a plugin group, it will include '
55+
'the entire group; to avoid this, use group/package as the name '
56+
'(e.g., shared_preferences/shared_preferences), or pass '
57+
'--$_exactMatchOnlyArg',
5458
valueHelp: 'package1,package2,...',
5559
aliases: <String>[_pluginsLegacyAliasArg],
5660
);
@@ -67,6 +71,9 @@ abstract class PackageCommand extends Command<void> {
6771
valueHelp: 'n',
6872
defaultsTo: '1',
6973
);
74+
argParser.addFlag(_exactMatchOnlyArg,
75+
help: 'Disables package group matching in package selection.',
76+
negatable: false);
7077
argParser.addMultiOption(
7178
_excludeArg,
7279
abbr: 'e',
@@ -136,6 +143,7 @@ abstract class PackageCommand extends Command<void> {
136143
static const String _pluginsLegacyAliasArg = 'plugins';
137144
static const String _runOnChangedPackagesArg = 'run-on-changed-packages';
138145
static const String _runOnDirtyPackagesArg = 'run-on-dirty-packages';
146+
static const String _exactMatchOnlyArg = 'exact-match-only';
139147
static const String _excludeArg = 'exclude';
140148
static const String _filterPackagesArg = 'filter-packages-to';
141149
// Diff base selection.
@@ -361,6 +369,15 @@ abstract class PackageCommand extends Command<void> {
361369
throw ToolExit(exitInvalidArguments);
362370
}
363371

372+
// Whether to require that a package name exactly match to be included,
373+
// rather than allowing package groups for federated plugins. Any cases
374+
// where the set of packages is determined programatically based on repo
375+
// state should use exact matching.
376+
final bool allowGroupMatching = !(getBoolArg(_exactMatchOnlyArg) ||
377+
argResults!.wasParsed(_runOnChangedPackagesArg) ||
378+
argResults!.wasParsed(_runOnDirtyPackagesArg) ||
379+
argResults!.wasParsed(_packagesForBranchArg));
380+
364381
Set<String> packages = Set<String>.from(getStringListArg(_packagesArg));
365382

366383
final GitVersionFinder? changedFileFinder;
@@ -458,6 +475,30 @@ abstract class PackageCommand extends Command<void> {
458475
excludeAllButPackageNames.intersection(possibleNames).isEmpty;
459476
}
460477

478+
await for (final RepositoryPackage package in _everyTopLevelPackage()) {
479+
if (packages.isEmpty ||
480+
packages
481+
.intersection(_possiblePackageIdentifiers(package,
482+
allowGroup: allowGroupMatching))
483+
.isNotEmpty) {
484+
// Exclusion is always human input, so groups should always be allowed
485+
// unless they have been specifically forbidden.
486+
final bool excluded = isExcluded(_possiblePackageIdentifiers(package,
487+
allowGroup: !getBoolArg(_exactMatchOnlyArg)));
488+
yield PackageEnumerationEntry(package, excluded: excluded);
489+
}
490+
}
491+
}
492+
493+
/// Returns every top-level package in the repository, according to repository
494+
/// conventions.
495+
///
496+
/// In particular, it returns:
497+
/// - Every package that is a direct child of one of the know "packages"
498+
/// directories.
499+
/// - Every package that is a direct child of a non-package subdirectory of
500+
/// one of those directories (to cover federated plugin groups).
501+
Stream<RepositoryPackage> _everyTopLevelPackage() async* {
461502
for (final Directory dir in <Directory>[
462503
packagesDir,
463504
if (thirdPartyPackagesDir.existsSync()) thirdPartyPackagesDir,
@@ -466,40 +507,44 @@ abstract class PackageCommand extends Command<void> {
466507
in dir.list(followLinks: false)) {
467508
// A top-level Dart package is a standard package.
468509
if (isPackage(entity)) {
469-
if (packages.isEmpty || packages.contains(p.basename(entity.path))) {
470-
yield PackageEnumerationEntry(
471-
RepositoryPackage(entity as Directory),
472-
excluded: isExcluded(<String>{entity.basename}));
473-
}
510+
yield RepositoryPackage(entity as Directory);
474511
} else if (entity is Directory) {
475512
// Look for Dart packages under this top-level directory; this is the
476513
// standard structure for federated plugins.
477514
await for (final FileSystemEntity subdir
478515
in entity.list(followLinks: false)) {
479516
if (isPackage(subdir)) {
480-
// There are three ways for a federated plugin to match:
481-
// - package name (path_provider_android)
482-
// - fully specified name (path_provider/path_provider_android)
483-
// - group name (path_provider), which matches all packages in
484-
// the group
485-
final Set<String> possibleMatches = <String>{
486-
path.basename(subdir.path), // package name
487-
path.basename(entity.path), // group name
488-
path.relative(subdir.path, from: dir.path), // fully specified
489-
};
490-
if (packages.isEmpty ||
491-
packages.intersection(possibleMatches).isNotEmpty) {
492-
yield PackageEnumerationEntry(
493-
RepositoryPackage(subdir as Directory),
494-
excluded: isExcluded(possibleMatches));
495-
}
517+
yield RepositoryPackage(subdir as Directory);
496518
}
497519
}
498520
}
499521
}
500522
}
501523
}
502524

525+
Set<String> _possiblePackageIdentifiers(
526+
RepositoryPackage package, {
527+
required bool allowGroup,
528+
}) {
529+
final String packageName = path.basename(package.path);
530+
if (package.isFederated) {
531+
// There are three ways for a federated plugin to be identified:
532+
// - package name (path_provider_android).
533+
// - fully specified name (path_provider/path_provider_android).
534+
// - group name (path_provider), which includes all packages in
535+
// the group.
536+
final io.Directory parentDir = package.directory.parent;
537+
return <String>{
538+
packageName,
539+
path.relative(package.path,
540+
from: parentDir.parent.path), // fully specified
541+
if (allowGroup) path.basename(parentDir.path), // group name
542+
};
543+
} else {
544+
return <String>{packageName};
545+
}
546+
}
547+
503548
/// Returns all Dart package folders (typically, base package + example) of
504549
/// the packages involved in this command execution.
505550
///

script/tool/lib/src/publish_command.dart

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ class PublishCommand extends PackageLoopingCommand {
5858
}) : _pubVersionFinder =
5959
PubVersionFinder(httpClient: httpClient ?? http.Client()),
6060
_stdin = stdinput ?? io.stdin {
61+
argParser.addFlag(_alreadyTaggedFlag,
62+
help:
63+
'Instead of tagging, validates that the current checkout is already tagged with the expected version.\n'
64+
'This is primarily intended for use in CI publish steps triggered by tagging.',
65+
negatable: false);
6166
argParser.addMultiOption(_pubFlagsOption,
6267
help:
6368
'A list of options that will be forwarded on to pub. Separate multiple flags with commas.');
@@ -83,13 +88,20 @@ class PublishCommand extends PackageLoopingCommand {
8388
argParser.addFlag(_skipConfirmationFlag,
8489
help: 'Run the command without asking for Y/N inputs.\n'
8590
'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n');
91+
argParser.addFlag(_tagForAutoPublishFlag,
92+
help:
93+
'Runs the dry-run publish, and tags if it succeeds, but does not actually publish.\n'
94+
'This is intended for use with a separate publish step that is based on tag push events.',
95+
negatable: false);
8696
}
8797

98+
static const String _alreadyTaggedFlag = 'already-tagged';
8899
static const String _pubFlagsOption = 'pub-publish-flags';
89100
static const String _remoteOption = 'remote';
90101
static const String _allChangedFlag = 'all-changed';
91102
static const String _dryRunFlag = 'dry-run';
92103
static const String _skipConfirmationFlag = 'skip-confirmation';
104+
static const String _tagForAutoPublishFlag = 'tag-for-auto-publish';
93105

94106
static const String _pubCredentialName = 'PUB_CREDENTIALS';
95107

@@ -193,15 +205,27 @@ class PublishCommand extends PackageLoopingCommand {
193205
return PackageResult.fail(<String>['uncommitted changes']);
194206
}
195207

196-
if (!await _publish(package)) {
197-
return PackageResult.fail(<String>['publish failed']);
208+
final bool tagOnly = getBoolArg(_tagForAutoPublishFlag);
209+
if (!tagOnly) {
210+
if (!await _publish(package)) {
211+
return PackageResult.fail(<String>['publish failed']);
212+
}
198213
}
199214

200-
if (!await _tagRelease(package)) {
201-
return PackageResult.fail(<String>['tagging failed']);
215+
final String tag = _getTag(package);
216+
if (getBoolArg(_alreadyTaggedFlag)) {
217+
if (!(await _getCurrentTags()).contains(tag)) {
218+
printError('The current checkout is not already tagged "$tag"');
219+
return PackageResult.fail(<String>['missing tag']);
220+
}
221+
} else {
222+
if (!await _tagRelease(package, tag)) {
223+
return PackageResult.fail(<String>['tagging failed']);
224+
}
202225
}
203226

204-
print('\nPublished ${package.directory.basename} successfully!');
227+
final String action = tagOnly ? 'Tagged' : 'Published';
228+
print('\n$action ${package.directory.basename} successfully!');
205229
return PackageResult.success();
206230
}
207231

@@ -277,8 +301,7 @@ Safe to ignore if the package is deleted in this commit.
277301
// Tag the release with <package-name>-v<version>, and push it to the remote.
278302
//
279303
// Return `true` if successful, `false` otherwise.
280-
Future<bool> _tagRelease(RepositoryPackage package) async {
281-
final String tag = _getTag(package);
304+
Future<bool> _tagRelease(RepositoryPackage package, String tag) async {
282305
print('Tagging release $tag...');
283306
if (!getBoolArg(_dryRunFlag)) {
284307
final io.ProcessResult result = await (await gitDir).runCommand(
@@ -301,6 +324,22 @@ Safe to ignore if the package is deleted in this commit.
301324
return success;
302325
}
303326

327+
Future<Iterable<String>> _getCurrentTags() async {
328+
// git tag --points-at HEAD
329+
final io.ProcessResult tagsResult = await (await gitDir).runCommand(
330+
<String>['tag', '--points-at', 'HEAD'],
331+
throwOnError: false,
332+
);
333+
if (tagsResult.exitCode != 0) {
334+
return <String>[];
335+
}
336+
337+
return (tagsResult.stdout as String)
338+
.split('\n')
339+
.map((String line) => line.trim())
340+
.where((String line) => line.isNotEmpty);
341+
}
342+
304343
Future<bool> _checkGitStatus(RepositoryPackage package) async {
305344
final io.ProcessResult statusResult = await (await gitDir).runCommand(
306345
<String>[

script/tool/test/common/package_command_test.dart

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,6 @@ void main() {
222222
test(
223223
'explicitly specifying the plugin (group) name of a federated plugin '
224224
'should include all plugins in the group', () async {
225-
processRunner.mockProcessesForExecutable['git-diff'] = <FakeProcessInfo>[
226-
FakeProcessInfo(MockProcess(stdout: '''
227-
packages/plugin1/plugin1/plugin1.dart
228-
''')),
229-
];
230225
final Directory pluginGroup = packagesDir.childDirectory('plugin1');
231226
final RepositoryPackage appFacingPackage =
232227
createFakePlugin('plugin1', pluginGroup);
@@ -235,8 +230,7 @@ packages/plugin1/plugin1/plugin1.dart
235230
final RepositoryPackage implementationPackage =
236231
createFakePlugin('plugin1_web', pluginGroup);
237232

238-
await runCapturingPrint(
239-
runner, <String>['sample', '--base-sha=main', '--packages=plugin1']);
233+
await runCapturingPrint(runner, <String>['sample', '--packages=plugin1']);
240234

241235
expect(
242236
command.plugins,
@@ -247,6 +241,21 @@ packages/plugin1/plugin1/plugin1.dart
247241
]));
248242
});
249243

244+
test(
245+
'specifying the app-facing package of a federated plugin with '
246+
'--exact-match-only should only include only that package', () async {
247+
final Directory pluginGroup = packagesDir.childDirectory('plugin1');
248+
final RepositoryPackage appFacingPackage =
249+
createFakePlugin('plugin1', pluginGroup);
250+
createFakePlugin('plugin1_platform_interface', pluginGroup);
251+
createFakePlugin('plugin1_web', pluginGroup);
252+
253+
await runCapturingPrint(runner,
254+
<String>['sample', '--packages=plugin1', '--exact-match-only']);
255+
256+
expect(command.plugins, unorderedEquals(<String>[appFacingPackage.path]));
257+
});
258+
250259
test(
251260
'specifying the app-facing package of a federated plugin using its '
252261
'fully qualified name should include only that package', () async {

0 commit comments

Comments
 (0)