[flutter_plugin_tools] publish-plugin check against pub to determine if a release should happen (#4068)

This PR removes a TODO where we used to check if a release should happen against git tags, instead, we check against pub.

Also, when auto-publish and the package is manually released, the CI will continue to fail without this change.

Fixes flutter/flutter#81047
This commit is contained in:
Chris Yang
2021-06-24 12:46:24 -07:00
committed by GitHub
parent 356d316717
commit 552fceef91
3 changed files with 282 additions and 71 deletions

View File

@ -6,6 +6,7 @@
- `xctest` now supports running macOS tests in addition to iOS
- **Breaking change**: it now requires an `--ios` and/or `--macos` flag.
- The tooling now runs in strong null-safe mode.
- `publish plugins` check against pub.dev to determine if a release should happen.
- Modified the output format of `pubspec-check` and `xctest`
## 0.2.0

View File

@ -8,6 +8,7 @@ import 'dart:io' as io;
import 'package:file/file.dart';
import 'package:git/git.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
@ -18,6 +19,7 @@ import 'common/core.dart';
import 'common/git_version_finder.dart';
import 'common/plugin_command.dart';
import 'common/process_runner.dart';
import 'common/pub_version_finder.dart';
@immutable
class _RemoteInfo {
@ -49,7 +51,10 @@ class PublishPluginCommand extends PluginCommand {
Print print = print,
io.Stdin? stdinput,
GitDir? gitDir,
}) : _print = print,
http.Client? httpClient,
}) : _pubVersionFinder =
PubVersionFinder(httpClient: httpClient ?? http.Client()),
_print = print,
_stdin = stdinput ?? io.stdin,
super(packagesDir, processRunner: processRunner, gitDir: gitDir) {
argParser.addOption(
@ -131,6 +136,7 @@ class PublishPluginCommand extends PluginCommand {
final Print _print;
final io.Stdin _stdin;
StreamSubscription<String>? _stdinSubscription;
final PubVersionFinder _pubVersionFinder;
@override
Future<void> run() async {
@ -182,6 +188,8 @@ class PublishPluginCommand extends PluginCommand {
remoteForTagPush: remote,
);
}
_pubVersionFinder.httpClient.close();
await _finish(successful);
}
@ -196,6 +204,7 @@ class PublishPluginCommand extends PluginCommand {
_print('No version updates in this commit.');
return true;
}
_print('Getting existing tags...');
final io.ProcessResult existingTagsResult =
await baseGitDir.runCommand(<String>['tag', '--sort=-committerdate']);
@ -212,7 +221,6 @@ class PublishPluginCommand extends PluginCommand {
.childFile(pubspecPath);
final _CheckNeedsReleaseResult result = await _checkNeedsRelease(
pubspecFile: pubspecFile,
gitVersionFinder: gitVersionFinder,
existingTags: existingTags,
);
switch (result) {
@ -271,7 +279,6 @@ class PublishPluginCommand extends PluginCommand {
// Returns a [_CheckNeedsReleaseResult] that indicates the result.
Future<_CheckNeedsReleaseResult> _checkNeedsRelease({
required File pubspecFile,
required GitVersionFinder gitVersionFinder,
required List<String> existingTags,
}) async {
if (!pubspecFile.existsSync()) {
@ -293,19 +300,24 @@ Safe to ignore if the package is deleted in this commit.
return _CheckNeedsReleaseResult.failure;
}
// Get latest tagged version and compare with the current version.
// TODO(cyanglaz): Check latest version of the package on pub instead of git
// https://github.com/flutter/flutter/issues/81047
final String latestTag = existingTags.firstWhere(
(String tag) => tag.split('-v').first == pubspec.name,
// Check if the package named `packageName` with `version` has already published.
final Version version = pubspec.version!;
final PubVersionFinderResponse pubVersionFinderResponse =
await _pubVersionFinder.getPackageVersion(package: pubspec.name);
if (pubVersionFinderResponse.versions.contains(version)) {
final String tagsForPackageWithSameVersion = existingTags.firstWhere(
(String tag) =>
tag.split('-v').first == pubspec.name &&
tag.split('-v').last == version.toString(),
orElse: () => '');
if (latestTag.isNotEmpty) {
final String latestTaggedVersion = latestTag.split('-v').last;
final Version latestVersion = Version.parse(latestTaggedVersion);
if (pubspec.version! < latestVersion) {
_print(
'The new version (${pubspec.version}) is lower than the current version ($latestVersion) for ${pubspec.name}.\nThis git commit is a revert, no release is tagged.');
'The version $version of ${pubspec.name} has already been published');
if (tagsForPackageWithSameVersion.isEmpty) {
_print(
'However, the git release tag for this version (${pubspec.name}-v$version) is not found. Please manually fix the tag then run the command again.');
return _CheckNeedsReleaseResult.failure;
} else {
_print('skip.');
return _CheckNeedsReleaseResult.noRelease;
}
}

View File

@ -13,6 +13,8 @@ import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/common/process_runner.dart';
import 'package:flutter_plugin_tools/src/publish_plugin_command.dart';
import 'package:git/git.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
@ -427,6 +429,37 @@ void main() {
});
test('can release newly created plugins', () async {
const Map<String, dynamic> httpResponsePlugin1 = <String, dynamic>{
'name': 'plugin1',
'versions': <String>[],
};
const Map<String, dynamic> httpResponsePlugin2 = <String, dynamic>{
'name': 'plugin2',
'versions': <String>[],
};
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'plugin1.json') {
return http.Response(json.encode(httpResponsePlugin1), 200);
} else if (request.url.pathSegments.last == 'plugin2.json') {
return http.Response(json.encode(httpResponsePlugin2), 200);
}
return http.Response('', 500);
});
final PublishPluginCommand command = PublishPluginCommand(packagesDir,
processRunner: processRunner,
print: (Object? message) => printedMessages.add(message.toString()),
stdinput: mockStdin,
httpClient: mockClient,
gitDir: gitDir);
commandRunner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
commandRunner.addCommand(command);
// Non-federated
final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir);
// federated
@ -446,7 +479,6 @@ void main() {
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
@ -463,10 +495,50 @@ void main() {
test('can release newly created plugins, while there are existing plugins',
() async {
const Map<String, dynamic> httpResponsePlugin0 = <String, dynamic>{
'name': 'plugin0',
'versions': <String>['0.0.1'],
};
const Map<String, dynamic> httpResponsePlugin1 = <String, dynamic>{
'name': 'plugin1',
'versions': <String>[],
};
const Map<String, dynamic> httpResponsePlugin2 = <String, dynamic>{
'name': 'plugin2',
'versions': <String>[],
};
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'plugin0.json') {
return http.Response(json.encode(httpResponsePlugin0), 200);
} else if (request.url.pathSegments.last == 'plugin1.json') {
return http.Response(json.encode(httpResponsePlugin1), 200);
} else if (request.url.pathSegments.last == 'plugin2.json') {
return http.Response(json.encode(httpResponsePlugin2), 200);
}
return http.Response('', 500);
});
final PublishPluginCommand command = PublishPluginCommand(packagesDir,
processRunner: processRunner,
print: (Object? message) => printedMessages.add(message.toString()),
stdinput: mockStdin,
httpClient: mockClient,
gitDir: gitDir);
commandRunner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
commandRunner.addCommand(command);
// Prepare an exiting plugin and tag it
createFakePlugin('plugin0', packagesDir);
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
await gitDir.runCommand(<String>['tag', 'plugin0-v0.0.1']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
@ -489,7 +561,6 @@ void main() {
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
@ -505,6 +576,36 @@ void main() {
});
test('can release newly created plugins, dry run', () async {
const Map<String, dynamic> httpResponsePlugin1 = <String, dynamic>{
'name': 'plugin1',
'versions': <String>[],
};
const Map<String, dynamic> httpResponsePlugin2 = <String, dynamic>{
'name': 'plugin2',
'versions': <String>[],
};
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'plugin1.json') {
return http.Response(json.encode(httpResponsePlugin1), 200);
} else if (request.url.pathSegments.last == 'plugin2.json') {
return http.Response(json.encode(httpResponsePlugin2), 200);
}
return http.Response('', 500);
});
final PublishPluginCommand command = PublishPluginCommand(packagesDir,
processRunner: processRunner,
print: (Object? message) => printedMessages.add(message.toString()),
stdinput: mockStdin,
httpClient: mockClient,
gitDir: gitDir);
commandRunner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
commandRunner.addCommand(command);
// Non-federated
final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir);
// federated
@ -527,7 +628,6 @@ void main() {
'Checking local repo...',
'Local repo is ready!',
'=============== DRY RUN ===============',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Tagging release plugin1-v0.0.1...',
'Pushing tag to upstream...',
@ -541,6 +641,37 @@ void main() {
});
test('version change triggers releases.', () async {
const Map<String, dynamic> httpResponsePlugin1 = <String, dynamic>{
'name': 'plugin1',
'versions': <String>[],
};
const Map<String, dynamic> httpResponsePlugin2 = <String, dynamic>{
'name': 'plugin2',
'versions': <String>[],
};
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'plugin1.json') {
return http.Response(json.encode(httpResponsePlugin1), 200);
} else if (request.url.pathSegments.last == 'plugin2.json') {
return http.Response(json.encode(httpResponsePlugin2), 200);
}
return http.Response('', 500);
});
final PublishPluginCommand command = PublishPluginCommand(packagesDir,
processRunner: processRunner,
print: (Object? message) => printedMessages.add(message.toString()),
stdinput: mockStdin,
httpClient: mockClient,
gitDir: gitDir);
commandRunner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
commandRunner.addCommand(command);
// Non-federated
final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir);
// federated
@ -558,7 +689,6 @@ void main() {
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
@ -600,7 +730,6 @@ void main() {
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
@ -619,6 +748,37 @@ void main() {
test(
'delete package will not trigger publish but exit the command successfully.',
() async {
const Map<String, dynamic> httpResponsePlugin1 = <String, dynamic>{
'name': 'plugin1',
'versions': <String>[],
};
const Map<String, dynamic> httpResponsePlugin2 = <String, dynamic>{
'name': 'plugin2',
'versions': <String>[],
};
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'plugin1.json') {
return http.Response(json.encode(httpResponsePlugin1), 200);
} else if (request.url.pathSegments.last == 'plugin2.json') {
return http.Response(json.encode(httpResponsePlugin2), 200);
}
return http.Response('', 500);
});
final PublishPluginCommand command = PublishPluginCommand(packagesDir,
processRunner: processRunner,
print: (Object? message) => printedMessages.add(message.toString()),
stdinput: mockStdin,
httpClient: mockClient,
gitDir: gitDir);
commandRunner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
commandRunner.addCommand(command);
// Non-federated
final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir);
// federated
@ -636,7 +796,6 @@ void main() {
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
@ -677,7 +836,6 @@ void main() {
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'The file at The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n',
'Packages released: plugin1',
@ -691,18 +849,48 @@ void main() {
expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2');
});
test(
'versions revert do not trigger releases. Also prints out warning message.',
test('Exiting versions do not trigger release, also prints out message.',
() async {
const Map<String, dynamic> httpResponsePlugin1 = <String, dynamic>{
'name': 'plugin1',
'versions': <String>['0.0.2'],
};
const Map<String, dynamic> httpResponsePlugin2 = <String, dynamic>{
'name': 'plugin2',
'versions': <String>['0.0.2'],
};
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'plugin1.json') {
return http.Response(json.encode(httpResponsePlugin1), 200);
} else if (request.url.pathSegments.last == 'plugin2.json') {
return http.Response(json.encode(httpResponsePlugin2), 200);
}
return http.Response('', 500);
});
final PublishPluginCommand command = PublishPluginCommand(packagesDir,
processRunner: processRunner,
print: (Object? message) => printedMessages.add(message.toString()),
stdinput: mockStdin,
httpClient: mockClient,
gitDir: gitDir);
commandRunner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
commandRunner.addCommand(command);
// Non-federated
final Directory pluginDir1 =
createFakePlugin('plugin1', packagesDir, version: '0.0.2');
// federated
final Directory pluginDir2 = createFakePlugin(
'plugin2', packagesDir.childDirectory('plugin2'),
createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'),
version: '0.0.2');
await gitDir.runCommand(<String>['add', '-A']);
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
await gitDir.runCommand(<String>['tag', 'plugin1-v0.0.2']);
await gitDir.runCommand(<String>['tag', 'plugin2-v0.0.2']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
@ -713,54 +901,64 @@ void main() {
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'Running `pub publish ` in ${pluginDir1.path}...\n',
'Running `pub publish ` in ${pluginDir2.path}...\n',
'Packages released: plugin1, plugin2',
'The version 0.0.2 of plugin1 has already been published',
'skip.',
'The version 0.0.2 of plugin2 has already been published',
'skip.',
'Done!'
]));
expect(processRunner.pushTagsArgs, isNotEmpty);
expect(processRunner.pushTagsArgs[0], 'push');
expect(processRunner.pushTagsArgs[1], 'upstream');
expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2');
expect(processRunner.pushTagsArgs[3], 'push');
expect(processRunner.pushTagsArgs[4], 'upstream');
expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2');
processRunner.pushTagsArgs.clear();
printedMessages.clear();
expect(processRunner.pushTagsArgs, isEmpty);
});
final List<String> plugin1Pubspec =
pluginDir1.childFile('pubspec.yaml').readAsLinesSync();
plugin1Pubspec[plugin1Pubspec.indexWhere(
(String element) => element.contains('version:'))] = 'version: 0.0.1';
pluginDir1
.childFile('pubspec.yaml')
.writeAsStringSync(plugin1Pubspec.join('\n'));
final List<String> plugin2Pubspec =
pluginDir2.childFile('pubspec.yaml').readAsLinesSync();
plugin2Pubspec[plugin2Pubspec.indexWhere(
(String element) => element.contains('version:'))] = 'version: 0.0.1';
pluginDir2
.childFile('pubspec.yaml')
.writeAsStringSync(plugin2Pubspec.join('\n'));
test(
'Exiting versions do not trigger release, but fail if the tags do not exist.',
() async {
const Map<String, dynamic> httpResponsePlugin1 = <String, dynamic>{
'name': 'plugin1',
'versions': <String>['0.0.2'],
};
const Map<String, dynamic> httpResponsePlugin2 = <String, dynamic>{
'name': 'plugin2',
'versions': <String>['0.0.2'],
};
final MockClient mockClient = MockClient((http.Request request) async {
if (request.url.pathSegments.last == 'plugin1.json') {
return http.Response(json.encode(httpResponsePlugin1), 200);
} else if (request.url.pathSegments.last == 'plugin2.json') {
return http.Response(json.encode(httpResponsePlugin2), 200);
}
return http.Response('', 500);
});
final PublishPluginCommand command = PublishPluginCommand(packagesDir,
processRunner: processRunner,
print: (Object? message) => printedMessages.add(message.toString()),
stdinput: mockStdin,
httpClient: mockClient,
gitDir: gitDir);
commandRunner = CommandRunner<void>(
'publish_check_command',
'Test for publish-check command.',
);
commandRunner.addCommand(command);
// Non-federated
createFakePlugin('plugin1', packagesDir, version: '0.0.2');
// federated
createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'),
version: '0.0.2');
await gitDir.runCommand(<String>['add', '-A']);
await gitDir
.runCommand(<String>['commit', '-m', 'Update versions to 0.0.1']);
await commandRunner
.run(<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']);
expect(
printedMessages,
containsAllInOrder(<String>[
'Checking local repo...',
'Local repo is ready!',
'Getting existing tags...',
'The new version (0.0.1) is lower than the current version (0.0.2) for plugin1.\nThis git commit is a revert, no release is tagged.',
'The new version (0.0.1) is lower than the current version (0.0.2) for plugin2.\nThis git commit is a revert, no release is tagged.',
'Done!'
]));
await gitDir.runCommand(<String>['commit', '-m', 'Add plugins']);
// Immediately return 0 when running `pub publish`.
processRunner.mockPublishCompleteCode = 0;
mockStdin.readLineOutput = 'y';
await expectLater(
() => commandRunner.run(
<String>['publish-plugin', '--all-changed', '--base-sha=HEAD~']),
throwsA(const TypeMatcher<ToolExit>()));
expect(processRunner.pushTagsArgs, isEmpty);
});