Skip to content

[updater] A case where GitHubProvider picks an invalid release as the "latest" and breaks the update process #9894

Description

@AbdulrhmanGoni

Description

When the following conditions are met in an electron app that uses electron-updater:

  • GitHubProvider is used
  • autoUpdater.allowPrerelease = true
  • No explicit update channel is set
  • The electron app is currently in a stable version

if autoUpdater.checkForUpdates is called, GitHubProvider.getLatestVersion method will pick the version of the first release entry in the GitHub Releases Atom feed without any validation, regardless of what that first release is!

While this current behavior works just fine in a repository dedicated to a single Electron app where every consecutive release is naturally a newer version, it becomes an issue in monorepos where other sub-packages are also released with their own custom tags in the same repository alongside an Electron app that uses electron-updater.

Example:

If the GitHub Releases Atom feed looks like this (very possible in monorepos):

<feed>
  <entry>
    <title>some-packageA@1.27.2</title>
  </entry>
  <entry>
    <title>v3.0.0-beta.1</title> <!-- what's supposed to be picked and offered as the new available update -->
  </entry>
  <entry>
    <title>v2.3.0</title>
  </entry>
  <entry>
    <title>v2.2.0</title>
  </entry>
  <entry>
    <title>some-toolB@3.0.0</title>
  </entry>
  <entry>
    <title>v2.1.0</title>
  </entry>
</feed>

GitHubProvider.getLatestVersion will pick "some-packageA@1.27.2" release as the latest version just because it's the first release to appear in the GitHub Releases Atom feed!, and that will cause autoUpdater.checkForUpdates to throw "Cannot find latest-linux.yml in the latest release artifacts" error every time it's called blocking the update process completely.

The cause of the bug:

This problem comes from here (line 69 and line 79 specifically):

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) {
// allowPrerelease=true with no explicit channel and stable current version:
// pick the first entry from the Atom feed, which may be a prerelease
// noinspection TypeScriptValidateJSTypes
tag = hrefRegExp.exec(latestRelease.element("link").attribute("href"))![1]
} else {

At line 69, latestRelease variable is initialized with the first entry in the GitHub Releases Atom feed,
which seems to me an unnecessary initialization as we can't be 100% sure that the first entry in the feed is always the latest/newest version, and we were going to update latestRelease variable anyway in later steps when we select the right latest release.

Then if the required conditions are met and the code reached line 79, the version/tag of latestRelease will be assigned to tag variable, which means "this is the version/tag of the release we are going to pick and offer as the new update to users"

Bug Reproduction

I created a simple single-file electron app in issue-reproduction branch on my electron-builder fork to reproduce the bug.
I added the app as a new package in the pnpm workspace under packages/app directory so it can use electron-updater by depending on it this:

// packages/app/package.json
"dependencies": {
  "electron-updater": "workspace:*"
}

I made it this way because after you reproduce the bug, you can apply a fix to the packages/electron-updater package in the codebase, re-compile, run the app and see the result of your fix in the same place.

Follow these steps to reproduce the bug:

  1. Clone issue-reproduction branch of AbdulrhmanGoni/electron-builder fork into electron-updater_issue-reproduction directory
git clone -b issue-reproduction https://github.com/AbdulrhmanGoni/electron-builder electron-updater_issue-reproduction
  1. Enter electron-updater_issue-reproduction, Install dependencies and compile packages
cd electron-updater_issue-reproduction && pnpm install && pnpm run compile
  1. Run the reproduction app and see the error
pnpm run --filter=./packages/app reproduce

You should see an error like this:

Error: Cannot find latest-linux.yml in the latest release artifacts (https://github.com/AbdulrhmanGoni/electron-updater-playground/releases/download/xyz%402.0.0/latest-linux.yml): HttpError: 404
...

The important thing to notice in the error message is the link between the parentheses, which tells us from which release the updater tried to get the update file (latest-linux.yml, latest-mac.yml, or latest.yml), we can infer from this link that the updater picked xyz@2.0.0 release (which isn't the expected one) as the latest version.

You can take a look at the Atom feed of the repository where this reproduction electron app is configured to look for updates from, and notice that xyz@2.0.0 release comes first in the atom feed, which is why the updater picked it.

Solution

I think a proper solution could be replacing line 79 with a loop that goes through the releases atom feed and finds the newest release (whether it's a pre-release or a stable release) and reassigns latestRelease and tag variables with it:

let newestRelease = null
for (const entry of feed.getElements("entry")) {
  const releaseTag = hrefRegExp.exec(entry.element("link").attribute("href"))?.[1]

  if (!releaseTag || !semver.valid(releaseTag)) continue

  if (semver.gt(this.updater.currentVersion, releaseTag)) continue

  if (!newestRelease || semver.gt(releaseTag, newestRelease)) {
    newestRelease = releaseTag
    latestRelease = entry
  }
}

if (newestRelease) {
  tag = newestRelease
}

We can also initialize the latestRelease variable with null at line 69 just like tag variable because these variables are going to be reassigned later with the actual latest release/version anyway, therefore there is no need to initialize them with something earlier.

let latestRelease: XElement | null = null
let tag: string | null = null

Note

If you are on Windows or Mac, implemented the fix and run the reproduction app, you are still going to see the "Cannot find latest.yml in the latest release artifacts" error because the repository where the updates are pulled from doesn't in fact have update files for these two platforms, so to check if your fix works and the updater actually picks the right release (which is v2.0.0, not xyz@2.0.0), just look at the link in the error message and check if it's a download link to an asset of the v2.0.0 release.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions