diff --git a/CHANGELOG.md b/CHANGELOG.md index 379ec3f56d..2690fc572b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Important changes to data models, configuration, and migrations between each AppEngine version, listed here to ease deployment and troubleshooting. ## Next Release (replace with git tag when deployed) + * Note: after the release we should update the load balancer rules to also include the `/api/packages//feed.atom` URLs. ## `20250403t085600-all` * Bump runtimeVersion to `2025.04.01`. diff --git a/app/lib/admin/actions/moderate_package.dart b/app/lib/admin/actions/moderate_package.dart index e0d0948a8c..3e3203ce97 100644 --- a/app/lib/admin/actions/moderate_package.dart +++ b/app/lib/admin/actions/moderate_package.dart @@ -79,6 +79,9 @@ Note: the action may take a longer time to complete as the public archive bucket return pkg; }); + // make sure visibility cache is updated immediately + await purgePackageCache(package); + // sync exported API(s) await apiExporter?.synchronizePackage(package, forceDelete: true); diff --git a/app/lib/admin/actions/moderate_package_versions.dart b/app/lib/admin/actions/moderate_package_versions.dart index 601c26d810..9a653fbacc 100644 --- a/app/lib/admin/actions/moderate_package_versions.dart +++ b/app/lib/admin/actions/moderate_package_versions.dart @@ -112,6 +112,9 @@ Set the moderated flag on a package version (updating the flag and the timestamp return v; }); + // make sure visibility cache is updated immediately + await purgePackageCache(package); + // sync exported API(s) await apiExporter?.synchronizePackage(package, forceDelete: true); diff --git a/app/lib/frontend/handlers/atom_feed.dart b/app/lib/frontend/handlers/atom_feed.dart index 2eb955283c..69dbd7e4bf 100644 --- a/app/lib/frontend/handlers/atom_feed.dart +++ b/app/lib/frontend/handlers/atom_feed.dart @@ -32,7 +32,7 @@ Future allPackagesAtomFeedhandler(shelf.Request request) async { ); } -/// Handles requests for `/packages//feed.atom` +/// Handles requests for `/api/packages//feed.atom` Future packageAtomFeedhandler( shelf.Request request, String package, diff --git a/app/lib/frontend/handlers/pubapi.client.dart b/app/lib/frontend/handlers/pubapi.client.dart index d0906302c7..f88ec73f89 100644 --- a/app/lib/frontend/handlers/pubapi.client.dart +++ b/app/lib/frontend/handlers/pubapi.client.dart @@ -132,6 +132,13 @@ class PubApiClient { )); } + Future> packageAtomFeed(String package) async { + return await _client.requestBytes( + verb: 'get', + path: '/api/packages/$package/feed.atom', + ); + } + Future<_i5.PublisherInfo> createPublisher(String publisherId) async { return _i5.PublisherInfo.fromJson(await _client.requestJson( verb: 'post', diff --git a/app/lib/frontend/handlers/pubapi.dart b/app/lib/frontend/handlers/pubapi.dart index fe8a855e73..e58424f76e 100644 --- a/app/lib/frontend/handlers/pubapi.dart +++ b/app/lib/frontend/handlers/pubapi.dart @@ -9,6 +9,7 @@ import 'package:_pub_shared/data/package_api.dart'; import 'package:_pub_shared/data/publisher_api.dart'; import 'package:_pub_shared/data/task_api.dart'; import 'package:api_builder/api_builder.dart'; +import 'package:pub_dev/frontend/handlers/atom_feed.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf_router/shelf_router.dart'; @@ -215,6 +216,11 @@ class PubApi { ) async => await packageBackend.inviteUploader(package, invite); + /// Renders the Atom XML feed for the package. + @EndPoint.get('/api/packages//feed.atom') + Future packageAtomFeed(Request request, String package) => + packageAtomFeedhandler(request, package); + // **** // **** Publisher API // **** diff --git a/app/lib/frontend/handlers/pubapi.g.dart b/app/lib/frontend/handlers/pubapi.g.dart index ea639aebe1..82c4a6b11c 100644 --- a/app/lib/frontend/handlers/pubapi.g.dart +++ b/app/lib/frontend/handlers/pubapi.g.dart @@ -254,6 +254,26 @@ Router _$PubApiRouter(PubApi service) { } }, ); + router.add( + 'GET', + r'/api/packages//feed.atom', + ( + Request request, + String package, + ) async { + try { + final _$result = await service.packageAtomFeed( + request, + package, + ); + return _$result; + } on ApiResponseException catch (e) { + return e.asApiResponse(); + } catch (e, st) { + return $utilities.unhandledError(e, st); + } + }, + ); router.add( 'POST', r'/api/publishers/', diff --git a/app/lib/frontend/handlers/routes.dart b/app/lib/frontend/handlers/routes.dart index c370d591d5..ec36d47235 100644 --- a/app/lib/frontend/handlers/routes.dart +++ b/app/lib/frontend/handlers/routes.dart @@ -176,11 +176,6 @@ class PubSiteService { Future packageVersions(Request request, String package) => packageVersionsListHandler(request, package); - /// Renders the Atom XML feed for the package. - @Route.get('/packages//feed.atom') - Future packageAtomFeed(Request request, String package) => - packageAtomFeedhandler(request, package); - @Route.get('/packages/') Future package(Request request, String package) => packageVersionHandlerHtml(request, package); diff --git a/app/lib/frontend/handlers/routes.g.dart b/app/lib/frontend/handlers/routes.g.dart index 962c484718..4a05a2e5e6 100644 --- a/app/lib/frontend/handlers/routes.g.dart +++ b/app/lib/frontend/handlers/routes.g.dart @@ -158,11 +158,6 @@ Router _$PubSiteServiceRouter(PubSiteService service) { r'/packages//versions', service.packageVersions, ); - router.add( - 'GET', - r'/packages//feed.atom', - service.packageAtomFeed, - ); router.add( 'GET', r'/packages/', diff --git a/app/lib/package/api_export/api_exporter.dart b/app/lib/package/api_export/api_exporter.dart index 2c29c0f47b..4215cd9637 100644 --- a/app/lib/package/api_export/api_exporter.dart +++ b/app/lib/package/api_export/api_exporter.dart @@ -244,6 +244,10 @@ final class ApiExporter { versionListing, forceWrite: forceWrite, ); + await _api.package(package).feedAtomFile.write( + await buildPackageAtomFeedContent(package), + forceWrite: forceWrite, + ); } /// Scan for updates from packages until [abort] is resolved, or [claim] diff --git a/app/lib/package/api_export/exported_api.dart b/app/lib/package/api_export/exported_api.dart index 2cd7f4c736..92b61fc525 100644 --- a/app/lib/package/api_export/exported_api.dart +++ b/app/lib/package/api_export/exported_api.dart @@ -273,6 +273,13 @@ final class ExportedPackage { ExportedJsonFile get advisories => _suffix('/advisories'); + /// Interface for writing `/api/packages//feed.atom` + ExportedAtomFeedFile get feedAtomFile => ExportedAtomFeedFile._( + _owner, + '/api/packages/$_package/feed.atom', + Duration(hours: 12), + ); + /// Interace for writing `/api/archives/-.tar.gz`. ExportedBlob tarball(String version) => ExportedBlob._( _owner, @@ -399,6 +406,7 @@ final class ExportedPackage { await Future.wait([ _owner._pool.withResource(() async => await versions.delete()), _owner._pool.withResource(() async => await advisories.delete()), + _owner._pool.withResource(() async => await feedAtomFile.delete()), ..._owner._prefixes.map((prefix) async { await _owner._listBucket( prefix: prefix + '/api/archives/$_package-', diff --git a/app/lib/shared/urls.dart b/app/lib/shared/urls.dart index 592ac254a0..1ae139c52e 100644 --- a/app/lib/shared/urls.dart +++ b/app/lib/shared/urls.dart @@ -135,7 +135,7 @@ String pkgArchiveDownloadUrl(String package, String version, {Uri? baseUri}) { } String pkgFeedUrl(String package) { - return '/packages/$package/feed.atom'; + return '/api/packages/$package/feed.atom'; } String pkgDocUrl( diff --git a/app/test/admin/exported_api_sync_test.dart b/app/test/admin/exported_api_sync_test.dart index cf27de267e..e8028ca5e4 100644 --- a/app/test/admin/exported_api_sync_test.dart +++ b/app/test/admin/exported_api_sync_test.dart @@ -82,11 +82,13 @@ void main() { '$runtimeVersion/api/archives/oxygen-2.0.0-dev.tar.gz', '$runtimeVersion/api/packages/oxygen', '$runtimeVersion/api/packages/oxygen/advisories', + '$runtimeVersion/api/packages/oxygen/feed.atom', 'latest/api/archives/oxygen-1.0.0.tar.gz', 'latest/api/archives/oxygen-1.2.0.tar.gz', 'latest/api/archives/oxygen-2.0.0-dev.tar.gz', 'latest/api/packages/oxygen', 'latest/api/packages/oxygen/advisories', + 'latest/api/packages/oxygen/feed.atom', }); final oxygenDataJson = data['latest/api/packages/oxygen']; diff --git a/app/test/frontend/handlers/atom_feed_test.dart b/app/test/frontend/handlers/atom_feed_test.dart index 659cc5809d..e6febf7783 100644 --- a/app/test/frontend/handlers/atom_feed_test.dart +++ b/app/test/frontend/handlers/atom_feed_test.dart @@ -77,9 +77,9 @@ void main() { }); }); - testWithProfile('/packages//feed.atom', fn: () async { + testWithProfile('/api/packages//feed.atom', fn: () async { final content = await expectAtomXmlResponse( - await issueGet('/packages/oxygen/feed.atom')); + await issueGet('/api/packages/oxygen/feed.atom')); // check if content is valid XML final root = XmlDocument.parse(content); final feed = root.rootElement; @@ -110,11 +110,11 @@ void main() { entries.forEach((e) => e.parent!.children.remove(e)); final restExp = RegExp('\n' - ' ${activeConfiguration.primarySiteUri}/packages/oxygen/feed.atom\n' + ' ${activeConfiguration.primarySiteUri}/api/packages/oxygen/feed.atom\n' ' Recently published versions of package oxygen on pub.dev\n' ' (.*)\n' ' \n' - ' \n' + ' \n' ' Pub Feed Generator\n' ' oxygen is awesome\n' '(\\s*)' diff --git a/app/test/package/api_export/api_exporter_test.dart b/app/test/package/api_export/api_exporter_test.dart index 27c264ef70..76b0c0926d 100644 --- a/app/test/package/api_export/api_exporter_test.dart +++ b/app/test/package/api_export/api_exporter_test.dart @@ -131,6 +131,10 @@ Future _testExportedApiSynchronization( await bucket.readBytes('$runtimeVersion/api/archives/foo-1.0.0.tar.gz'), isNotNull, ); + expect( + await bucket.readBytes('$runtimeVersion/api/packages/foo/feed.atom'), + isNotNull, + ); expect( await bucket.readGzippedJson('$runtimeVersion/api/packages/foo'), { @@ -152,6 +156,10 @@ Future _testExportedApiSynchronization( await bucket.readString('$runtimeVersion/feed.atom'), contains('v1.0.0 of foo'), ); + expect( + await bucket.readString('$runtimeVersion/api/packages/foo/feed.atom'), + contains('v1.0.0 of foo'), + ); } _log.info('## New package'); @@ -185,6 +193,10 @@ Future _testExportedApiSynchronization( await bucket.readString('latest/feed.atom'), contains('v1.0.0 of foo'), ); + expect( + await bucket.readString('latest/api/packages/foo/feed.atom'), + contains('v1.0.0 of foo'), + ); // Note. that name completion data won't be updated until search caches // are purged, so we won't test that it is updated. @@ -205,6 +217,10 @@ Future _testExportedApiSynchronization( await bucket.readString('latest/feed.atom'), contains('v2.0.0 of bar'), ); + expect( + await bucket.readString('latest/api/packages/bar/feed.atom'), + contains('v2.0.0 of bar'), + ); } _log.info('## New package version'); @@ -247,6 +263,10 @@ Future _testExportedApiSynchronization( await bucket.readString('$runtimeVersion/feed.atom'), contains('v3.0.0 of bar'), ); + expect( + await bucket.readString('latest/api/packages/bar/feed.atom'), + contains('v3.0.0 of bar'), + ); } _log.info('## Discontinued flipped on'); @@ -424,6 +444,10 @@ Future _testExportedApiSynchronization( await bucket.readGzippedJson('latest/api/packages/bar'), isNull, ); + expect( + await bucket.readGzippedJson('latest/api/packages/feed.atom'), + isNull, + ); expect( await bucket.readBytes('latest/api/archives/bar-2.0.0.tar.gz'), isNull, diff --git a/pkg/_pub_shared/lib/src/pubapi.client.dart b/pkg/_pub_shared/lib/src/pubapi.client.dart index d0906302c7..f88ec73f89 100644 --- a/pkg/_pub_shared/lib/src/pubapi.client.dart +++ b/pkg/_pub_shared/lib/src/pubapi.client.dart @@ -132,6 +132,13 @@ class PubApiClient { )); } + Future> packageAtomFeed(String package) async { + return await _client.requestBytes( + verb: 'get', + path: '/api/packages/$package/feed.atom', + ); + } + Future<_i5.PublisherInfo> createPublisher(String publisherId) async { return _i5.PublisherInfo.fromJson(await _client.requestJson( verb: 'post', diff --git a/pkg/pub_integration/lib/script/public_pages.dart b/pkg/pub_integration/lib/script/public_pages.dart index 273bf097b9..4f6918f0d0 100644 --- a/pkg/pub_integration/lib/script/public_pages.dart +++ b/pkg/pub_integration/lib/script/public_pages.dart @@ -73,8 +73,8 @@ class PublicPagesScript { 'Pub Feed Generator', ); _contains( - await _pubClient.getContent('/packages/retry/feed.atom'), - '/packages/retry/feed.atom', + await _pubClient.getContent('/api/packages/retry/feed.atom'), + '/api/packages/retry/feed.atom', ); }