Skip to content

fix(updater): ensure full changelog includes only release notes up to the latest release#9573

Merged
mmaietta merged 26 commits into
electron-userland:masterfrom
AbdulrhmanGoni:fix/fullChangelog-notes-range
May 31, 2026
Merged

fix(updater): ensure full changelog includes only release notes up to the latest release#9573
mmaietta merged 26 commits into
electron-userland:masterfrom
AbdulrhmanGoni:fix/fullChangelog-notes-range

Conversation

@AbdulrhmanGoni

@AbdulrhmanGoni AbdulrhmanGoni commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #9570


Summary

When fullChangelog: true is set and the GitHub atom feed contains releases above the marked "latest" release (e.g. a draft, a staging release, or a release from another package in a monorepo), electron-updater incorrectly included those newer release notes in the changelog shown to the user — notes for changes they would never actually receive.

This PR fixes the root cause, closes two additional code-quality issues found during review, and adds a comprehensive unit test suite for computeReleaseNotes.


Bug: incorrect full changelog range (fixes #9570)

What was wrong

computeReleaseNotes accumulated all feed entries where version > currentVersion, with no upper bound. If GitHub's atom feed contained a release newer than the "latest" marked one, its notes leaked into the changelog.

Example:

  • Current version: 2.0.0
  • Feed entries: 2.3.0, 2.2.0 (latest), 2.1.0
  • Old behaviour: changelog showed 2.3.0, 2.2.0, 2.1.0
  • Correct behaviour: changelog shows 2.2.0, 2.1.0 only

Fix

computeReleaseNotes now extracts the version from latestRelease's link, validates it with semver.valid, and applies an upper-bound filter (semver.lte(entry, latestVersion)). Entries above latestVersion are excluded.

When the latestRelease link cannot be parsed to a valid semver version, the function returns null (no notes) rather than an unbounded list.


Improvements to allowPrerelease=true tag selection

When allowPrerelease=true with an explicit channel set (e.g. "beta" or "alpha"), the code walks the Atom feed to find the best matching entry. Two improvements were made to this feed-iteration loop while preserving its existing behaviour:

  • semver.valid guard: non-semver tags (docs, website, other package releases in monorepos) are now skipped, preventing semver.prerelease from operating on garbage input.
  • latestRelease assignment: the matched feed element is now assigned back to latestRelease so that the release name and notes sourced later are consistent with the tag that was chosen (previously latestRelease could point to a different entry than the resolved tag).

The allowPrerelease=true with no explicit channel (stable current version) case is unchanged: the first Atom feed entry is still used as the candidate, preserving the existing behaviour where a prerelease can be picked when it is the newest release.


Code quality fixes

Null guard in stable-release feed lookup (line 114)

The loop that maps a resolved tag back to its feed entry used:

if (hrefRegExp.exec(element.element("link").attribute("href"))![1] === tag)

The ! non-null assertion is TypeScript-only — at runtime, if a feed entry's link doesn't match the pattern, null[1] throws TypeError. This is caught by the outer try/catch but produces an opaque ERR_UPDATER_INVALID_RELEASE_FEED error instead of silently skipping the entry. Now matches the defensive pattern already used in the prerelease loop:

const hrefMatch = hrefRegExp.exec(element.element("link").attribute("href"))
if (hrefMatch == null) continue
if (hrefMatch[1] === tag) { ... }

TypeScript type narrowing in computeReleaseNotes

versionRelease was declared as string | undefined (assignment inside a try block) but passed to semver.gt() after only a runtime semver.valid() guard — TypeScript cannot narrow the type from that call signature. Restructured the try block so versionRelease is string or the iteration continues, removing the type mismatch.

Misleading comment

The comment // If we cannot parse the latest version, cntinue and return all release notes without filtering by version had a typo ("cntinue") and was factually wrong (the code returns null, not "all notes"). Fixed to: // If we cannot parse the latest release version, return null — notes cannot be determined.


Tests

Added test/src/updater/updateUtilTest.ts with 23 unit tests for computeReleaseNotes covering:

Scenario Verifies
isFullChangelog=false Returns single note string, short-circuits feed traversal
"No content." handling Normalised to "" in both single-note and array paths
Missing <content> element Returns "" (not an error)
Core regression (#9570) Versions above latestVersion are excluded from changelog
latestVersion === currentVersion Returns []
latestVersion < currentVersion Returns []
Entry exactly at currentVersion Excluded (gt, not gte)
Entries above latestVersion Excluded from result
Non-semver tags interspersed Silently skipped; valid versions still collected
Single-entry feed Returns that one entry
Prerelease as latestRelease Range boundary still works correctly
v-prefix stripping Emitted ReleaseNoteInfo.version never contains v
Duplicate version entries Both included (no deduplication)
No content. in array result Converted to "" per entry
Ascending-order feed Result is sorted descending
latestRelease link missing /tag/ Returns null
All entries excluded Returns []
Build-metadata tags Sorted correctly with semver.rcompare
Regex non-match in feed entry Entry silently skipped

… the latest release

Make sure that the GitHub provider only collects release notes up to the latest release
when `autoUpdater.fullChangelog` is set to `true`.
@changeset-bot

changeset-bot Bot commented Feb 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f1d4f1c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
electron-updater Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@mmaietta

mmaietta commented Feb 8, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR and contribution!

I see what you're trying to do. I'm going to make a few edits to this PR via gh pr checkout to make sure it can appropriately handle a few edge cases and always fallback to valid if semver checks fail.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes electron-updater’s “full changelog” generation for GitHub Releases so it only includes release notes up to the version that will actually be installed (the “latest” release), addressing #9570.

Changes:

  • Update computeReleaseNotes in GitHubProvider to bound collected notes by the latest release’s version.
  • Improve Atom feed tag parsing robustness by reusing a shared regex and skipping entries that can’t be parsed.
  • Add a changeset to publish a patch release of electron-updater.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
packages/electron-updater/src/providers/GitHubProvider.ts Adds latest-version bounding to full changelog computation and refactors tag parsing.
.changeset/clever-candles-cut.md Declares a patch changeset for the updater fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/electron-updater/src/providers/GitHubProvider.ts Outdated
Comment thread packages/electron-updater/src/providers/GitHubProvider.ts Outdated
Comment thread packages/electron-updater/src/providers/GitHubProvider.ts Outdated
Comment thread packages/electron-updater/src/providers/GitHubProvider.ts Outdated
Comment thread packages/electron-updater/src/providers/GitHubProvider.ts Outdated
@mmaietta

mmaietta commented Feb 8, 2026

Copy link
Copy Markdown
Collaborator

@AbdulrhmanGoni would you mind giving a quick review?

@AbdulrhmanGoni

Copy link
Copy Markdown
Contributor Author

@AbdulrhmanGoni would you mind giving a quick review?

Sure!

I see your idea of making sure to parse release versions from links correctly before proceeding to the comparisons,
I think its a good idea, but as Copilot commented here: #9573 (comment), the function could still throw if one of the release versions we compare is an invalid semver, when this is fixed, i think we are good to go for this pr

Regarding this Copilot comment: #9573 (comment), There is indeed an issue occurs with the target update version when allowPrerelease is enabled, I actually reproduced it accidentally once and planned to investigate it the next few days

@mmaietta

mmaietta commented Feb 9, 2026

Copy link
Copy Markdown
Collaborator

@AbdulrhmanGoni would you mind implementing those changes? Particularly since you can reproduce one of the logic flows.

@AbdulrhmanGoni

Copy link
Copy Markdown
Contributor Author

@AbdulrhmanGoni would you mind implementing those changes? Particularly since you can reproduce one of the logic flows.

Sure, I will update the PR now

@AbdulrhmanGoni

AbdulrhmanGoni commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

@mmaietta

I updated the logic that would fix the issue this PR mainly tries to resolve.

I didn't touch anything about this yet:

Regarding this Copilot comment: #9573 (comment), There is indeed an issue occurs with the target update version when allowPrerelease is enabled, I actually reproduced it accidentally once and planned to investigate it the next few days

It's a whole new problem, I suggest to address it in a separate Issue/PR, what do you think?
After re-looking at it, I think it's ok to include the fix for that with this PR, specially because it affects the full changelog we are trying fix.

I included the fix of #9573 (comment), could you take a look?

When the logic enters the branch where prereleases are promoted
to be the target version to be installed (when allowPrerelease is true),
make sure to set `latestRelease` variable as well as `tag` variable to
the picked pre-release to be installed.

This ensures that `computeReleaseNotes` always receives the release
that is actually going to be installed as `latestRelease` variable so it
can collect release notes only up to the version of `latestRelease`

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/electron-updater/src/providers/GitHubProvider.ts Outdated
Comment thread packages/electron-updater/src/providers/GitHubProvider.ts
@mmaietta

Copy link
Copy Markdown
Collaborator

@AbdulrhmanGoni I've added tests. Turns out that semver.gt throws an error when comparing a string to a SemVer

TypeError: Invalid version. Must be a string. Got type "object".
 ❯ new SemVer node_modules/.pnpm/semver@7.7.3/node_modules/semver/classes/semver.js:21:13
 ❯ compare node_modules/.pnpm/semver@7.7.3/node_modules/semver/functions/compare.js:5:32
 ❯ Proxy.gt node_modules/.pnpm/semver@7.7.3/node_modules/semver/functions/gt.js:4:29
 ❯ computeReleaseNotes packages/electron-updater/src/providers/GitHubProvider.ts:257:41

@AbdulrhmanGoni

AbdulrhmanGoni commented Feb 12, 2026

Copy link
Copy Markdown
Contributor Author

Turns out that semver.gt throws an error when comparing a string to a SemVer

Weird 🤔, I just run the following with bun run -i test.ts to test it and it was ok:

import * as semver from "semver@7.7.3"
const result = semver.gt("v3.0.0", new semver.SemVer("v2.0.0"))
console.log(result) // true

Also tested the change locally before pushing it, semver.gt didn't throw!

I think what caused the error is the parameter to new SemVer() constructor.

mmaietta
mmaietta previously approved these changes Mar 12, 2026
@AbdulrhmanGoni

Copy link
Copy Markdown
Contributor Author

I don't get if that failed test shard (Test / Test Windows Shard 1) fails randomly or there is something in this pr that causes it to fail! 🤔

I wonder if it's random because I see the same test has succeeded here: Test / Test Windows Shard 1

@mmaietta do you have an idea?

@mmaietta

Copy link
Copy Markdown
Collaborator

You can ignore it. It's a flaky test due to the heavy system-level integration it handles for an noninteractive installation e2e update test.

Kicking off CI again.
Note: please don't pull in latest master as I have to restart CI when doing so. I should be able to handle it from here, but will ping you if anything else arises.

@AbdulrhmanGoni

AbdulrhmanGoni commented Apr 5, 2026

Copy link
Copy Markdown
Contributor Author

I think i spotted something that could go wrong here:

const feed = parseXml(feedXml)
// noinspection TypeScriptValidateJSTypes
let latestRelease = feed.element("entry", false, `No published versions on GitHub`)
let tag: string | null = null
try {
if (this.updater.allowPrerelease) {
const currentChannel = this.updater?.channel || (semver.prerelease(this.updater.currentVersion)?.[0] as string) || null
if (currentChannel === null) {
// noinspection TypeScriptValidateJSTypes
tag = hrefRegExp.exec(latestRelease.element("link").attribute("href"))![1]
} else {

At line 75, if currentChannel is null, I believe this means "fallback to latest stable version" although allowPrerelease enabled.
The problem is that the fallback logic assumes that latestRelease variable is already initialized with the latest release entry from the atom feed in line 69, but it's in fact not guaranteed to always be the actual latest release that we want to fallback to when allowPrerelease is true but currentChannel is null.

In line 69, latestRelease variable is initially assigned with the first entry inside the atom feed (feed.element method returns the first matching element), but the first entry in the feed doesn't mean it's the latest release, it could be a release for something else 9542#issuecomment

The fix could just be falling back to same logic that runs to get latest release tag/version from github when allowPrerelease is false:

} else {
tag = await this.getLatestTagName(cancellationToken)
for (const element of feed.getElements("entry")) {
// noinspection TypeScriptValidateJSTypes
if (hrefRegExp.exec(element.element("link").attribute("href"))![1] === tag) {
latestRelease = element
break
}
}
}

I actually was able to reproduce the issue and apply the fix locally with this testing electron app
you can also see the first entry in the atom feed of the repository not being the latest release

@mmaietta what do you say about this?

@AbdulrhmanGoni AbdulrhmanGoni requested a review from mmaietta April 14, 2026 14:08
@mmaietta

mmaietta commented May 4, 2026

Copy link
Copy Markdown
Collaborator

it's in fact not guaranteed to always be the actual latest release that we want to fallback to when allowPrerelease is true but currentChannel is null.

Hi, apologies on the delayed reply/review! I'm curious what you mean by this? There was another report recently of not-latest releases are selected if a draft already exists (PrivateGithubProvider). Is the issue you're mentioning similar?

@AbdulrhmanGoni

AbdulrhmanGoni commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Hi, apologies on the delayed reply/review! I'm curious what you mean by this? There was another report recently of not-latest releases are selected if a draft already exists (PrivateGithubProvider). Is the issue you're mentioning similar?

No problem, it's your time.

What i mean is that currently, if for example v3.6.0 is the latest release of an electron app on a given repository (a monorepo that also releases other things not just an electron app), there is a chance in that certain case (allowPrerelease=true but currentChannel is null, meaning the user is on a stable version) the updater picks something other than v3.6.0 release as the latest release for the electron app. this happens because currently there is an assumption at a specific point that the first entry in the github atom feed of public repos is the latest release, but it's not always true! (as you can use in the testing repo i shared in the previous comment)

Regarding the issue you mentioned, If you mean this issue: #9691, I do see it reports the same problem i mentioned here, which is a case where the wrong release is picked as the "latest release" when checking for updates, but i don't see it has the same cause, specially because of the fact that it happens in PrivateGithubProvider.ts not in GithubProvider.ts and related to draft releases (the one i mentioned doesn't need a draft release to happen).

@AbdulrhmanGoni

Copy link
Copy Markdown
Contributor Author

Hi @mmaietta

Take a look at the commit I just pushed to fix the problem I talked about last time, maybe it helps you understand what i am trying to address.

@mmaietta

mmaietta commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Thanks @AbdulrhmanGoni! Just checked it all out locally and added several more tests to cover the logic paths. Kicking off Copilot review now and CI in parallel

I can take it from here I think. I'll ping/tag you if there are any follow-ups needed from your side 🙃

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread packages/electron-updater/src/providers/GitHubProvider.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@mmaietta mmaietta merged commit ac45083 into electron-userland:master May 31, 2026
42 checks passed
@AbdulrhmanGoni AbdulrhmanGoni deleted the fix/fullChangelog-notes-range branch June 7, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[updater] fullChangelog includes notes for releases that won't be installed with the update

3 participants