Skip to content

fix(updates): false 'update available' prompt after self-update (#515)#522

Merged
rainxchzed merged 2 commits into
mainfrom
fix/515-self-update-stale-installed-version-tag
May 5, 2026
Merged

fix(updates): false 'update available' prompt after self-update (#515)#522
rainxchzed merged 2 commits into
mainfrom
fix/515-self-update-stale-installed-version-tag

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 5, 2026

Closes #515

Root cause

Self-update flow leaves the row's `installedVersion` (tag string like `v1.7.0`) un-touched when reconciling the post-install state. `installedVersionName` and `installedVersionCode` get refreshed from `PackageManager`, but the tag does not. `checkForUpdates` then compares freshly-fetched `matchedRelease.tagName` (`v1.8.0`) against the stale tag (`v1.7.0`) via `VersionMath.isVersionNewer` → returns true → flips `isUpdateAvailable` back on every periodic sweep.

Three code paths shared the bug:

  1. `GithubStoreApp.resolveSelfPendingInstall` (broadcast missed for self-update).
  2. `SyncInstalledAppsUseCase.toResolvePending` (cold-start sync sweep).
  3. No backfill for users already in the broken state.

Fix

  • Forward fix: both `resolveSelfPendingInstall` and `toResolvePending` now also write `installedVersion = app.latestVersion ?: systemInfo.versionName` so the tag stays in sync with the version that was actually installed.
  • Canary in checkForUpdates: when `installedVersionCode` and `latestVersionCode` are both known and equal, force `isUpdateAvailable = false` regardless of what the tag-string compare says. Codes match → it's the same release. Also pins `installedVersion` to the matched tag so the row self-heals on the same call.
  • Cold-start backfill (`normalizeSelfInstalledVersion`): on every cold start where the self-row exists and isn't pending an install, check for tag drift between `installedVersion` and `latestVersion`. If the system's package versionCode matches the row's `installedVersionCode`, normalize the tag and clear `isUpdateAvailable`. Heals existing 1.8.0 users next time they open the app — no new release required.

Test plan

  • Cold-start with a manually-poisoned self row (`installedVersion = "v1.7.0"`, `installedVersionCode = current`, `latestVersion = "v1.8.0"`) → row heals to `installedVersion = "v1.8.0"`, `isUpdateAvailable = false`.
  • Trigger a self-update from the Apps screen → after install, the row's `installedVersion` matches the new tag, no false update prompt.
  • Regression: external (non-self) updates still flow through the existing PackageEventReceiver path correctly — `installedVersion` was already being updated there, no change.
  • Regression: legitimate update available (newer release exists) still shows correctly.
  • What's-new sheet on next 1.8.1 install shows the fix bullet in device language.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue where the store incorrectly reported an available update for itself after being updated.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Walkthrough

The PR fixes an issue where the GitHub Store app continued reporting an available update for itself after being updated. It adds version-code parity checks, backfill logic to pin installedVersion tags, and startup-time normalization to detect and resolve stale update states.

Changes

Self-Update False-Positive Resolution

Layer / File(s) Summary
Version Detection & Backfill
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt
checkForUpdates now uses version-code equality as a primary canary: when installedVersionCode and latestVersionCode both exist and match, it forces isUpdateAvailable=false. If codes match but installedVersion tag differs from the release's tagName, it backfills the tag to the matched release.
Pending-Install Resolution
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt
During pending-install resolution, the use case now derives resolvedTag from latestVersion (or system versionName) and updates the row with installedVersion = resolvedTag alongside existing version-code and update-availability logic.
Self-App Startup Normalization
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt
Added normalizeSelfInstalledVersion(...) helper invoked for existing non-pending self-rows: it compares system versionCode against stored row, and if matched, updates the row to pin installedVersion to latestVersion and clears isUpdateAvailable. Also updated resolveSelfPendingInstall(...) to pin installedVersion to the resolved tag.
Release Notes
core/presentation/src/commonMain/composeResources/files/whatsnew/*/16.json
Release notes across 14 languages (English, Arabic, Bengali, Spanish, French, Hindi, Italian, Japanese, Korean, Polish, Russian, Turkish, Simplified Chinese) document the fix: "Store no longer claims an update is available for itself after already being updated."

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A store once claimed it needed care,
Though freshly patched beyond compare!
But now with codes and tags aligned,
No phantom updates you will find.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(updates): false 'update available' prompt after self-update (#515)' accurately describes the main bug fix: preventing false update prompts after the app updates itself.
Linked Issues check ✅ Passed The PR fully addresses issue #515 by implementing a forward fix in resolveSelfPendingInstall/toResolvePending to write installedVersion tags, adding a canary check in checkForUpdates for version code parity, and implementing cold-start backfill to heal affected users.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the false update prompt issue: code changes to three repositories (app, data, domain layers) and what's-new translations document the fix without introducing unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/515-self-update-stale-installed-version-tag

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt (1)

374-438: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Canary becomes inert after first checkForUpdates due to latestVersionCode = null in updateVersionInfo, creating a re-emergence window

updateVersionInfo (line 418, pre-existing) always writes latestVersionCode = null. This clears the field the canary depends on. The sequence:

  1. Pending install resolved → latestVersionCode and installedVersionCode are equal in DB → canary fires → isUpdateAvailable = false
  2. updateVersionInfo runs → latestVersionCode is now null in DB
  3. If the process crashes before updateInstalledVersion (the backfill at lines 428–438) completes, the tag stays stale
  4. Next checkForUpdates: latestCode = nullcodesAlreadyMatch = false → falls back to tag compare → stale tag → isUpdateAvailable = true → bug re-emerges, and the backfill never fires again (because codesAlreadyMatch stays false)

For the self-app, normalizeSelfInstalledVersion at cold-start covers this. For non-self apps in the broken state where PackageEventReceiver was also missed, no equivalent recovery exists.

The fix is to preserve latestVersionCode in updateVersionInfo when codes already matched, keeping the canary durable across runs:

🛡️ Proposed fix — preserve latestVersionCode when canary fired
             installedAppsDao.updateVersionInfo(
                 packageName = packageName,
                 available = isUpdateAvailable,
                 version = matchedRelease.tagName,
                 assetName = primaryAsset.name,
                 assetUrl = primaryAsset.downloadUrl,
                 assetSize = primaryAsset.size,
                 releaseNotes = matchedRelease.description ?: "",
                 timestamp = System.currentTimeMillis(),
                 latestVersionName = matchedRelease.tagName,
-                latestVersionCode = null,
+                // When codes already matched we confirmed parity — keep
+                // the stored code so the canary stays active on the next
+                // sweep even if the tag backfill below didn't complete
+                // atomically (`#515` defence-in-depth).
+                latestVersionCode = if (codesAlreadyMatch) latestCode else null,
                 latestReleasePublishedAt = matchedRelease.publishedAt,
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt`
around lines 374 - 438, The canary becomes inert because
installedAppsDao.updateVersionInfo is always called with latestVersionCode =
null, clearing the DB field that codesAlreadyMatch relies on; change the call
site in InstalledAppsRepositoryImpl (where updateVersionInfo is invoked after
computing codesAlreadyMatch and isUpdateAvailable) to preserve the existing
latestVersionCode when codesAlreadyMatch is true (i.e., pass the current
latestVersionCode from app.latestVersionCode instead of null), so the canary
persists across restarts and the backfill (updateInstalledVersion) remains
effective; ensure updateVersionInfo still writes null when you truly don't have
a latest code.
🧹 Nitpick comments (1)
composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt (1)

220-262: 💤 Low value

Orphaned KDoc — resolveSelfPendingInstall is now undocumented

The original KDoc at lines 220–225 was for resolveSelfPendingInstall. After inserting normalizeSelfInstalledVersion (with its own KDoc at lines 226–239) between them, the old KDoc is now a dangling block comment associated with normalizeSelfInstalledVersion rather than its intended function. resolveSelfPendingInstall at line 262 has no KDoc.

✏️ Proposed fix — move the original KDoc to its intended function
-    /**
-     * Resolves a stale `isPendingInstall` flag for the app's own
-     * database row. Called on every cold start when the row exists
-     * and still has the flag set — the typical scenario after a
-     * successful self-update where the broadcast path missed.
-     */
     /**
      * Self-heal stale `installedVersion` tags on the self-row carried
      * over from before `#515` was fixed. ...
      */
     private suspend fun normalizeSelfInstalledVersion(...) { ... }

+    /**
+     * Resolves a stale `isPendingInstall` flag for the app's own
+     * database row. Called on every cold start when the row exists
+     * and still has the flag set — the typical scenario after a
+     * successful self-update where the broadcast path missed.
+     */
     private suspend fun resolveSelfPendingInstall(...) { ... }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt`
around lines 220 - 262, The KDoc for resolveSelfPendingInstall got left above
normalizeSelfInstalledVersion and is now orphaned; move the original KDoc block
that currently sits before normalizeSelfInstalledVersion down so it directly
precedes the resolveSelfPendingInstall function declaration (and keep
normalizeSelfInstalledVersion's own KDoc intact), ensuring
resolveSelfPendingInstall regains its documentation and there are no duplicate
or dangling KDoc comments for these methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt`:
- Around line 374-438: The canary becomes inert because
installedAppsDao.updateVersionInfo is always called with latestVersionCode =
null, clearing the DB field that codesAlreadyMatch relies on; change the call
site in InstalledAppsRepositoryImpl (where updateVersionInfo is invoked after
computing codesAlreadyMatch and isUpdateAvailable) to preserve the existing
latestVersionCode when codesAlreadyMatch is true (i.e., pass the current
latestVersionCode from app.latestVersionCode instead of null), so the canary
persists across restarts and the backfill (updateInstalledVersion) remains
effective; ensure updateVersionInfo still writes null when you truly don't have
a latest code.

---

Nitpick comments:
In
`@composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt`:
- Around line 220-262: The KDoc for resolveSelfPendingInstall got left above
normalizeSelfInstalledVersion and is now orphaned; move the original KDoc block
that currently sits before normalizeSelfInstalledVersion down so it directly
precedes the resolveSelfPendingInstall function declaration (and keep
normalizeSelfInstalledVersion's own KDoc intact), ensuring
resolveSelfPendingInstall regains its documentation and there are no duplicate
or dangling KDoc comments for these methods.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ed0c02c-c2e8-4cdb-91bc-e529d0ef28f3

📥 Commits

Reviewing files that changed from the base of the PR and between ceb650b and 1c73015.

📒 Files selected for processing (16)
  • composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/use_cases/SyncInstalledAppsUseCase.kt
  • core/presentation/src/commonMain/composeResources/files/whatsnew/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/es/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/it/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

store更新

1 participant