diff --git a/app/lib/admin/actions/actions.dart b/app/lib/admin/actions/actions.dart index 1cd07723b5..378a627083 100644 --- a/app/lib/admin/actions/actions.dart +++ b/app/lib/admin/actions/actions.dart @@ -18,7 +18,9 @@ import 'moderation_case_list.dart'; import 'moderation_case_resolve.dart'; import 'moderation_case_update.dart'; import 'moderation_transparency_metrics.dart'; +import 'package_discontinue.dart'; import 'package_info.dart'; +import 'package_latest_update.dart'; import 'package_reservation_create.dart'; import 'package_reservation_delete.dart'; import 'package_reservation_list.dart'; @@ -102,7 +104,9 @@ final class AdminAction { moderationCaseResolve, moderationCaseUpdate, moderationTransparencyMetrics, + packageDiscontinue, packageInfo, + packageLatestUpdate, packageReservationCreate, packageReservationDelete, packageReservationList, diff --git a/app/lib/admin/actions/package_discontinue.dart b/app/lib/admin/actions/package_discontinue.dart new file mode 100644 index 0000000000..1c7132d428 --- /dev/null +++ b/app/lib/admin/actions/package_discontinue.dart @@ -0,0 +1,67 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:clock/clock.dart'; +import 'package:pub_dev/package/backend.dart'; +import 'package:pub_dev/package/models.dart'; +import 'package:pub_dev/shared/datastore.dart'; + +import 'actions.dart'; + +final packageDiscontinue = AdminAction( + name: 'package-discontinue', + summary: 'Sets the package discontinued.', + description: ''' +Sets the `Package.isDiscontinued` and `Package.replacedBy` properties. +''', + options: { + 'package': 'The package to be discontinued.', + 'value': 'The value to set (defaults to true).', + 'replaced-by': + 'The Package.replacedBy field (if not set will be set to `null`).', + }, + invoke: (options) async { + final package = options['package']; + InvalidInputException.check( + package != null && package.isNotEmpty, + '`package` must be given', + ); + final value = options['value'] ?? 'true'; + InvalidInputException.checkAnyOf(value, 'value', ['true', 'false']); + final valueToSet = value == 'true'; + + final p = await packageBackend.lookupPackage(package!); + if (p == null) { + throw NotFoundException.resource(package); + } + + final replacedBy = options['replaced-by']; + if (replacedBy != null) { + final rp = await packageBackend.lookupPackage(replacedBy); + if (rp == null) { + throw NotFoundException('Replacing package "$replacedBy" not found.'); + } + } + + final info = await withRetryTransaction(dbService, (tx) async { + final pkg = await tx.lookupOrNull(p.key); + if (pkg == null) { + throw NotFoundException.resource(package); + } + pkg.isDiscontinued = valueToSet; + pkg.replacedBy = valueToSet ? replacedBy : null; + pkg.updated = clock.now().toUtc(); + tx.insert(pkg); + return pkg; + }); + + return { + 'package': { + 'name': info.name, + 'isDiscontinued': info.isDiscontinued, + 'replacedBy': info.replacedBy, + }, + }; + }, +); diff --git a/app/lib/admin/actions/package_latest_update.dart b/app/lib/admin/actions/package_latest_update.dart new file mode 100644 index 0000000000..2099a4b983 --- /dev/null +++ b/app/lib/admin/actions/package_latest_update.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:pub_dev/package/backend.dart'; + +import 'actions.dart'; + +final packageLatestUpdate = AdminAction( + name: 'package-latest-update', + summary: 'Updates the latest version of a package or all packages.', + description: ''' +Ensures Package.latestVersion / latestPreviewVersion / latestPrereleaseVersion is up-to-date. + +When no package is specified, all packages will be updated. +''', + options: { + 'package': 'The package to be updated (optional).', + 'concurrency': + 'The concurrently running update operations (defaults to 10).', + }, + invoke: (options) async { + final package = options['package']; + final concurrency = int.parse(options['concurrency'] ?? '10'); + + if (package != null) { + final updated = await packageBackend.updatePackageVersions(package); + return { + 'updated': updated, + }; + } else { + final stat = await packageBackend.updateAllPackageVersions( + concurrency: concurrency); + return { + 'updatedCount': stat, + }; + } + }, +); diff --git a/app/lib/admin/backend.dart b/app/lib/admin/backend.dart index e76878ce28..7b6e30c5ff 100644 --- a/app/lib/admin/backend.dart +++ b/app/lib/admin/backend.dart @@ -44,13 +44,11 @@ import 'tools/delete_all_staging.dart'; import 'tools/list_package_blocked.dart'; import 'tools/list_tools.dart'; import 'tools/notify_service.dart'; -import 'tools/package_discontinued.dart'; import 'tools/package_publisher.dart'; import 'tools/publisher_member.dart'; import 'tools/recent_uploaders.dart'; import 'tools/set_package_blocked.dart'; import 'tools/set_user_blocked.dart'; -import 'tools/update_package_versions.dart'; import 'tools/user_merger.dart'; final _logger = Logger('pub.admin.backend'); @@ -69,9 +67,7 @@ final Map availableTools = { 'delete-all-staging': executeDeleteAllStaging, 'list-package-blocked': executeListPackageBlocked, 'notify-service': executeNotifyService, - 'package-discontinued': executeSetPackageDiscontinued, 'package-publisher': executeSetPackagePublisher, - 'update-package-versions': executeUpdatePackageVersions, 'recent-uploaders': executeRecentUploaders, 'publisher-member': executePublisherMember, 'publisher-invite-member': executePublisherInviteMember, diff --git a/app/lib/admin/tools/package_discontinued.dart b/app/lib/admin/tools/package_discontinued.dart deleted file mode 100644 index d8a862304d..0000000000 --- a/app/lib/admin/tools/package_discontinued.dart +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:args/args.dart'; -import 'package:clock/clock.dart'; - -import 'package:pub_dev/package/backend.dart'; -import 'package:pub_dev/package/models.dart'; -import 'package:pub_dev/shared/datastore.dart'; -import 'package:pub_dev/task/backend.dart'; - -final _argParser = ArgParser() - ..addOption('package', help: 'The package to update.') - ..addFlag('discontinued', help: 'The `isDiscontinued` value to set.') - ..addOption('replaced-by', help: 'The `replacedBy` value to set.') - ..addFlag('help', abbr: 'h', defaultsTo: false, help: 'Show help.'); - -Future executeSetPackageDiscontinued(List args) async { - final argv = _argParser.parse(args); - final packageName = argv['package'] as String?; - final discontinued = argv['discontinued'] as bool?; - final replacedBy = argv['replaced-by'] as String?; - - if (argv['help'] as bool || packageName == null || discontinued == null) { - return 'Sets the `isDiscontinued` and `replacedBy` fields for a `package`.\n' - ' --package --discontinued --replaced-by \n' - '${_argParser.usage}'; - } - - final package = await packageBackend.lookupPackage(packageName); - if (package == null) { - return 'Package $packageName not found'; - } - if (package.isDiscontinued == discontinued && - package.replacedBy == replacedBy) { - return 'No update needed.'; - } - if (replacedBy != null) { - final rp = await packageBackend.lookupPackage(replacedBy); - if (rp == null) { - return '`$replacedBy` not found.'; - } - } - await withRetryTransaction(dbService, (tx) async { - final pkg = await tx.lookupValue(package.key); - pkg.isDiscontinued = discontinued; - pkg.replacedBy = replacedBy; - pkg.updated = clock.now().toUtc(); - tx.insert(pkg); - }); - await purgePackageCache(packageName); - await taskBackend.trackPackage(packageName); - return 'Done.'; -} diff --git a/app/lib/admin/tools/update_package_versions.dart b/app/lib/admin/tools/update_package_versions.dart deleted file mode 100644 index 164fc53354..0000000000 --- a/app/lib/admin/tools/update_package_versions.dart +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:args/args.dart'; - -import 'package:pub_dev/package/backend.dart'; - -final _argParser = ArgParser() - ..addOption('concurrency', - abbr: 'c', defaultsTo: '1', help: 'Number of concurrent processing.') - ..addOption('package', abbr: 'p', help: 'The package to update.') - ..addFlag('help', abbr: 'h', defaultsTo: false, help: 'Show help.'); - -Future executeUpdatePackageVersions(List args) async { - final argv = _argParser.parse(args); - if (argv['help'] as bool) { - return 'Ensures Package.latestVersion / latestPreviewVersion / latestPrereleaseVersion is up-to-date.\n' - 'Usage: --package [pkg] -- updates package\n' - '${_argParser.usage}'; - } - - final concurrency = int.parse(argv['concurrency'] as String); - final package = argv['package'] as String?; - - if (package != null) { - final stat = await packageBackend.updatePackageVersions(package); - return stat ? 'Updated.' : 'No change.'; - } else { - final stat = - await packageBackend.updateAllPackageVersions(concurrency: concurrency); - return 'Updated $stat packages.'; - } -} diff --git a/app/test/admin/package_actions_test.dart b/app/test/admin/package_actions_test.dart index 88d68e0f82..a3995abd3a 100644 --- a/app/test/admin/package_actions_test.dart +++ b/app/test/admin/package_actions_test.dart @@ -28,5 +28,47 @@ void main() { } }); }); + + testWithProfile('discontinue', fn: () async { + final client = createPubApiClient(authToken: siteAdminToken); + final rs = await client.adminInvokeAction( + 'package-discontinue', + AdminInvokeActionArguments(arguments: { + 'package': 'oxygen', + 'replaced-by': 'neon', + }), + ); + expect(rs.output, { + 'package': { + 'name': 'oxygen', + 'isDiscontinued': true, + 'replacedBy': 'neon', + }, + }); + }); + + testWithProfile('update latest on a single package', fn: () async { + final client = createPubApiClient(authToken: siteAdminToken); + final rs = await client.adminInvokeAction( + 'package-latest-update', + AdminInvokeActionArguments(arguments: { + 'package': 'oxygen', + }), + ); + expect(rs.output, { + 'updated': false, + }); + }); + + testWithProfile('update latest on all packages', fn: () async { + final client = createPubApiClient(authToken: siteAdminToken); + final rs = await client.adminInvokeAction( + 'package-latest-update', + AdminInvokeActionArguments(arguments: {}), + ); + expect(rs.output, { + 'updatedCount': 0, + }); + }); }); }