Conversation
There was a problem hiding this comment.
Pull request overview
Adds an icp project bundle command to produce a deployable .tar.gz by building canisters, rewriting the manifest to use pre-built WASM steps, and packaging WASMs plus asset sync directories. This also updates manifest serialization and JSON schemas so the rewritten manifest is emitted cleanly, and adds integration tests validating bundling + deploy behavior.
Changes:
- Add
icp project bundleCLI subcommand and bundling operation that builds canisters and emits a rewrittenicp.yaml+ artifacts into a.tar.gz. - Add/adjust
Serializeimplementations across manifest types to support emitting a rewritten manifest (and update JSON schemas/defaults accordingly). - Add end-to-end tests and CLI docs for the new command.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/schemas/icp-yaml-schema.json | Adjust defaults (e.g., settings {}, lists []) to match new serialization output and bundling use cases. |
| docs/schemas/environment-yaml-schema.json | Remove default: null for reserved cycles field. |
| docs/schemas/canister-yaml-schema.json | Adjust settings default to {} and remove default: null for reserved cycles field. |
| docs/reference/cli.md | Document icp project bundle and update project command description. |
| crates/icp/src/manifest/recipe.rs | Implement string serialization for RecipeType; ensure recipe structs remain serializable. |
| crates/icp/src/manifest/project.rs | Derive Serialize for ProjectManifest (needed to emit rewritten manifests). |
| crates/icp/src/manifest/network.rs | Derive Serialize and omit None fields in serialized output. |
| crates/icp/src/manifest/mod.rs | Export additional manifest types and implement Serialize for Item<T>. |
| crates/icp/src/manifest/environment.rs | Implement custom Serialize for EnvironmentManifest to omit default/implicit fields. |
| crates/icp/src/manifest/canister.rs | Derive Serialize and omit None init args / sync fields in serialized output. |
| crates/icp/src/canister/mod.rs | Skip serializing None settings fields to avoid emitting nulls. |
| crates/icp-cli/tests/bundle_tests.rs | Add integration tests for bundling + deploy, and for rejecting script sync steps. |
| crates/icp-cli/src/operations/mod.rs | Register new bundle operation module. |
| crates/icp-cli/src/operations/bundle.rs | Implement tar.gz bundle creation, manifest rewriting, and asset directory packaging. |
| crates/icp-cli/src/main.rs | Wire icp project bundle into command dispatch. |
| crates/icp-cli/src/commands/project/mod.rs | Add bundle subcommand under project. |
| crates/icp-cli/src/commands/project/bundle.rs | Implement CLI args and invoke bundling operation. |
| crates/icp-cli/Cargo.toml | Add tar dependency for bundle creation. |
| Cargo.lock | Record new dependency usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "settings": { | ||
| "anyOf": [ | ||
| { | ||
| "$ref": "#/$defs/Settings" | ||
| }, | ||
| { | ||
| "type": "null" | ||
| } | ||
| ], | ||
| "default": { | ||
| "compute_allocation": null, | ||
| "environment_variables": null, | ||
| "freezing_threshold": null, | ||
| "log_memory_limit": null, | ||
| "log_visibility": null, | ||
| "memory_allocation": null, | ||
| "reserved_cycles_limit": null, | ||
| "wasm_memory_limit": null, | ||
| "wasm_memory_threshold": null | ||
| }, | ||
| "default": {}, | ||
| "description": "The configuration specifying the various settings when creating the canister." | ||
| } |
There was a problem hiding this comment.
Same as in the project schema: settings allows null (anyOf includes {"type":"null"}) but CanisterManifest deserializes settings into Settings directly, so settings: null will fail at runtime. Consider removing null from this schema or making deserialization accept null as the default settings value.
| // Re-read the raw manifest to preserve networks and environments verbatim. | ||
| let raw_manifest: ProjectManifest = | ||
| icp::manifest::load_manifest_from_path(&project_dir.join(PROJECT_MANIFEST)) | ||
| .await | ||
| .context(LoadManifestSnafu)?; | ||
|
|
There was a problem hiding this comment.
raw_manifest.networks / raw_manifest.environments are copied into the bundle manifest verbatim, but the bundle archive only includes icp.yaml, *.wasm, and asset directories. If those lists contain Item::Path entries (external network/environment YAMLs) or environment init_args that reference files, the extracted bundle won’t be self-contained and icp deploy will fail. Consider inlining referenced manifests (load them and emit Item::Manifest), and/or embedding referenced init_args files and rewriting paths; alternatively, explicitly reject these cases with a clear error.
| let sha256 = hex::encode(Sha256::digest(&wasm)); | ||
| let wasm_filename = format!("{}.wasm", canister.name); | ||
|
|
||
| // Collect asset dirs and rewrite their paths for the bundle. |
There was a problem hiding this comment.
Archive entry names are derived from canister.name (e.g. format!("{}.wasm", canister.name)). Since the schema allows any string for canister names, a name containing path separators or .. could create invalid/unsafe tar paths (and likely break extraction). It’d be safer to validate/sanitize canister names for bundling (e.g., reject names containing '/', '\', or ..) before using them as filenames.
| for dir in dirs { | ||
| let src_path = canister_path.join(dir); | ||
| let archive_prefix = format!("{canister_name}/{dir}"); | ||
| archive | ||
| .append_dir_all(&archive_prefix, src_path.as_std_path()) | ||
| .context(WriteArchiveEntrySnafu { |
There was a problem hiding this comment.
When adding asset directories, src_path = canister_path.join(dir) will treat absolute dir values as absolute paths and will also allow .. traversal. Additionally, archive_prefix = format!("{canister_name}/{dir}") can embed .. segments into the tar entry path. Consider validating that asset dir entries are relative, normalized (no ..), and stay within canister_path before archiving; otherwise bundling can fail on unpack or unintentionally include files outside the project.
| Bundle a project into a self-contained deployable archive. | ||
|
|
||
| Builds all project canisters and packages them with a rewritten manifest into a `.tar.gz` file. The rewritten manifest replaces all build steps with pre-built steps referencing the bundled WASM files. Asset sync directories are included in the archive. | ||
|
|
||
| Projects with script sync steps cannot be bundled. |
There was a problem hiding this comment.
The docs describe the bundle as a “self-contained deployable archive”, but the current implementation doesn’t include external network/environment manifest files referenced via Item::Path, nor any environment init-args files. Either the implementation should bundle/inline those referenced files, or this command description should document the limitation so users don’t end up with non-deployable bundles.
| @@ -982,6 +972,7 @@ | |||
| ] | |||
There was a problem hiding this comment.
This schema allows canisters to be null (type: ["array","null"]) while the Rust ProjectManifest field is a Vec with #[serde(default)], which will error on an explicit YAML null. If null is not intended to be accepted, consider removing null from the schema here; if it is intended, adjust deserialization to treat null the same as missing (empty list).
| "settings": { | ||
| "anyOf": [ | ||
| { | ||
| "$ref": "#/$defs/Settings" | ||
| }, | ||
| { | ||
| "type": "null" | ||
| } | ||
| ], | ||
| "default": { | ||
| "compute_allocation": null, | ||
| "environment_variables": null, | ||
| "freezing_threshold": null, | ||
| "log_memory_limit": null, | ||
| "log_visibility": null, | ||
| "memory_allocation": null, | ||
| "reserved_cycles_limit": null, | ||
| "wasm_memory_limit": null, | ||
| "wasm_memory_threshold": null | ||
| }, | ||
| "default": {}, | ||
| "description": "The configuration specifying the various settings when creating the canister." | ||
| } | ||
| }, |
There was a problem hiding this comment.
settings is declared as anyOf [Settings, null] with a default of {}, but the CanisterManifest deserializer parses settings directly into Settings (not Option<Settings>), so settings: null will fail at runtime even though it validates against the schema. Either tighten the schema to disallow null for settings, or update deserialization to treat null as the default empty settings.
No description provided.