Skip to content

Let a compiled app carry its own update check (config, embed, notice) - #7789

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
jdalton:feat/app-update-config
Aug 11, 2026
Merged

Let a compiled app carry its own update check (config, embed, notice)#7789
proggeramlug merged 1 commit into
PerryTS:mainfrom
jdalton:feat/app-update-config

Conversation

@jdalton

@jdalton jdalton commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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.update block — 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 of main, 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:

  • a URL must be 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;
  • each source needs the keys it readsurl for gh-releases and custom, package for the npm-shaped ones;
  • a zero check interval is rejected, since it would ask on every run. Removing the block is how you disable checks;
  • an app with no version has nothing to compare against, so that is caught too.

enabled = false keeps 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-wide NO_UPDATE_NOTIFIER, CI and CONTINUOUS_INTEGRATION, a non-terminal stderr, and the app's own update command — so app self-update does 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-runtime links 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 of main, 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.update to 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 the dbgloc and fmath entries that file already documents.

The embed is skipped for a dylib, for the same reason the App Group init is: there is no main to 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 same None this 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.sh passes.

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:

Update available: myapp 1.2.3 → 1.4.0
  Run `myapp self-update` to update

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 dirs so 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:

  • The notify interval is keyed to the version, not just the clock. Keyed on time alone it swallows the next release whenever that arrives inside the window — so an interval set to stop nagging about 1.4.0 would also hide the 1.4.1 that fixed it.
  • The state file is written to a per-write temporary name and renamed over the target. Two instances of the same app can run at once, and one shared name lets each rename a file the other is still writing.
  • An unreadable timestamp notifies rather than staying silent, because silence on a damaged file hides updates indefinitely.

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

    • Added optional update checks for Perry-built applications.
    • Supports GitHub releases, npm, GitHub Packages, and custom sources with configurable intervals, versions, commands, and opt-out settings.
    • Added runtime APIs for configuration, request preparation, response recording, and refresh scheduling.
    • Displays throttled notices on stderr when newer versions are available.
    • Safely stores notification state in platform-appropriate cache locations.
    • Checks are disabled by default and suppressed in unsuitable environments.
  • Documentation

    • Added CLI reference guidance for configuration, supported sources, behavior, and disablement.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Perry now supports optional application update checks. The compiler validates and embeds perry.update settings. Executables invoke the runtime at startup. The runtime applies gates, manages state, handles sources, and exposes updater APIs.

Changes

Application update configuration

Layer / File(s) Summary
Compile-time configuration and cache integration
crates/perry/src/commands/compile/*
The compiler resolves package JSON and TOML settings, validates and serializes them, and includes them in object-cache keys.
Metadata embedding and startup wiring
crates/perry-codegen/src/codegen/*, crates/perry-codegen/src/runtime_decls/mod.rs
Executable entry modules embed configured metadata and call perry_update_notify_startup before user code. Dylib and staticlib entries remain unchanged.
Runtime parsing, gates, and persisted notifications
crates/perry-runtime/src/update_notify.rs
The runtime validates configuration, applies startup gates, persists per-application state, compares versions, throttles notices, and renders sanitized stderr output.
Source refresh and check-state updates
crates/perry-runtime/src/update_notify.rs
The runtime builds requests and parses responses for GitHub releases, custom URLs, npm, and GitHub Packages. Recorded checks preserve notification throttling fields.
Updater API and parity checks
crates/perry-runtime/src/update_notify.rs, crates/perry-dispatch/src/updater_table.rs, crates/perry-api-manifest/src/*, types/perry/updater/index.d.ts
The updater API exposes embedded configuration, request metadata, response recording, and refresh timing. Dispatch and manifest tests validate the exported mappings and runtime symbols.
Documentation and release notes
docs/src/cli/app-updates.md, docs/src/SUMMARY.md, changelog.d/7789-app-update-config.md
The documentation describes configuration, runtime behavior, updater APIs, validation, and verification coverage.

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
Loading

Possibly related PRs

  • PerryTS/perry#7785: Implements related update-check sources, request construction, credential handling, and response parsing.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: embedding update configuration and startup notices in compiled applications.
Description check ✅ Passed The description thoroughly explains the changes, validation, tests, documentation, and remaining work, but omits explicit template sections for related issues and checklist items.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@jdalton
jdalton force-pushed the feat/app-update-config branch 3 times, most recently from 86dd241 to 2ab1538 Compare August 10, 2026 18:44
@jdalton jdalton changed the title Let a compiled app carry its own update check (config + embed) Let a compiled app carry its own update check (config, embed, notice) Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
crates/perry-codegen/src/codegen/entry.rs (1)

517-527: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Correct the ordering rationale in the comment.

The comment states the call runs "before anything can change the working directory". perry_macos_bundle_chdir is 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

📥 Commits

Reviewing files that changed from the base of the PR and between db44b31 and 2ab1538.

📒 Files selected for processing (12)
  • changelog.d/7789-app-update-config.md
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/opts.rs
  • crates/perry-codegen/src/runtime_decls/mod.rs
  • crates/perry-runtime/src/lib.rs
  • crates/perry-runtime/src/update_notify.rs
  • crates/perry/src/commands/compile.rs
  • crates/perry/src/commands/compile/host_config.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry/src/commands/compile/update_config.rs
  • docs/src/SUMMARY.md
  • docs/src/cli/app-updates.md

Comment thread changelog.d/7789-app-update-config.md Outdated
Comment thread crates/perry-runtime/src/update_notify.rs Outdated
Comment thread crates/perry-runtime/src/update_notify.rs Outdated
Comment thread crates/perry/src/commands/compile/host_config.rs Outdated
Comment thread crates/perry/src/commands/compile/update_config.rs
Comment thread docs/src/cli/app-updates.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2ab1538 and 9bcc938.

📒 Files selected for processing (2)
  • changelog.d/7789-app-update-config.md
  • crates/perry-runtime/src/update_notify.rs

Comment thread crates/perry-runtime/src/update_notify.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between db44b31 and 9bcc938.

📒 Files selected for processing (12)
  • changelog.d/7789-app-update-config.md
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/opts.rs
  • crates/perry-codegen/src/runtime_decls/mod.rs
  • crates/perry-runtime/src/lib.rs
  • crates/perry-runtime/src/update_notify.rs
  • crates/perry/src/commands/compile.rs
  • crates/perry/src/commands/compile/host_config.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry/src/commands/compile/update_config.rs
  • docs/src/SUMMARY.md
  • docs/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

Comment thread changelog.d/7789-app-update-config.md Outdated
Comment thread docs/src/cli/app-updates.md Outdated
Comment thread docs/src/cli/app-updates.md Outdated
Comment thread docs/src/cli/app-updates.md Outdated
@jdalton
jdalton force-pushed the feat/app-update-config branch from 55ce301 to 9576910 Compare August 10, 2026 19:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/perry-runtime/src/update_notify.rs (1)

1694-1701: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make an_unconfigured_app_exports_nothing assert the behaviour it names.

The test asserts that a locally created empty String is empty, and that config_to_json of a configured app is not empty. It never exercises the unconfigured export path, so it cannot fail for the reason it documents.

CONFIG is process-global, so extract the fallback into a helper that takes Option<&EmbeddedUpdateConfig> and assert that None yields 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_empty inside perry_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 win

Add the reverse manifest-to-dispatch check.

This test only rejects dispatch rows that lack manifest entries. A perry/updater manifest method with no PERRY_UPDATER_TABLE row 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9bcc938 and 9576910.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • changelog.d/7789-app-update-config.md
  • crates/perry-api-manifest/Cargo.toml
  • crates/perry-api-manifest/src/entries/part_4.rs
  • crates/perry-api-manifest/src/lib.rs
  • crates/perry-dispatch/src/updater_table.rs
  • crates/perry-runtime/src/update_notify.rs
  • docs/src/cli/app-updates.md
  • types/perry/updater/index.d.ts

Comment thread crates/perry-runtime/src/update_notify.rs
@jdalton
jdalton force-pushed the feat/app-update-config branch from 9576910 to 6587370 Compare August 10, 2026 19:13
…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.
@jdalton
jdalton force-pushed the feat/app-update-config branch from fd149df to 4f6484a Compare August 10, 2026 20:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9576910 and fd149df.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • changelog.d/7789-app-update-config.md
  • crates/perry-runtime/src/update_notify.rs
  • crates/perry/src/commands/compile/host_config.rs
  • crates/perry/src/commands/compile/update_config.rs
  • docs/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

Comment thread docs/src/cli/app-updates.md
Comment thread docs/src/cli/app-updates.md
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

@proggeramlug
proggeramlug merged commit 750a436 into PerryTS:main Aug 11, 2026
13 of 18 checks passed
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.

2 participants