From 58c167e58f54a2397d686d9dcc0d16c0ddcd59fb Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 4 Sep 2025 19:21:18 +0200 Subject: [PATCH] Use the changelog excerpt in the atom feeds. --- app/lib/frontend/handlers/atom_feed.dart | 64 ++++++++++++++++--- app/lib/shared/redis_cache.dart | 7 ++ .../frontend/handlers/atom_feed_test.dart | 8 ++- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/app/lib/frontend/handlers/atom_feed.dart b/app/lib/frontend/handlers/atom_feed.dart index 4ceaa19ca2..bf1c9755b3 100644 --- a/app/lib/frontend/handlers/atom_feed.dart +++ b/app/lib/frontend/handlers/atom_feed.dart @@ -6,7 +6,9 @@ import 'dart:async'; import 'dart:convert'; import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; +import 'package:pub_dev/shared/changelog.dart'; import 'package:shelf/shelf.dart' as shelf; import '../../admin/actions/actions.dart'; @@ -53,7 +55,9 @@ Future packageAtomFeedhandler( Future buildAllPackagesAtomFeedContent() async { final versions = await packageBackend.latestPackageVersions(limit: 100); versions.removeWhere((pv) => pv.isNotVisible || pv.isRetracted); - final feed = _allPackagesFeed(versions); + final contents = await Future.wait( + versions.map((v) => _getChangelogReleaseContent(v.package, v.version!))); + final feed = _allPackagesFeed(versions, contents); return feed.toXmlDocument(); } @@ -70,10 +74,34 @@ Future buildPackageAtomFeedContent(String package) async { ) .toList(); versions.removeWhere((pv) => pv.isNotVisible || pv.isRetracted); - final feed = _packageFeed(package, versions); + final contents = await Future.wait( + versions.map((v) => _getChangelogReleaseContent(package, v.version!))); + final feed = _packageFeed(package, versions, contents); return feed.toXmlDocument(); } +Future _getChangelogReleaseContent( + String package, String version) async { + final content = await cache + .changelogReleaseContentAsMarkdown(package, version) + .get(() async { + final asset = await packageBackend.lookupPackageVersionAsset( + package, version, AssetKind.changelog); + final content = asset?.textContent; + if (content == null) { + return ''; + } + final parsed = ChangelogParser().parseMarkdownText(content); + final release = + parsed.releases.firstWhereOrNull((r) => r.version == version); + if (release == null) { + return ''; + } + return release.content.asMarkdownText; + }); + return content ?? ''; +} + class FeedEntry { final String id; final String title; @@ -181,7 +209,10 @@ class Feed { } } -Feed _allPackagesFeed(List versions) { +Feed _allPackagesFeed( + List versions, + List releaseContents, +) { final entries = []; for (var i = 0; i < versions.length; i++) { final version = versions[i]; @@ -195,7 +226,14 @@ Feed _allPackagesFeed(List versions) { sha512.convert(utf8.encode('${version.package}/${version.version}')); final id = createUuid(hash.bytes.sublist(0, 16)); final title = 'v${version.version} of ${version.package}'; - final content = version.ellipsizedDescription ?? '[no description]'; + final fullReleaseContent = releaseContents[i]; + final releaseContent = fullReleaseContent.length > 512 + ? '${fullReleaseContent.substring(0, 500)}[...]' + : fullReleaseContent; + final content = [ + version.ellipsizedDescription ?? '[no description]', + if (releaseContent.isNotEmpty) 'Changelog excerpt:\n$releaseContent', + ].join('\n\n'); entries.add(FeedEntry( id: id, title: title, @@ -216,7 +254,11 @@ Feed _allPackagesFeed(List versions) { ); } -Feed _packageFeed(String package, List versions) { +Feed _packageFeed( + String package, + List versions, + List releaseContents, +) { return Feed( title: 'Recently published versions of package $package on pub.dev', alternateUrl: activeConfiguration.primarySiteUri @@ -227,7 +269,7 @@ Feed _packageFeed(String package, List versions) { .resolve(urls.pkgFeedUrl(package)) .toString(), author: versions.firstOrNull?.publisherId, - entries: versions.map((v) { + entries: versions.mapIndexed((i, v) { final hash = sha512.convert(utf8.encode('package-feed/$package/${v.version}')); final id = createUuid(hash.bytes.sublist(0, 16)); @@ -238,13 +280,19 @@ Feed _packageFeed(String package, List versions) { version: v.version, )) .toString(); + final fullReleaseContent = releaseContents[i]; + final releaseContent = fullReleaseContent.length > 1024 + ? '${fullReleaseContent.substring(0, 1000)}[...]' + : fullReleaseContent; return FeedEntry( id: id, title: 'v${v.version} of $package', alternateUrl: alternateUrl, alternateTitle: v.version, - content: - '${v.version} was published on ${shortDateFormat.format(v.created!)}.', + content: [ + '${v.version} was published on ${shortDateFormat.format(v.created!)}.', + if (releaseContent.isNotEmpty) 'Changelog excerpt:\n$releaseContent', + ].join('\n\n'), updated: v.created!, ); }).toList(), diff --git a/app/lib/shared/redis_cache.dart b/app/lib/shared/redis_cache.dart index 683986003d..446a9f5230 100644 --- a/app/lib/shared/redis_cache.dart +++ b/app/lib/shared/redis_cache.dart @@ -130,6 +130,13 @@ class CachePatterns { decode: (d) => d as bool, ))[package]; + Entry changelogReleaseContentAsMarkdown( + String package, String version) => + _cache + .withPrefix('changelog-release-content-md/') + .withTTL(Duration(hours: 1)) + .withCodec(utf8)['$package-$version']; + Entry> packageData(String package) => _cache .withPrefix('api-package-data-by-uri/') .withTTL(Duration(minutes: 10))['$package']; diff --git a/app/test/frontend/handlers/atom_feed_test.dart b/app/test/frontend/handlers/atom_feed_test.dart index e6febf7783..5b1ca4031a 100644 --- a/app/test/frontend/handlers/atom_feed_test.dart +++ b/app/test/frontend/handlers/atom_feed_test.dart @@ -36,20 +36,22 @@ void main() { ' urn:uuid:a6a43bff-e1ef-4633-b5ee-e0516b655be9\n' ' v1.2.0 of oxygen\n' ' (.*)\n' - ' oxygen is awesome\n' + // Note: pretty format + indenting converts the newlines into spaces. + ' oxygen is awesome Changelog excerpt: - updated\n' ' \n' ''); expect( oxygenExpr.hasMatch(entries[1].toXmlString(pretty: true, indent: ' ')), isTrue, - reason: entries[1].toXmlString(), + reason: entries[1].toXmlString(pretty: true, indent: ' '), ); final neonExpr = RegExp('\n' ' urn:uuid:5f920595-c067-404a-bb19-2b0918372eb6\n' ' v1.0.0 of neon\n' ' (.*)\n' - ' neon is awesome\n' + // Note: pretty format + indenting converts the newlines into spaces. + ' neon is awesome Changelog excerpt: - updated\n' ' \n' ''); expect(