refactor: put script sync-step execution behind a ScriptRunner seam - #711
Draft
raymondk wants to merge 2 commits into
Draft
refactor: put script sync-step execution behind a ScriptRunner seam#711raymondk wants to merge 2 commits into
raymondk wants to merge 2 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Refactors script sync execution behind an injectable runner while preserving host behavior.
Changes:
- Separates script invocation resolution from execution.
- Adds
ScriptRunnerwith host and test implementations. - Improves script-step error context and coverage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
crates/icp/src/context/init.rs |
Initializes the host-backed syncer. |
crates/icp/src/canister/sync/script.rs |
Adds invocation resolution and runner abstraction. |
crates/icp/src/canister/sync/mod.rs |
Injects and dispatches through ScriptRunner. |
crates/icp/src/canister/script.rs |
Extracts resolved-command execution. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+26
to
+27
| /// A fully-resolved script sync step: the command(s), the working directory, and | ||
| /// the complete environment the subprocess runs with (see [`system_env_vars`]). |
Sync has two kinds of step. Plugin steps already run inside the wasmtime WASI sandbox; script steps must spawn a shell. Because script execution was reached directly, the whole sync path inherited that requirement, and the part of a script step that is pure data — which commands to run, in which directory, with which `ICP_CLI_*` variables — was inline, untestable code in the middle of it. Split the script path into resolution and execution: - `ScriptInvocation::new(adapter, params)` resolves a manifest script step into commands, cwd and environment, running nothing. `system_env_vars` is now a named function, and the canister-name-to-env-key normalization it does (uppercase, non-alphanumerics to `_`) is tested rather than assumed. - `ScriptRunner` executes a resolved invocation. `HostScripts` is the implementation that spawns subprocesses, injected into `Syncer` by `Syncer::host()` — what `icp sync` and `icp deploy` use. An environment without subprocesses can substitute a runner that refuses script steps instead of losing the sync path entirely. - `script::execute_commands` takes resolved `&[String]` commands, so the subprocess executor no longer needs to know about manifest types. `script::execute` stays as the adapter-taking wrapper the build path uses. The seam also makes step dispatch testable: a recording `ScriptRunner` now asserts that a script step arrives fully resolved, with no shell involved. `icp-cli` is untouched — the seam is internal to `icp`. One user-visible change: a failing script step now reports `script sync step failed` with the specific failure on the following `caused by:` line, matching how plugin step failures already read. Salvaged from PR 660, originally authored by Adam Spofford, re-executed on current `main`: that branch introduced this seam as part of a larger crate reorganization, and the seam stands on its own.
raymondk
force-pushed
the
fm/icp-sync-script-runner-seam
branch
from
August 13, 2026 19:31
f0351c0 to
c82e9c6
Compare
…ironment Review caught that the contract oversold what `env` is. It said "the complete environment the subprocess runs with", but `HostScripts` applies the entries with `Command::env` and never calls `env_clear`, so the script still inherits ambient variables such as `PATH` and `HOME`. A runner written against the old wording could have cleared the inherited environment and behaved differently while still looking correct. Documentation only — the runtime behaviour is the one that was already there, and is now stated accurately: these are variables a runner adds to its execution environment, winning on a name collision, and a runner is not expected to clear what it inherits. Adds a test that pins it, so the contract and the behaviour cannot drift apart. Two portability constraints shape how it is written, both learned from CI: - `printenv` takes only one operand on BSD, so `printenv A B` silently drops `B` on macOS. The test uses one `echo` — a builtin everywhere — per variable. - Git-for-Windows bash rewrites `PATH` into POSIX form, so its value inside the script never equals the `PATH` the Rust side reads. The inherited variable the test asserts on is therefore one it sets itself, with a value that is not path-shaped, which keeps the assertion an exact match on every platform.
raymondk
force-pushed
the
fm/icp-sync-script-runner-seam
branch
from
August 13, 2026 19:47
c82e9c6 to
259faed
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Salvaged from draft PR 660 (
spofford/deploylib), originally authored by Adam Spofford.That branch is being superseded for architectural reasons, but two of its splits stand on
their own. This is the second. It is re-executed on current
mainrather than cherry-picked,since the branch is 33 commits behind; the branch's file layout and module docstrings were
used as the specification.
What this changes
Sync has two kinds of step. Plugin steps already run inside the wasmtime WASI sandbox;
script steps must spawn a shell. Because script execution was reached directly, the whole
sync path inherited that requirement — and the part of a script step that is pure data
(which commands, in which directory, with which
ICP_CLI_*variables) was inline,untestable code in the middle of it.
ScriptInvocation::new(adapter, params)resolves a manifest script step into commands,cwd and environment, running nothing.
system_env_varsis now a named function, and thecanister-name-to-env-key normalization it does (uppercase, non-alphanumerics to
_) istested rather than assumed.
ScriptRunnerexecutes a resolved invocation.HostScriptsis the implementation thatspawns subprocesses, injected into
SyncerbySyncer::host()— whaticp syncandicp deployuse. An environment without subprocesses can substitute a runner that refusesscript steps instead of losing the sync path entirely.
script::execute_commandstakes resolved&[String]commands, so the subprocessexecutor no longer needs to know about manifest types.
script::executestays as theadapter-taking wrapper the build path uses.
The seam also makes step dispatch testable: a recording
ScriptRunnernow asserts that ascript step arrives fully resolved, with no shell involved.
One user-visible change
A failing script step now reports
script sync step failedwith the specific failure on thefollowing
caused by:line, matching how plugin step failures already read. There is a testasserting exactly that chain, so the claim is verified rather than assumed.
icp-cliis untouched — the seam is internal toicp.Scope note
This carries only the seam. The branch's
NoScriptsrefuse-everything implementation is notincluded: nothing on
maincalls it, and adding unused surface is the specific thing thatmade the superseded branch hard to justify. It is ~12 lines for whoever needs it.
Validation
cargo build,cargo fmt --checkandcargo clippy --workspace --all-targetsare cleanwith zero warnings.
cargo test --workspace: 38 targets pass. The 7 failures(
identity_link_hsm*,canister_snapshot_{download,upload}_resume) are environmental —they need SoftHSM2 and mitmproxy, and fail identically on unmodified
main.Review notes
This is independent of the
Resolvefetch/render salvage PR — different code, no sharedcommits, both based on
main. Either can be reviewed, merged or reverted without the other.