Skip to content

feat: notify when a newer Basecamp release is available - #320

Open
danisharora099 wants to merge 4 commits into
logos-co:masterfrom
danisharora099:feat/in-app-update-notice
Open

feat: notify when a newer Basecamp release is available#320
danisharora099 wants to merge 4 commits into
logos-co:masterfrom
danisharora099:feat/in-app-update-notice

Conversation

@danisharora099

Copy link
Copy Markdown

Summary

Basecamp had no way to tell you it was out of date. This adds a passive version check against the GitHub releases API and, when a newer stable release exists, an in-app notice plus an optional download of the matching platform artifact.

It implements tiers (a) notice and (b) download-and-prompt from #319. It does not self-install — no relaunch, no execve, no writing into the app bundle. That's tier (c), and it wants the DMG signing work in #27 first.

Basecamp's package manager already solves this for modules (AppsModel::recomputeInstallStatus → an "Update" LogosBadge). The host app was the one piece of the stack with zero staleness signal, which is how a user ended up on 0.2.1 with a stale bundled delivery_module and spent real time debugging what looked like a product bug.

StructureUpdateChecker is a fourth child of MainUIBackend, deliberately outside the CoreModuleManager/UIPluginManager/PackageCoordinator triangle: it touches no logos_core_* C API and no package_manager IPC. The decision logic lives in a header-only UpdateInfo.h rather than in the I/O shell, because tests/CMakeLists.txt links no Qt6::Network and doesn't include app/utils — anything touching QNetworkAccessManager or BuildInfo.h is structurally untestable. Keeping the rules pure buys 158 unit cases over the parts that decide what to download.

Four commits, each independently buildable: backend, UI, tests, docs.

Three details that are load-bearing
  1. The eligibility gate uses semver::valid(), not compare(). compare() sorts unparseable versions below every real one, so without the gate every dev build (stamped pre-release-<sha7>) would be told it was out of date. There's a test that pins exactly this.
  2. QSysInfo::currentCpuArchitecture() returns arm64; the release assets are named -aarch64. That mismatch is the single most likely way this ships silently doing nothing on every Mac and every ARM Linux box.
  3. /releases/latest is ordered by creation date, not semver precedence. A hotfix cut from an old branch can legitimately be "latest", so the comparison requires a strict > and never offers a downgrade.

Linked issues

Closes #319

Screenshots / recordings

All captured on macOS with LOGOS_UPDATE_CURRENT_VERSION=0.2.1 to force a stale version against the live 0.2.3 release.

Beforemaster at 484da77. Dashboard goes straight from Version to Commits; no badge in the sidebar, no marker on Settings.

Before

After — an Updates card between the build summary and Commits, an UPDATE badge under the sidebar version, and a marker on the Settings button so the badge is discoverable without opening anything.

After

Download states (click to expand)

Downloading — real transfer of the published 98.7 MB DMG, with cancel.

Downloading

Done — resolved path, platform-correct instructions, and Show in Finder. The instructions lead with "Quit Basecamp" because replacing a running .app is what produces "the application is damaged" reports.

Done

The section renders nothing at all on builds that report Skipped (dev / pre-release / non-nix), so this is invisible during normal local development.

Test plan

  • nix build .#app succeeds
  • nix build .#smoke-test -L passes
  • nix build .#integration-test -L passes — 18/18, now including the two new update tests
  • Doctests — not touched
  • Manual verification on:
    • Linux (AppImage) — not verified, see below
    • Linux (Nix local build) — unit/qml/sandbox suites
    • macOS
  • N/A

Also run: .#unit-tests 10/10 (158 update cases), .#shutdown-test 3 passed / 0 failed, .#qml-tests, .#sandbox-test.

Verified by hand end to end: a real 98.7 MB download completing byte-exact against the published size, cancel mid-flight leaving no .part, the com.apple.quarantine xattr set with spctl reporting accepted / source=Notarized Developer ID, the already-downloaded short-circuit, the kill switch, and the dev-build skip.

One gap I can't close from this machine: the Linux AppImage. It can't be built from an aarch64-darwin host without a remote builder, and it's where the only real unknown lives — this is the first code in a shipped bundle that ever loads a Qt TLS backend. I verified the macOS half: nix build .#bin-macos-app carries all three Qt TLS plugins and libqopensslbackend.dylib resolves libssl.3/libcrypto.3 via @loader_path into Contents/Frameworks/. Qt ignores SSL_CERT_FILE and scans its own hardcoded CA directory list, which covers mainstream distros, but someone with a Linux builder should confirm on a real AppImage. A TLS failure degrades to "couldn't check" — never to a false "up to date".

Release notes

feat: notify when a newer Basecamp release is available, with optional in-app download

Checklist

  • Branch prefixed fix/, feat/, chore/, docs/, test/, or ci/
  • No unrelated changes bundled in
  • CLAUDE.md / README.md / docs/ updated if behaviour or build steps changed — README documents the notice and the opt-out
  • Uncommitted binaries, screenshots, or .DS_Store files removed — screenshots are hosted on a separate branch, nothing binary is in this diff

Notes for reviewers

  • No VERSION bump or CHANGELOG entry, deliberately. flake.nix:58-66 documents that VERSION exists only on release/** branches and master builds fall back to pre-release-<sha7>; there's no CHANGELOG.md. Adding a VERSION file here would make every master build report itself as a release — which, given this feature, would turn the update checker on for master builds and tell every developer they're out of date.
  • This is the app's first outbound network request. One unauthenticated GET to api.github.com, no identifiers beyond a LogosBasecamp/<version> user agent, and no Authorization header (Qt copies headers verbatim across redirects, so one would leak to the CDN). LOGOS_DISABLE_UPDATE_CHECK=1 turns it off. It makes no request at all on dev builds.
  • The size check is a corruption detector, not an integrity control. No .sha256 is published, so TLS to an allowlisted host is the real guarantee. Both the initial asset URL and every redirect hop are host-allowlisted, and the asset filename is validated as a bare filename before it becomes a path. Publishing a signed digest from Jenkins is the natural follow-up — the Verifying stage is already reserved for it.
  • nix/integration-test.nix pins the check at a file:// stub rather than setting LOGOS_DISABLE_UPDATE_CHECK. That derivation is the only automated runner of ui-tests.mjs, and the tests self-skip on the kill switch — disabling would have meant CI printed a green skip forever. smoke-test and shutdown-test keep the kill switch, since they assert startup and teardown rather than update behaviour.
  • Known follow-ups, none blocking: DRY up the repeated blocks in DashboardView.qml; namespace the five unprefixed download* properties on the facade (they read as package-download state, which PackageCoordinator also owns); split the NOTIFY so progress ticks don't re-evaluate four constant bindings; confirm the com.apple.quarantine flag word empirically; disk writes currently happen on the GUI thread.

🤖 Generated with Claude Code

danisharora099 and others added 4 commits August 7, 2026 12:17
Basecamp's package manager already tells you when a MODULE is out of date;
nothing told you when Basecamp itself was. A user on 0.2.1 with 0.2.3 out
shipped a stale bundled delivery_module, which presented as a runtime bug
rather than "you're three versions behind" — and cost real debugging time.

UpdateChecker is a fourth child of MainUIBackend, deliberately outside the
CoreModuleManager/UIPluginManager/PackageCoordinator triangle: it touches no
logos_core_* C API and no package_manager IPC. It compares the baked-in
VERSION against /releases/latest and, when a matching platform asset exists,
streams it to ~/Downloads and tells the user what to do with it. It does NOT
self-install.

The decision logic lives in the header-only UpdateInfo.h rather than in the
I/O shell, because tests/CMakeLists.txt links no Qt6::Network and does not
include app/utils — anything touching QNetworkAccessManager or BuildInfo.h is
structurally untestable. Keeping the rules pure buys 158 unit-test cases over
the parts that decide what to download.

Load-bearing details:
  * The eligibility gate uses logos::semver::valid(), not compare(). compare()
    sorts unparseable versions BELOW every real one, so without the gate every
    dev build (stamped "pre-release-<sha7>") would be told it was out of date.
  * QSysInfo::currentCpuArchitecture() returns "arm64" while the release assets
    are named "-aarch64". That mismatch is the single most likely way this
    ships silently doing nothing.
  * The asset name comes from the network and becomes a filesystem path, so it
    is validated as a bare filename — prefix/suffix matching alone lets
    "...-v9.9.9/../../../../tmp/pwn-aarch64.dmg" through, and on Linux the
    result would then be chmod +x'd.
  * Both the initial asset URL and every redirect hop are host-allowlisted.
    The size check is a corruption detector, not an integrity control: no
    .sha256 is published, so TLS to an allowlisted host is the real guarantee.
  * macOS: the quarantine xattr is restored, because downloading the file
    ourselves would otherwise REMOVE the Gatekeeper assessment a browser
    download would have triggered.

Co-Authored-By: Claude <noreply@anthropic.com>
…o#319)

Three surfaces, chosen so a stale build is visible without a modal:

  * An "Update" badge under the sidebar version footer — where a user already
    looks to answer "what version am I on?". The 80px column fits one word.
  * A notification dot on the Settings button, reusing the shape of
    SidebarAppDelegate's missing-deps marker in the primary accent rather than
    red, since this is informational and not an error. Without it the badge is
    present but not discoverable.
  * An Updates card in Settings -> Dashboard carrying the state machine:
    Checking / UpToDate / Failed, and under UpdateAvailable the seven download
    stages.

The card is the only card on an otherwise flat, read-only page. That contrast
is the point: as loose text between "Dev build" and "Commits" the notice reads
as one more line of build metadata and is easy to scroll past, which is the
exact failure logos-co#319 exists to fix.

Icons pass brightness: 1.0 — LogosIcon tints via MultiEffect colorization,
which leaves a dark source silhouette essentially untouched, so the install
glyph rendered in its raw asset color instead of the button's foreground.

The whole section collapses on builds reporting Skipped (dev / pre-release /
non-nix). Those users see nothing here, not an "unknown" row.

Co-Authored-By: Claude <noreply@anthropic.com>
Two UI tests driven by a file:// stub, so they never depend on what GitHub
currently calls "latest" and never open a socket.

nix/integration-test.nix pins the stub rather than setting
LOGOS_DISABLE_UPDATE_CHECK. That derivation is the ONLY automated runner of
ui-tests.mjs, and the tests self-skip on the kill switch — disabling would have
meant CI printed a green skip forever while the feature's UI coverage never
actually executed. Hermeticity comes from the stub either way.

smoke-test and shutdown-test keep the kill switch: they assert startup and
teardown, not update behavior, and the check would otherwise be eligible on a
release/** branch and hit api.github.com — succeeding on a sandbox=false host
and DNS-failing on Linux CI, i.e. the same derivation behaving two ways.

The stub publishes assets for architectures no host uses, so selectAssetName is
genuinely exercised. With an empty assets[] the "unsupported platform" test
asserted three values that were all field defaults and would have passed with
the selection logic deleted.

Co-Authored-By: Claude <noreply@anthropic.com>
logos-co#319 noted the README gives no guidance on how to update and no in-app path
back to the releases page. There is now both.

Also states the privacy shape plainly — one unauthenticated GET, no identifiers
beyond a version-bearing user agent, never runs on dev builds, and
LOGOS_DISABLE_UPDATE_CHECK=1 turns it off.

Co-Authored-By: Claude <noreply@anthropic.com>
@danisharora099

Copy link
Copy Markdown
Author

@dlipicar @Khushboo-dev-cpp gentle ping, requested 2026-08-07. If the size is the blocker (about 3k lines), I can split it into the version check plus notice (small) and the download-and-prompt (larger) so the first half can land on its own. Trial users are still finding out they are on an old Basecamp by asking in Discord.

@Khushboo-dev-cpp

Khushboo-dev-cpp commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

@

@dlipicar @Khushboo-dev-cpp gentle ping, requested 2026-08-07. If the size is the blocker (about 3k lines), I can split it into the version check plus notice (small) and the download-and-prompt (larger) so the first half can land on its own. Trial users are still finding out they are on an old Basecamp by asking in Discord.

Sorry, for not closing the loop here @danisharora099
My concern is that this adds another HTTPS call on launch that sends the user's IP, UA and launch timing to GitHub, without the user being told. We already do that for the package catalog, but that's expected to move to Logos storage, so I'd rather not add a new GitHub dependency now.
I'd be fine with either: a first-run consent prompt, or serving the version manifest from Logos storage once the catalog moves. In both cases a Settings toggle is needed

I want see if @dlipicar has different thoughts on this.

after that I still need to review this PR and test it also to add @qnou0x has some new designs for this but which may not be prioritized for v0.3, depends on bandwidth left after completing other tasks - https://www.figma.com/design/iohHxlOhF9RmhBwjKB1SPj/Basecamp---MVP-v.1?node-id=1141-1868&t=UPYga8MrAtlHz9JW-4

@danisharora099

Copy link
Copy Markdown
Author

Fair concern, and I agree it should not ship as-is. Proposal: the check is off until a first-run prompt asks, with a Settings toggle either way, and no call is ever made before consent. I will also structure the manifest fetch behind a single source so it swaps to Logos storage without a UI change once the catalog moves.

That fits the split I offered: the first PR becomes consent prompt + toggle + version notice (small, reviewable), the download-and-prompt half follows separately and can wait for @qnou0x's designs if that is the direction. If @dlipicar agrees on the consent shape I will rework the branch accordingly rather than asking anyone to review 3k lines that are about to change.

@Khushboo-dev-cpp

Copy link
Copy Markdown
Collaborator

Fair concern, and I agree it should not ship as-is. Proposal: the check is off until a first-run prompt asks, with a Settings toggle either way, and no call is ever made before consent. I will also structure the manifest fetch behind a single source so it swaps to Logos storage without a UI change once the catalog moves.

That fits the split I offered: the first PR becomes consent prompt + toggle + version notice (small, reviewable), the download-and-prompt half follows separately and can wait for @qnou0x's designs if that is the direction. If @dlipicar agrees on the consent shape I will rework the branch accordingly rather than asking anyone to review 3k lines that are about to change.

I was wondering maybe its fine for V0.3 without consent and so on cause we do it for packages anyways. but lets still wait for @dlipicar
It was kinda decided that PR's for features will not be submitted by eco dev, but since this PR feels close to completion, we can still use it, the split is not necessary I think. its fine, I can review it this way if we agree with the approach for V0.3

@danisharora099

danisharora099 commented Aug 27, 2026

Copy link
Copy Markdown
Author

this is more of an enhancement than anything critical, so please feel free to just close this heh!

It was kinda decided that PR's for features will not be submitted by eco dev

i raised this PR as I thought it's a great addition as a user (and we are open source:D), more than an eco dev eng - i wanted to upgrade my basecamp and it was a whole process, versus being able to click a button :D

if this is planed for the future, please close the PR:D

thanks @Khushboo-dev-cpp and team for the prompt responses as always - it's great to work with you guys.

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.

In-app update notification / auto-update for Basecamp itself

2 participants