Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
Back to tool/Back to issues
#2223·umbrel

App update installs a different version than the one it offers, when two app stores carry the same app ID

Author: davotoulaCreated Sep 8, 2026Updated Sep 8, 2026

What happened

An app installed from a community app store was also present, at a higher version, in a second community app store I had added. The dashboard offered the higher version and rendered its release notes. Clicking Update installed a different, lower version — the one from the other store — and then immediately went back to offering the higher version again.

The update badge never clears. You can click Update as many times as you like; it re-installs a version you already have and re-offers one it will not fetch.

Version offered in App Store → Updates 0.1.20-rc1 (with 0.1.20-rc1's release notes)
Version actually installed by the click 0.1.19
Badge after the update still offering 0.1.20-rc1

Confirmed on the box after the click:

$ docker inspect <app>_server_1 --format '{{.Config.Image}}'
ghcr.io/<owner>/<app>:0.1.19@sha256:4bc8620e...

$ grep '^version' ~/umbrel/app-data/<app>/umbrel-app.yml
version: "0.1.19"

$ grep -H '^version' ~/umbrel/app-stores/*/<app>/umbrel-app.yml
.../<store-A>/<app>/umbrel-app.yml:version: "0.1.19"      # listed first  in appRepositories
.../<store-B>/<app>/umbrel-app.yml:version: "0.1.20-rc1"  # listed second in appRepositories

No error was shown at any point. The update reported success, and it did do a real update (0.1.19-rc1 → 0.1.19) — just not the one advertised.

Cause

The install side resolves an app to the first repository in appRepositories order that contains the app ID, with no version comparison at all — source/modules/apps/app-store.ts:

typescript
async getAppTemplateFilePath(appId: string) {
    const registry = await this.registry()

    // Find the app in the registry
    for (const repo of registry) {
        const app = repo.apps.find((app) => app.id === appId)
        if (app) {
            const repositories = await this.getRepositories()
            const repoPath = repositories.find((repository) => repository.url === repo.url)!.path
            if (!repoPath) throw new Error(`Repository path not found for ${repo.url}`)
            return `${repoPath}/${appId}`
        }
    }

    throw new Error(`App with ID ${appId} not found in any repository`)
}

registry() preserves appRepositories order, so with store A ahead of store B in that list, every install and update of that app ID comes from store A regardless of what store B holds.

The offer side evidently does not use the same resolution — it surfaced store B's version and store B's release notes. I read the install path in the shipped TypeScript source on the box and confirmed it there; I did not chase the offer-side selection through the minified UI bundle, so I can only report that half empirically: the version offered was the one this function does not return.

That mismatch is the bug, whichever way it is resolved.

Impact

  1. The update button never converges. A permanently stuck badge, and repeated no-op updates.
  2. The release notes belong to a version you do not receive. This is the part that worries me. An operator reads notes describing fixes — including, in the general case, security fixes — clicks Update, is told it succeeded, and is left on a build without them. Nothing anywhere says a different version was installed.
  3. It is silent. No warning, no error, no log line, and apps.state.query returns ready. The only way to notice is to compare the installed manifest or the container image digest against what the dashboard offered.

Expected

Either behaviour would be defensible, but the two halves must agree:

  • Offer what will be installed — compute the offered version with the same resolution getAppTemplateFilePath uses, so an app resolving to store A only ever offers store A's version; or
  • Install what was offered — resolve to the store whose version is being advertised.

If the first-match rule is intended, it would also help to surface it: something in the UI saying which store an app resolves to, and a warning when a registered store carries an app ID that another store already claims. Two stores silently claiming the same ID, with order deciding the winner and nothing displaying that order, is difficult to diagnose from the dashboard alone.

Workaround (verified)

Remove the shadowing store so that only one registered store carries the app ID, update, then add the other store back. Confirmed to work: with the second store removed, the update button offered and installed 0.1.20-rc1, with the container image digests matching the intended build exactly.

Removing an app store does not disturb anything already installed from it. I can state this from the accidental case below: the app stayed running, its containers were untouched, apps.state.query kept returning ready, and its app-data was unaffected — for a period during which no registered store carried its app ID at all. Installs appear fully decoupled from the store they came from. That makes the workaround cheap, but it is still a workaround: it requires knowing about a resolution rule the UI never shows.

Related, met while applying the workaround

The UI gives no feedback nor confirmation when deleting a community app store. I accidentally deleted all added stores.

Re-adding does not require the dashboard, which helps when the dashboard is the thing misbehaving:

umbreld client appStore.addRepository.mutate --url "<store url>"

Adding a repository also triggers an immediate clone — the store showed its version in ~10–15 s, against roughly 270 s for the periodic re-sync to notice a push. Useful to know independently of this bug.

Environment

  • umbrelOS 1.7.4, Raspberry Pi 4 Model B, aarch64
  • Three app repositories registered: the official store plus two community stores
  • The app was installed from one community store; the other carried a higher version of the same app ID

How I found it

Field-testing a release candidate of BrollyZapper, an app I publish myself, served from a second community store so the candidate never reaches the public one. The candidate was offered, and the public store's older release was installed instead.

The reason it was caught at all is that the release procedure verifies image digests rather than version strings. A version number is not sufficient evidence here: the dashboard, the tile and apps.list.query will all report a version that came from a store you were not deploying from, and everything looks successful. An operator checking the version alone would conclude the update had worked.

Source: getumbrel/umbrel

View original on GitHubView discussion on GitHub