Let a compiled app carry its own update check (config, embed, notice) - #7789
Conversation
📝 WalkthroughWalkthroughPerry now supports optional application update checks. The compiler validates and embeds ChangesApplication update configuration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Compiler
participant Executable
participant Runtime
participant UpdateSource
participant CacheState
participant Stderr
Compiler->>Executable: Embed validated update configuration
Executable->>Runtime: Call perry_update_notify_startup
Runtime->>Runtime: Apply startup gates
Runtime->>CacheState: Load persisted state
Runtime->>UpdateSource: Build request and parse response
Runtime->>Stderr: Render due update notice
Runtime->>CacheState: Save check and notification state
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
86dd241 to
2ab1538
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
crates/perry-codegen/src/codegen/entry.rs (1)
517-527: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCorrect the ordering rationale in the comment.
The comment states the call runs "before anything can change the working directory".
perry_macos_bundle_chdiris emitted at Line 507, ahead of this call, and the state directory resolves from the platform cache location rather than the working directory. Both halves of that claim are wrong.Keep the first reason, which is accurate, and drop the working-directory clause.
🤖 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 `@crates/perry-codegen/src/codegen/entry.rs` around lines 517 - 527, Update the comment immediately above the update_init handling in the entry codegen flow, retaining only the rationale that the update check runs before user code so early exits still receive the notice. Remove the inaccurate working-directory and state-directory explanation without changing the perry_update_notify_startup call.
🤖 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.
Inline comments:
In `@changelog.d/7789-app-update-config.md`:
- Around line 53-57: Reword the first changelog slice around the gate checks so
it no longer claims that runtime output is never printed. Keep its scope limited
to the gates and their CI/script safety, leaving the two-line notice and
printing behavior described exclusively in the later slice.
In `@crates/perry-runtime/src/update_notify.rs`:
- Around line 228-230: Update the update-notification gate in
is_update_check_enabled (or the surrounding function) to use
is_present(env.no_update_check), matching NO_UPDATE_NOTIFIER so any non-empty,
non-falsey value disables checks. In docs/src/cli/app-updates.md lines 92-94,
make no direct change because the runtime behavior will be aligned with the
existing presence-based documentation.
- Around line 810-815: Update run_startup_notice to read the first command-line
argument with std::env::args_os() instead of std::env::args(), converting it
only when valid UTF-8 and skipping the update check when conversion fails, so
non-UTF-8 arguments cannot terminate startup.
In `@crates/perry/src/commands/compile/host_config.rs`:
- Around line 915-928: Update the version selection before calling
update_config::resolve so app_metadata.version from perry.toml takes precedence
over package.json’s version. Preserve the APP_VERSION fallback already
represented by app_metadata.version, and ensure default_version no longer embeds
package.json’s version when both manifests define one.
In `@crates/perry/src/commands/compile/update_config.rs`:
- Around line 246-261: Update require_https so each loopback prefix match is
accepted only when the prefix ends at the URL or is immediately followed by a
port or path separator, rejecting hosts such as localhost.example.test and
127.0.0.1.example.test while preserving valid loopback URLs.
In `@docs/src/cli/app-updates.md`:
- Around line 26-36: Update the documentation around the npm update notice to
state that version lookup/network refresh is not yet shipped and will arrive in
a later release, so readers do not expect the currently incomplete behavior.
Apply the same clarification to the GH_TOKEN requirement for gh-registry where
the page makes that feature appear available.
---
Nitpick comments:
In `@crates/perry-codegen/src/codegen/entry.rs`:
- Around line 517-527: Update the comment immediately above the update_init
handling in the entry codegen flow, retaining only the rationale that the update
check runs before user code so early exits still receive the notice. Remove the
inaccurate working-directory and state-directory explanation without changing
the perry_update_notify_startup call.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d42d2bed-eb36-485c-a812-90349921f581
📒 Files selected for processing (12)
changelog.d/7789-app-update-config.mdcrates/perry-codegen/src/codegen/entry.rscrates/perry-codegen/src/codegen/opts.rscrates/perry-codegen/src/runtime_decls/mod.rscrates/perry-runtime/src/lib.rscrates/perry-runtime/src/update_notify.rscrates/perry/src/commands/compile.rscrates/perry/src/commands/compile/host_config.rscrates/perry/src/commands/compile/object_cache.rscrates/perry/src/commands/compile/update_config.rsdocs/src/SUMMARY.mddocs/src/cli/app-updates.md
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@crates/perry-runtime/src/update_notify.rs`:
- Around line 1277-1288: Update the `"npm" | "gh-registry"` handling in the
response-building logic so only `"npm"` constructs the npmjs.com release URL;
return `None` for `"gh-registry"` unless configuration supplies a verified
browser URL. Add a response test covering `gh-registry` and assert that its
release URL is absent.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 84779cb8-eaff-43a5-9ad9-470bbdb0c567
📒 Files selected for processing (2)
changelog.d/7789-app-update-config.mdcrates/perry-runtime/src/update_notify.rs
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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.
Inline comments:
In `@changelog.d/7789-app-update-config.md`:
- Around line 169-180: Remove the “Added (third slice)” source-refresh section
describing request construction, response interpretation, and runtime state
updates from this changelog fragment, or move it unchanged to the follow-up PR’s
changelog fragment; retain only changes included in this PR.
In `@docs/src/cli/app-updates.md`:
- Around line 70-72: Rewrite the sentence in the build-time error explanation so
it clearly states that users learn about the problem only when the check fails,
replacing the unclear “users find out as silence” wording while preserving the
surrounding explanation.
- Around line 40-57: The configuration table in the app-updates documentation
does not show the required key spelling for both formats. Update the table
around the configuration keys to display each setting’s package.json camelCase
name and perry.toml snake_case name, while preserving the existing defaults and
descriptions.
- Around line 103-109: Complete the Linux and Windows entries in the platform
cache path table by appending the same update state filename, update-check.json,
to each application directory path. Keep the existing platform-specific
directory conventions unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: afa368b5-1346-4cc9-a5c8-c7b4258a7765
📒 Files selected for processing (12)
changelog.d/7789-app-update-config.mdcrates/perry-codegen/src/codegen/entry.rscrates/perry-codegen/src/codegen/opts.rscrates/perry-codegen/src/runtime_decls/mod.rscrates/perry-runtime/src/lib.rscrates/perry-runtime/src/update_notify.rscrates/perry/src/commands/compile.rscrates/perry/src/commands/compile/host_config.rscrates/perry/src/commands/compile/object_cache.rscrates/perry/src/commands/compile/update_config.rsdocs/src/SUMMARY.mddocs/src/cli/app-updates.md
🚧 Files skipped from review as they are similar to previous changes (10)
- crates/perry/src/commands/compile/object_cache.rs
- crates/perry/src/commands/compile/host_config.rs
- docs/src/SUMMARY.md
- crates/perry-runtime/src/lib.rs
- crates/perry/src/commands/compile.rs
- crates/perry-codegen/src/runtime_decls/mod.rs
- crates/perry-codegen/src/codegen/entry.rs
- crates/perry-codegen/src/codegen/opts.rs
- crates/perry/src/commands/compile/update_config.rs
- crates/perry-runtime/src/update_notify.rs
55ce301 to
9576910
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
crates/perry-runtime/src/update_notify.rs (1)
1694-1701: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake
an_unconfigured_app_exports_nothingassert the behaviour it names.The test asserts that a locally created empty
Stringis empty, and thatconfig_to_jsonof a configured app is not empty. It never exercises the unconfigured export path, so it cannot fail for the reason it documents.
CONFIGis process-global, so extract the fallback into a helper that takesOption<&EmbeddedUpdateConfig>and assert thatNoneyields an empty string.♻️ Proposed refactor
+fn config_json_or_empty(config: Option<&EmbeddedUpdateConfig>) -> String { + config.map(config_to_json).unwrap_or_default() +}fn an_unconfigured_app_exports_nothing() { - // CONFIG is process-global and set once, so this asserts the shape of - // the empty case rather than mutating it. - let empty = String::new(); - assert!(empty.is_empty()); - assert_eq!(config_to_json(&config()).is_empty(), false); + assert!(config_json_or_empty(None).is_empty()); + assert!(!config_json_or_empty(Some(&config())).is_empty()); }Then use
config_json_or_emptyinsideperry_updater_get_config.🤖 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 `@crates/perry-runtime/src/update_notify.rs` around lines 1694 - 1701, Refactor the configuration export flow by adding a helper such as config_json_or_empty that accepts Option<&EmbeddedUpdateConfig>, returns an empty string for None, and serializes Some values via config_to_json. Update perry_updater_get_config to use this helper, and change an_unconfigured_app_exports_nothing to assert that passing None produces an empty string instead of testing a locally created String and the configured global config.crates/perry-api-manifest/src/lib.rs (1)
1353-1368: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd the reverse manifest-to-dispatch check.
This test only rejects dispatch rows that lack manifest entries. A
perry/updatermanifest method with noPERRY_UPDATER_TABLErow is not detected. Add a second assertion that every updater manifest method has a dispatch row.Proposed test
+ #[test] + fn every_updater_manifest_method_has_a_dispatch_row() { + let dispatched: Vec<&str> = perry_dispatch::PERRY_UPDATER_TABLE + .iter() + .map(|row| row.method) + .collect(); + let missing: Vec<&str> = crate::entries_for_module("perry/updater") + .map(|entry| entry.name) + .filter(|method| !dispatched.contains(method)) + .collect(); + assert!( + missing.is_empty(), + "manifest entries with no updater dispatch row: {missing:?}" + ); + }🤖 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 `@crates/perry-api-manifest/src/lib.rs` around lines 1353 - 1368, Extend every_updater_dispatch_row_has_a_manifest_entry with a reverse assertion: collect updater manifest method names from entries_for_module("perry/updater"), compare them against perry_dispatch::PERRY_UPDATER_TABLE row.method values, and assert no manifest methods are missing from the dispatch table. Keep the existing dispatch-to-manifest assertion unchanged and provide a clear missing-methods diagnostic.
🤖 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.
Inline comments:
In `@crates/perry-runtime/src/update_notify.rs`:
- Around line 565-577: Update blob_field to return owned, unescaped String
values, reversing the escape sequences emitted by escape_json for quotes and
backslashes. Adjust parse_blob, load_state, and parse_check_response to use the
new return type while preserving existing field parsing behavior, and add a
round-trip test covering a value containing both characters.
---
Nitpick comments:
In `@crates/perry-api-manifest/src/lib.rs`:
- Around line 1353-1368: Extend every_updater_dispatch_row_has_a_manifest_entry
with a reverse assertion: collect updater manifest method names from
entries_for_module("perry/updater"), compare them against
perry_dispatch::PERRY_UPDATER_TABLE row.method values, and assert no manifest
methods are missing from the dispatch table. Keep the existing
dispatch-to-manifest assertion unchanged and provide a clear missing-methods
diagnostic.
In `@crates/perry-runtime/src/update_notify.rs`:
- Around line 1694-1701: Refactor the configuration export flow by adding a
helper such as config_json_or_empty that accepts Option<&EmbeddedUpdateConfig>,
returns an empty string for None, and serializes Some values via config_to_json.
Update perry_updater_get_config to use this helper, and change
an_unconfigured_app_exports_nothing to assert that passing None produces an
empty string instead of testing a locally created String and the configured
global config.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c9f3c313-5134-44cc-be35-5e36a6780f4b
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (8)
changelog.d/7789-app-update-config.mdcrates/perry-api-manifest/Cargo.tomlcrates/perry-api-manifest/src/entries/part_4.rscrates/perry-api-manifest/src/lib.rscrates/perry-dispatch/src/updater_table.rscrates/perry-runtime/src/update_notify.rsdocs/src/cli/app-updates.mdtypes/perry/updater/index.d.ts
9576910 to
6587370
Compare
…e check Perry's CLI has checked for updates for a long time; an app it builds could not, so shipping one meant hand-writing a version check or shipping none. A perry.update block in package.json -- or [update] in perry.toml, which wins key by key -- is validated at compile time and baked into the executable. Two halves: Perry does the noticing, reading the app's own state file at startup and printing a two-line notice on stderr when the last lookup found something newer; the app does the asking, using its own fetch() with the URL, headers and response parsing Perry supplies for whichever of the four sources was configured. That division is the one docs/src/updater/overview.md already states for the desktop updater, and it keeps an HTTP stack out of every compiled binary that never checks for an update. With no block configured NOTHING is emitted: not an empty blob, not a disabled one. The blob is part of the object-cache fingerprint, without which adding the block and rebuilding would serve the cached entry object and ship a binary with no update check while reporting success. Validation is a build failure rather than a warning, because a warning scrolls past in build output while the consequence lands on the app's users, who get no notices and no error. HTTPS is required with the loopback exemption anchored to a host boundary, each source must carry the keys it reads, a zero check interval is rejected, and an app with no version is caught. The version comes from perry.toml's [project] version so the notice agrees with what the rest of the binary reports. Three lessons from the CLI's own post-merge review are applied here rather than rediscovered: the notify interval is keyed to the announced VERSION so it cannot swallow the next release, the state file is written to a per-write temporary name so two instances cannot rename over each other, and an unreadable timestamp notifies rather than silencing updates forever. Four are specific to an app: the notice is stderr only because an app's stdout belongs to the app, control characters are stripped because a release name is attacker-influenceable terminal input, argv is read with args_os because args() panic-drops on non-UTF-8 input before any app code runs, and an unparseable version never reads as newer -- node-smol compared against a hardcoded 0.0.0, which made every release look newer than the running binary. The blob reader unescapes what the writer escaped, without which a url containing a quote or backslash doubled its escapes on every save until it was unusable. gh-registry produces no URL without a token, because that 404 reads as up to date, and no npmjs.com link at all, because the package may be private there. Also closes a gap found on the way past: PERRY_UPDATER_TABLE's comment claims it is auto-derivable from the api-manifest entries, but those are hand-listed and nothing checked they agreed -- a row without its entry makes the strict unimplemented-API gate reject user code that calls it, in somebody else's build. There is now a sabotage-verified parity test, plus one asserting no runtime symbol starts with a prefix Windows stubs to a no-op.
fd149df to
4f6484a
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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.
Inline comments:
In `@docs/src/cli/app-updates.md`:
- Line 39: Update the fenced code block at the documented location in
app-updates.md to include a suitable language tag, preferably text, and validate
the change with cargo run -p perry-doc-tests -- --lint docs/src.
- Around line 69-71: Update the settings table to document both currentVersion
and current_version as supported [update] overrides. Revise the
version-resolution descriptions near the existing version text to state the
precedence: current_version override first, then perry.toml [project] version,
then package.json version.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: cbe47ac7-2c3e-4f1f-8a55-5915e0e19338
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (5)
changelog.d/7789-app-update-config.mdcrates/perry-runtime/src/update_notify.rscrates/perry/src/commands/compile/host_config.rscrates/perry/src/commands/compile/update_config.rsdocs/src/cli/app-updates.md
🚧 Files skipped from review as they are similar to previous changes (3)
- crates/perry/src/commands/compile/host_config.rs
- crates/perry/src/commands/compile/update_config.rs
- crates/perry-runtime/src/update_notify.rs
✅ Action performedComments resolved. Approval is disabled; enable |
Covers the 32 PRs admin-merged in one pass (audited in principle at the maintainer's direction): PerryTS#7768 PerryTS#7772 PerryTS#7779 PerryTS#7784 PerryTS#7785 PerryTS#7786 PerryTS#7788 PerryTS#7789 PerryTS#7797 PerryTS#7798 PerryTS#7801 PerryTS#7802 PerryTS#7804 PerryTS#7805 PerryTS#7806 PerryTS#7807 PerryTS#7808 PerryTS#7810 PerryTS#7811 PerryTS#7815 PerryTS#7816 PerryTS#7818 PerryTS#7819 PerryTS#7820 PerryTS#7821 PerryTS#7822 PerryTS#7823 PerryTS#7824 PerryTS#7825 PerryTS#7826 PerryTS#7827 PerryTS#7828. (PerryTS#7787 closed as already-landed via the PerryTS#7786 stack.) Per-change history lives in each PR's changelog.d fragment as usual. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
Phase B, first slice. An application Perry compiles can now carry its own update check.
Perry's CLI has checked for updates for a long time. An app it builds could not — so shipping one meant the author wrote a version check by hand, or shipped none and hoped users noticed.
{ "perry": { "update": { "source": "npm", "package": "myapp", "command": "self-update" } } }A
perry.updateblock — or an[update]table in perry.toml, which wins key by key — is validated at compile time and baked into the executable as a small blob. The runtime reads it at the top ofmain, before any user code.With no block configured, nothing is emitted
Not an empty blob, and not a disabled one. A binary that configures no updates is byte-identical to one built before this existed. A feature whose off-state still emits code is one you cannot prove is off, so this is checked end to end rather than assumed.
Validation is a build failure, on purpose
A typo in an update URL is discovered either by the person who typed it, at build time, with a message naming the key — or by their users, in production, as silence. So these are errors, not warnings:
https://, with loopback allowed for local testing. Plain HTTP is refused rather than warned about: an on-path attacker can suppress a legitimate update by answering "you are current", and build-output warnings are not where that gets noticed;urlforgh-releasesandcustom,packagefor the npm-shaped ones;enabled = falsekeeps the settings on disk and emits nothing, rather than embedding a disabled block complete with its URL and its startup call.What the runtime half does, and what it deliberately does not
It parses the blob and applies every gate that decides whether a check may happen: the app's own opt-out variable,
PERRY_NO_UPDATE_CHECK, the ecosystem-wideNO_UPDATE_NOTIFIER,CIandCONTINUOUS_INTEGRATION, a non-terminal stderr, and the app's own update command — soapp self-updatedoes not check on its way to updating.It does not yet reach the network or print anything.
That split is deliberate. The gates are where this feature goes wrong quietly: a check that fires in CI, or in a script parsing the app's output, is a bug that surfaces as somebody else's flaky pipeline. They are worth landing and testing ahead of the code that would exercise them.
A blob whose schema this build does not recognize is ignored rather than read field by field. It is emitted by the same Perry that compiled the binary, so a mismatch means something is wrong upstream — and guessing at a moved layout would run a network check with settings nobody wrote.
The blob reader is a small flat-object scanner rather than a JSON library, for two reasons:
perry-runtimelinks into every compiled binary, so a parser pulled in for this would be paid for by every program that configures no updates; and this runs at the very top ofmain, before the collector is usable, so the runtime's own JSON path (which allocates JS values) is not available.The cache key, which is where this would have broken silently
The blob is part of the object-cache fingerprint. Without that, adding
perry.updateto a project and rebuilding incrementally would serve the cached entry object from before — and the binary would ship with no update check while the build reported success. Same class as thedbglocandfmathentries that file already documents.The embed is skipped for a dylib, for the same reason the App Group init is: there is no
mainto put a prelude in.Tests
22 new. Ten on the compiler side cover the parse and every validation rule, including that perry.toml overrides package.json key by key while leaving keys it does not set alone, and that the blob stamps its schema and omits unset optionals rather than writing nulls.
Twelve on the runtime side cover the reader and every gate — including that a key name appearing inside a value is not mismatched (an app whose name contained
"url":would otherwise read its own name as a URL), and that an app with no config reports "not configured" rather than "go ahead". That last one was a real inversion the test caught during development: the code used?, which returns the sameNonethis function reads as "yes, check".Verified end to end: a configured project's binary contains the blob, the same project without the block produces one that does not, the configured binary runs normally, and a plain-HTTP URL fails the build with a message naming the key.
cargo test -p perry: 912 passed.cargo test -p perry-runtime: 2064 passed. Both zero failures.scripts/check_file_size.shpasses.The notice, and the state behind it
An app with an embedded block now reads its own state file at startup and prints two lines to stderr when a previous run recorded something newer:
If the app declared no
command, the second line points at the release page instead. Perry does not tell users to run something that does not exist.The network refresh that records "something newer" is the remaining slice, so a first run is silent — which is the right way round: an app that has never checked has nothing to say.
Where the state lives, and why it is per-app
One file in the platform's cache directory, resolved through
dirsso it asks the real APIs — Known Folders on Windows, the Foundation search paths on macOS — rather than trusting environment variables that a launcher or service manager may not have set. The environment-derived rules remain as a fallback, and are what the tests drive.Keyed per app, because one app's notice silencing another's would be invisible and maddening to debug. The app id is sanitized into a single path component: it is a manifest value that becomes a path, and
..is cheaper to make impossible than to reason about.Three lessons from this repo's own review, applied here rather than rediscovered
The CLI's update surface was reviewed after merging, and three of the findings apply verbatim to this half. They are already fixed here:
1.4.0would also hide the1.4.1that fixed it.Two more are specific to an app rather than a CLI. The notice is stderr only — an app's stdout belongs to the app. And every value in it arrived in a network document, so control characters are stripped: a release name is attacker-influenceable terminal input, and a notice must not be able to repaint somebody's screen.
One last thing worth naming, since node-smol is the design this borrows from: an unparseable version never reads as newer. node-smol's equivalent compared against a hardcoded
"0.0.0", which made every release look newer than the running binary. Returning "unknown" is what avoids that.Documentation
docs/src/cli/app-updates.md, written for the app author rather than for Perry's maintainers: the smallest configuration that does something useful, every key, how to pick a source, the mistakes that fail the build and why each is an error rather than a warning, the cases where a user's run will not check at all, where the state file lives per platform, and how to give your own users an off switch.What remains
The network refresh — the four sources actually fetching — and the TypeScript apply surface for apps that want to install rather than just be told. Both are separate slices for the same reason this one stops where it does: the interesting failures here are quiet ones, and they are easier to see a layer at a time.
28 tests in the runtime module, 10 on the compiler side. Beyond the units there is a wiring test that drives the whole startup path — blob in, notice out, state advanced, second run quiet — because everything else here is a piece tested in isolation, and a feature whose pieces all pass while the path between them is broken is what ships doing nothing.
cargo test -p perry-runtime: 2080 passed.cargo test -p perry: 912 passed. Both zero failures.No version bump — the maintainer bumps at merge, per the external-contributor flow.
Summary by CodeRabbit
New Features
Documentation