Skip to content

Let an app built on core supply its update source through AppExtensions #3320

Closed

Description

@mstomar125

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> } and LatestVersionSource::{GitHubLatestRelease { repo }, VersionEndpoint { url }};
  • adds AppExtensions::update_source(source), which assemble stores as managed state;
  • makes the startup check and check_for_update_command read 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> to UpdateStatus;
  • added COMMUNITY_RELEASES_PAGE_URL and the pure status_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) and PRO_LATEST_URL (34-42);
  • latest_tag_from_json (97-108);
  • check_for_update (110-133), with its is_pro_build() branch (116-124);
  • fetch_latest_tag (135-156);
  • pro_version_from_json (158-170) and fetch_pro_latest_version (172-194);
  • check_for_update_command (196-203);
  • the tests pro_version_parsed_from_worker_json (262-272) and pro_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

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).

Generic fetchers and parsers

  • fn github_latest_release_url(repo: &str) -> String.
  • async fn fetch_github_latest_release(repo): today's fetch_latest_tag, with the URL built from repo.
  • async fn fetch_version_endpoint(url): today's fetch_pro_latest_version, with the URL passed in.
  • fn version_from_endpoint_json(body): renamed from pro_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_version and pro_version_from_json.
  • check_for_update_command delegates to check_for_update_for_app.
  • The module doc names the update source generically. Leave PRO_LATEST_URL and 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_parsed and version_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.
  • assemble manages UpdateSourceState(extensions.update_source.unwrap_or_else(|| builtin_update_source_for(is_pro_build()))).
  • Re-export UpdateSource and LatestVersionSource from extensions and 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 VersionEndpoint must return {"version": "<semver or v-tag>"};
    • download_url: None hides 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

Steps

  1. Run the CLAUDE.md startup sequence. Self-assign at pickup; the worktree is issue-<N>-extension-update-source.
  2. Add the types and the built-in source, and refactor the fetchers. The existing community tests must stay green.
  3. Add AppExtensions::update_source and the assemble state.
  4. Rewire both callers.
  5. Add the tests below.
  6. Run bun run scripts/check-pro-boundary.ts, then lower BASELINES to the printed counts.
  7. Run bun run quality:fix, then bun run test:changed.

Acceptance criteria

  • With AppExtensions::none(), a community build checks https://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 UpdateSource decides both the version source and the Download URL, for the startup event and for check_for_update_command.
  • check_for_update is gone. The file's only is_pro_build() read is the argument to builtin_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.ts passes, with BASELINES lowered 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_url of community()'s repo equals today's literal URL.
  • community_source_downloads_from_the_releases_page
  • builtin_source_is_community_outside_pro_builds: builtin_update_source_for(false) == UpdateSource::community().
  • version_endpoint_body_parsed and version_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_source
  • an_extension_update_source_replaces_the_builtin
  • the_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

Risks and rollback


Plan key: C2.4 (pro-boundary DAG).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Labels

pro-boundaryPro/core boundary: Pro code moves to nodespace-sync (ADR-081..085)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions