Skip to content

refactor: put script sync-step execution behind a ScriptRunner seam - #711

Draft
raymondk wants to merge 2 commits into
mainfrom
fm/icp-sync-script-runner-seam
Draft

refactor: put script sync-step execution behind a ScriptRunner seam#711
raymondk wants to merge 2 commits into
mainfrom
fm/icp-sync-script-runner-seam

Conversation

@raymondk

Copy link
Copy Markdown
Collaborator

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 main rather 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_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.

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. There is a test
asserting exactly that chain, so the claim is verified rather than assumed.

icp-cli is untouched — the seam is internal to icp.

Scope note

This carries only the seam. The branch's NoScripts refuse-everything implementation is not
included: nothing on main calls it, and adding unused surface is the specific thing that
made the superseded branch hard to justify. It is ~12 lines for whoever needs it.

Validation

cargo build, cargo fmt --check and cargo clippy --workspace --all-targets are clean
with 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 Resolve fetch/render salvage PR — different code, no shared
commits, both based on main. Either can be reviewed, merged or reverted without the other.

Copilot AI balanced review requested due to automatic review settings August 13, 2026 10:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Refactors script sync execution behind an injectable runner while preserving host behavior.

Changes:

  • Separates script invocation resolution from execution.
  • Adds ScriptRunner with 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 thread crates/icp/src/canister/sync/script.rs Outdated
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
raymondk force-pushed the fm/icp-sync-script-runner-seam branch from f0351c0 to c82e9c6 Compare August 13, 2026 19:31
…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
raymondk force-pushed the fm/icp-sync-script-runner-seam branch from c82e9c6 to 259faed Compare August 13, 2026 19:47
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