From 1974abccb4e3e434ffc76085f0399f51cf09e1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Ya=C4=9F=C4=B1z?= Date: Tue, 20 Oct 2026 01:51:44 +0300 Subject: [PATCH 1/3] fix(cache): look up empty release notes again when the repository changes Versions looked up without a repository, or under a wrong one, were stored as empty and stayed empty for 30 days after the repository was corrected. A metadata refresh that finds a different repository now clears the source of those empty notes and queues a scrape, so they are read again once. Co-Authored-By: Claude Opus 5.5 --- .../Services/PackageCacheService.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/backend/PkgDiff.Application/Services/PackageCacheService.cs b/src/backend/PkgDiff.Application/Services/PackageCacheService.cs index 6fbdcc4..8edadc8 100644 --- a/src/backend/PkgDiff.Application/Services/PackageCacheService.cs +++ b/src/backend/PkgDiff.Application/Services/PackageCacheService.cs @@ -233,11 +233,27 @@ private async Task BuildMetadataFromCache(CachedPackage cached, }; } + private async Task ForgetEmptyReleaseNotesAsync(int cachedPackageId, string registry, string packageId) + { + var emptyNotes = (await _unitOfWork.CachedReleaseNotes + .FindAsync(rn => rn.CachedPackageId == cachedPackageId && rn.IsEmpty && rn.ReleaseNotesSource != null)) + .ToList(); + if (emptyNotes.Count == 0) + return; + + foreach (var note in emptyNotes) + note.ReleaseNotesSource = null; + await _unitOfWork.SaveChangesAsync(); + + _jobs.Enqueue(service => service.ScrapePackageReleaseNotesAsync(packageId, registry)); + } + private async Task SaveFreshMetadataToCache( CachedPackage? cached, PackageMetadata metadata, string registry, string packageId) { var registryId = await ResolveRegistryIdAsync(registry); + var repositoryChanged = false; if (cached == null) { cached = new CachedPackage @@ -263,6 +279,8 @@ private async Task SaveFreshMetadataToCache( cached.Authors = metadata.Authors; cached.IconUrl = metadata.IconUrl; cached.ProjectUrl = metadata.ProjectUrl; + repositoryChanged = metadata.GitHubRepositoryUrl != null + && !string.Equals(metadata.GitHubRepositoryUrl, cached.GitHubRepositoryUrl, StringComparison.OrdinalIgnoreCase); // A repository found while scraping release notes is kept when the registry names none cached.GitHubRepositoryUrl = metadata.GitHubRepositoryUrl ?? cached.GitHubRepositoryUrl; cached.AllVersionsJson = JsonSerializer.Serialize(metadata.Versions.Select(v => v.Version)); @@ -273,6 +291,9 @@ private async Task SaveFreshMetadataToCache( await _unitOfWork.SaveChangesAsync(); + if (repositoryChanged) + await ForgetEmptyReleaseNotesAsync(cached.Id, registry.ToLowerInvariant(), packageId); + var latestVersion = metadata.LatestVersion ?? string.Empty; var latestPublished = metadata.Versions.FirstOrDefault(v => v.Version == latestVersion)?.PublishedDate; await _liveUpdates.RecordLatestVersionAsync(cached, registry, latestVersion, latestPublished, authoritative: true); From 41a91c711b0d4afef2216489b1b0f97bebf4bf95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Ya=C4=9F=C4=B1z?= Date: Tue, 20 Oct 2026 01:51:44 +0300 Subject: [PATCH 2/3] fix(github): match rel_X_Y_Z release tags SQLAlchemy and Alembic tag releases as rel_2_1_5, which no tag candidate matched, so their GitHub release notes were never found. Co-Authored-By: Claude Opus 5.5 --- .../PkgDiff.Infrastructure/Services/GitHubScraperService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/PkgDiff.Infrastructure/Services/GitHubScraperService.cs b/src/backend/PkgDiff.Infrastructure/Services/GitHubScraperService.cs index 9028542..b572d4f 100644 --- a/src/backend/PkgDiff.Infrastructure/Services/GitHubScraperService.cs +++ b/src/backend/PkgDiff.Infrastructure/Services/GitHubScraperService.cs @@ -336,6 +336,7 @@ private static IEnumerable TagCandidates(string? packageId, string versi } yield return $"release-{version}"; yield return $"release/{version}"; + yield return $"rel_{version.Replace('.', '_')}"; } [System.Text.RegularExpressions.GeneratedRegex(@"^[vV]?\d")] From 0705d9d03fcf22753cd75d4ce8a2ab6128a3ea6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Ya=C4=9F=C4=B1z?= Date: Tue, 20 Oct 2026 01:51:44 +0300 Subject: [PATCH 3/3] fix(npm): skip unknown packument values with TrySkip The packument is deserialized from a stream, where Utf8JsonReader.Skip throws on a partial buffer. An old author object with a url before its name (glob, minimatch) made the whole lookup fail and the package was reported as not found on npm. Co-Authored-By: Claude Opus 5.5 --- .../PkgDiff.Infrastructure/Services/NpmService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/backend/PkgDiff.Infrastructure/Services/NpmService.cs b/src/backend/PkgDiff.Infrastructure/Services/NpmService.cs index 94332df..a1d6751 100644 --- a/src/backend/PkgDiff.Infrastructure/Services/NpmService.cs +++ b/src/backend/PkgDiff.Infrastructure/Services/NpmService.cs @@ -265,7 +265,7 @@ private class NpmAuthorConverter : JsonConverter var propName = reader.GetString(); reader.Read(); if (propName == "name") author.Name = reader.GetString(); - else reader.Skip(); + else reader.TrySkip(); } } return author; @@ -299,7 +299,7 @@ private class NpmRepositoryConverter : JsonConverter return new NpmRepository { Url = reader.GetString() }; if (reader.TokenType != JsonTokenType.StartObject) { - reader.Skip(); + reader.TrySkip(); return null; } @@ -312,7 +312,7 @@ private class NpmRepositoryConverter : JsonConverter reader.Read(); var value = reader.TokenType == JsonTokenType.String ? reader.GetString() : null; if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) - reader.Skip(); + reader.TrySkip(); switch (propName) { case "type": repository.Type = value; break; @@ -349,11 +349,11 @@ private class LooseStringConverter : JsonConverter if (reader.TokenType == JsonTokenType.String) first ??= reader.GetString(); else if (reader.TokenType is JsonTokenType.StartObject or JsonTokenType.StartArray) - reader.Skip(); + reader.TrySkip(); } return first; case JsonTokenType.StartObject: - reader.Skip(); + reader.TrySkip(); return null; default: return null;