Repository navigation
Let an app built on core supply its update source through AppExtensions #3320
Description
Part of #3322 (Tauri extension points for the desktop app).
Summary
Let an app crate built on core decide where its update check looks and what the banner's Download opens (ADR-082 (proposed) §3.6). This PR:
- adds the data types
UpdateSource { latest: LatestVersionSource, download_url: Option<&'static str> }andLatestVersionSource::{GitHubLatestRelease { repo }, VersionEndpoint { url }}; - adds
AppExtensions::update_source(source), whichassemblestores as managed state; - makes the startup check and
check_for_update_commandread that state.
Core's default is today's behaviour, held in one function, builtin_update_source_for(is_pro). It keeps the in-core Pro arm, which #3323 deletes, so every build behaves as it does today. check_for_update no longer branches on edition.
Context
What #3319 already did. #3319, a dependency:
- added
download_url: Option<String>toUpdateStatus; - added
COMMUNITY_RELEASES_PAGE_URLand the purestatus_from(current, latest, download_url); - made the banner open the payload's URL, and hide Download when there isn't one.
This PR doesn't touch the frontend or UpdateStatus.
app-lib/src/update_check.rs (af8ddb6 lines, before #3319):
- the module doc describing the Pro source (1-23);
LATEST_RELEASE_URL(28-32) andPRO_LATEST_URL(34-42);latest_tag_from_json(97-108);check_for_update(110-133), with itsis_pro_build()branch (116-124);fetch_latest_tag(135-156);pro_version_from_json(158-170) andfetch_pro_latest_version(172-194);check_for_update_command(196-203);- the tests
pro_version_parsed_from_worker_json(262-272) andpro_unconfigured_or_error_body_yields_none(274-283).
Callers. There are two: the startup spawn in run()'s setup (lib.rs 468-486 at af8ddb6) and check_for_update_command.
Endpoint shape. A version endpoint returns JSON with a version string, which is what today's parser reads. That shape becomes part of the extension API.
Scope
app-lib/src/update_check.rs
Types, with rustdoc, deriving Clone, Debug, PartialEq and Eq:
pub struct UpdateSource { pub latest: LatestVersionSource, pub download_url: Option<&'static str> }pub enum LatestVersionSource { GitHubLatestRelease { repo: &'static str }, VersionEndpoint { url: &'static str } }
impl UpdateSource { pub const fn community() -> Self } returns GitHubLatestRelease { repo: "NodeSpaceAI/nodespace-core" } with download_url: Some(COMMUNITY_RELEASES_PAGE_URL).
pub(crate) fn builtin_update_source_for(is_pro: bool) -> UpdateSource
true:VersionEndpoint { url: PRO_LATEST_URL }withdownload_url: None, exactly what Carry the download URL in the update-check payload instead of a hardcoded releases page #3319 set for Pro.false:UpdateSource::community().- The caller passes
crate::daemon_setup::is_pro_build(). That is the only edition read left in the file, and Delete the Pro arm of core's built-in update source and its worker parsing #3323 deletes thetruearm.
State and reads
pub(crate) struct UpdateSourceState(pub UpdateSource).fn update_source_for_app<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> UpdateSource: the managed source, or the built-in one if none is managed.pub async fn check_for_update_for_app<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> UpdateStatus:- reads the current version from
app.package_info().version; - fetches the latest version from the source;
- calls
status_from(current, latest, source.download_url).
- reads the current version from
Generic fetchers and parsers
fn github_latest_release_url(repo: &str) -> String.async fn fetch_github_latest_release(repo): today'sfetch_latest_tag, with the URL built fromrepo.async fn fetch_version_endpoint(url): today'sfetch_pro_latest_version, with the URL passed in.fn version_from_endpoint_json(body): renamed frompro_version_from_json, with a generic doc.async fn fetch_latest_version(&LatestVersionSource): dispatches to the right fetcher.
Removals and rewiring
- Remove
check_for_update,LATEST_RELEASE_URL,fetch_latest_tag,fetch_pro_latest_versionandpro_version_from_json. check_for_update_commanddelegates tocheck_for_update_for_app.- The module doc names the update source generically. Leave
PRO_LATEST_URLand its doc for Delete the Pro arm of core's built-in update source and its worker parsing #3323. - Rename the two Pro parser tests to
version_endpoint_body_parsedandversion_endpoint_error_body_yields_none, with product-neutral fixture bodies.
app-lib/src/extensions/
AppExtensions::update_source(self, source: UpdateSource) -> Self. If it is called more than once, the last call wins.assemblemanagesUpdateSourceState(extensions.update_source.unwrap_or_else(|| builtin_update_source_for(is_pro_build()))).- Re-export
UpdateSourceandLatestVersionSourcefromextensionsand from the crate root. - Rustdoc covers:
- the source is read once at startup and fixed for the process;
- the app's version comes from its bundle config;
- a
VersionEndpointmust return{"version": "<semver or v-tag>"}; download_url: Nonehides Download.
app-lib/src/lib.rs
The startup spawn calls update_check::check_for_update_for_app(&update_app_handle). Drop the current_version local if nothing else uses it.
Out of scope
UpdateStatus, the frontend store and the banner (Carry the download URL in the update-check payload instead of a hardcoded releases page #3319).- Deleting
PRO_LATEST_URLor the Pro arm (Delete the Pro arm of core's built-in update source and its worker parsing #3323). - Auto-update.
Steps
- Run the CLAUDE.md startup sequence. Self-assign at pickup; the worktree is
issue-<N>-extension-update-source. - Add the types and the built-in source, and refactor the fetchers. The existing community tests must stay green.
- Add
AppExtensions::update_sourceand theassemblestate. - Rewire both callers.
- Add the tests below.
- Run
bun run scripts/check-pro-boundary.ts, then lowerBASELINESto the printed counts. - Run
bun run quality:fix, thenbun run test:changed.
Acceptance criteria
- With
AppExtensions::none(), a community build checkshttps://api.github.com/repos/NodeSpaceAI/nodespace-core/releases/latest, and Download opens the releases page, as today. - A Pro-mode build still checks its in-core endpoint with no Download URL, as after Carry the download URL in the update-check payload instead of a hardcoded releases page #3319.
- An extension's
UpdateSourcedecides both the version source and the Download URL, for the startup event and forcheck_for_update_command. -
check_for_updateis gone. The file's onlyis_pro_build()read is the argument tobuiltin_update_source_for. - Every existing update-check test passes, including Carry the download URL in the update-check payload instead of a hardcoded releases page #3319's. The two parser tests pass under their new names.
-
bun run scripts/check-pro-boundary.tspasses, withBASELINESlowered to the exact new counts (from the renamed parser and tests, and the reworded module doc). No count rises. - The Pro-mode compile check passes.
- No product names or GitHub issue numbers in new code or docs.
Tests and verification
In update_check.rs:
community_source_targets_the_public_release_endpoint:github_latest_release_urlofcommunity()'s repo equals today's literal URL.community_source_downloads_from_the_releases_pagebuiltin_source_is_community_outside_pro_builds:builtin_update_source_for(false) == UpdateSource::community().version_endpoint_body_parsedandversion_endpoint_error_body_yields_none(renamed).
In extensions/fixture_tests.rs, each through assemble and update_source_for_app on a mock handle, with no network:
none_manages_the_builtin_update_sourcean_extension_update_source_replaces_the_builtinthe_last_update_source_wins
A source-text test in lib.rs: the startup spawn calls check_for_update_for_app(, and so does check_for_update_command's body.
Mutation-check each new test: break the code path it covers, confirm the test fails, then restore.
cargo test --lib -p nodespace-app-lib -- update_check extensions:: --test-threads=2
cargo clippy -p nodespace-app-lib --all-targets -- -D warnings -D clippy::unused-async
NODESPACE_PRO_SUPABASE_URL=https://example.invalid NODESPACE_PRO_ANON_KEY=dummy cargo check -p nodespace-app
bun run scripts/check-pro-boundary.ts
bun run test:changed
Dependencies
- Route the desktop app through run(extensions, context) with no extensions in core #3306
- Carry the download URL in the update-check payload instead of a hardcoded releases page #3319: it owns
UpdateStatus.download_url,COMMUNITY_RELEASES_PAGE_URL,status_fromand the frontend.
Risks and rollback
- A wrong source for Pro users. The Pro arm is kept verbatim in
builtin_update_source_for(true), and the Pro-mode compile check covers it. - Note for Delete the Pro arm of core's built-in update source and its worker parsing #3323. After this PR, Delete the Pro arm of core's built-in update source and its worker parsing #3323 deletes only
PRO_LATEST_URLand thetruearm ofbuiltin_update_source_for. The Pro parser and fetcher are now the genericVersionEndpointhelpers, and they stay. - Rollback: revert.
Plan key: C2.4 (pro-boundary DAG).
🤖 Generated with Claude Code
Activity
Let an app crate supply its update source through AppExtensions (closes