Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Unreleased

* feat: `script` sync steps now receive `ICP_CLI_ENVIRONMENT`, `ICP_CLI_NETWORK`, `ICP_CLI_CID` (the current canister's principal), and `ICP_CLI_CID_<NAME>` (every canister's principal) as environment variables.
* fix: `icp canister call` with both `--json` and `-o hex` no longer prints both kinds of output at once.
* fix: `icp` no longer picks up a stale inherited `$PWD` when launched as a subprocess via `chdir(2)` + `execve` (e.g. from a test harness). The logical `$PWD` path is now validated against `getcwd()` by inode before use, preserving symlink-aware project root discovery while ignoring stale values.

Expand Down
9 changes: 9 additions & 0 deletions crates/icp-cli/src/commands/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use icp::{
};
use icp_canister_interfaces::candid_ui::MAINNET_CANDID_UI_CID;
use serde::Serialize;
use std::collections::BTreeMap;
use tracing::info;

use crate::{
Expand Down Expand Up @@ -362,12 +363,20 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow:
// method to permit the user identity to upload assets directly before syncing.
info!("Syncing canisters:");

let canister_ids: BTreeMap<String, Principal> = ctx
.ids_by_environment(&environment_selection)
.await?
.into_iter()
.collect();

let pkg_cache = ctx.dirs.package_cache()?;
sync_many(
ctx.syncer.clone(),
agent.clone(),
sync_canisters,
environment_selection.name().to_owned(),
env.network.name.clone(),
canister_ids,
args.proxy,
ctx.debug,
&pkg_cache,
Expand Down
9 changes: 9 additions & 0 deletions crates/icp-cli/src/commands/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use clap::Args;
use futures::future::try_join_all;
use icp::context::{CanisterSelection, Context, EnvironmentSelection};
use icp::identity::IdentitySelection;
use std::collections::BTreeMap;
use tracing::info;

use crate::{
Expand Down Expand Up @@ -82,12 +83,20 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E

info!("Syncing canisters:");

let canister_ids: BTreeMap<String, Principal> = ctx
.ids_by_environment(&environment_selection)
.await?
.into_iter()
.collect();

let pkg_cache = ctx.dirs.package_cache()?;
sync_many(
ctx.syncer.clone(),
agent,
sync_canisters,
environment_selection.name().to_owned(),
env.network.name.clone(),
canister_ids,
args.proxy,
ctx.debug,
&pkg_cache,
Expand Down
11 changes: 11 additions & 0 deletions crates/icp-cli/src/operations/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use icp::{
prelude::PathBuf,
};
use snafu::prelude::*;
use std::collections::BTreeMap;
use std::sync::Arc;
use tracing::error;

Expand Down Expand Up @@ -35,6 +36,8 @@ async fn sync_canister(
canister_id: Principal,
canister_info: &Canister,
environment: &str,
network: &str,
canister_ids: &BTreeMap<String, Principal>,
proxy: Option<Principal>,
pb: &mut MultiStepProgressBar,
pkg_cache: &PackageCache,
Expand All @@ -56,6 +59,8 @@ async fn sync_canister(
path: canister_path.clone(),
cid: canister_id,
environment: environment.to_owned(),
network: network.to_owned(),
canister_ids: canister_ids.clone(),
proxy,
},
agent,
Expand All @@ -79,6 +84,8 @@ pub(crate) async fn sync_many(
agent: Agent,
canisters: Vec<(Principal, PathBuf, Canister)>,
environment: String,
network: String,
canister_ids: BTreeMap<String, Principal>,
proxy: Option<Principal>,
debug: bool,
pkg_cache: &PackageCache,
Expand All @@ -93,6 +100,8 @@ pub(crate) async fn sync_many(
let agent = agent.clone();
let syncer = syncer.clone();
let environment = environment.clone();
let network = network.clone();
let canister_ids = canister_ids.clone();

async move {
// Define the sync logic
Expand All @@ -103,6 +112,8 @@ pub(crate) async fn sync_many(
cid,
&canister_info,
&environment,
&network,
&canister_ids,
proxy,
&mut pb,
pkg_cache,
Expand Down
87 changes: 87 additions & 0 deletions crates/icp-cli/tests/deploy_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::common::{
use icp::{
fs::{create_dir_all, write_string},
prelude::*,
store_id::IdMapping,
};

mod common;
Expand Down Expand Up @@ -1038,3 +1039,89 @@ async fn deploy_through_proxy() {
.success()
.stdout(contains("Status: Running").and(contains(&proxy_cid)));
}

#[tokio::test]
async fn deploy_sync_script_icp_env_vars() {
let ctx = TestContext::new();
let project_dir = ctx.create_project_dir("icp");
let wasm = ctx.make_asset("example_icp_mo.wasm");

// canister-a verifies env/network vars during deploy; canister-b verifies cross-canister
// CID visibility during the explicit sync step.
let pm = formatdoc! {r#"
canisters:
- name: canister-a
build:
steps:
- type: script
command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH"
sync:
steps:
- type: script
command: echo "ENV=$ICP_CLI_ENVIRONMENT NET=$ICP_CLI_NETWORK CID=$ICP_CLI_CID B_CID=$ICP_CLI_CID_CANISTER_B"
- name: canister-b
build:
steps:
- type: script
command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH"
sync:
steps:
- type: script
command: echo "B_SEES_A=$ICP_CLI_CID_CANISTER_A"

{NETWORK_RANDOM_PORT}
{ENVIRONMENT_RANDOM_PORT}
"#};

write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest");

let _g = ctx.start_network_in(&project_dir, "random-network").await;
ctx.ping_until_healthy(&project_dir, "random-network");

clients::icp(&ctx, &project_dir, Some("random-environment".to_string()))
.mint_cycles(10 * TRILLION);

ctx.icp()
.current_dir(&project_dir)
.args([
"--debug",
"deploy",
"--subnet",
common::SUBNET_ID,
"--environment",
"random-environment",
])
.assert()
.success()
.stderr(contains("ENV=random-environment"))
.stderr(contains("NET=random-network"));

// Read the assigned canister IDs and verify CID vars and cross-canister visibility.
let id_mapping: IdMapping = icp::fs::json::load(
&project_dir
.join(".icp")
.join("cache")
.join("mappings")
.join("random-environment.ids.json"),
)
.expect("failed to read ID mapping");

let cid_a = id_mapping
.get("canister-a")
.expect("canister-a ID not found")
.to_text();

let cid_b = id_mapping
.get("canister-b")
.expect("canister-b ID not found")
.to_text();

ctx.icp()
.current_dir(&project_dir)
.args(["--debug", "sync", "--environment", "random-environment"])
.assert()
.success()
.stderr(contains(format!("CID={cid_a}")))
.stderr(contains(format!("B_CID={cid_b}")))
.stderr(contains(format!("B_SEES_A={cid_a}")));
}
83 changes: 83 additions & 0 deletions crates/icp-cli/tests/sync_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,89 @@ async fn sync_plugin_registers_seed_data() {
);
}

#[tokio::test]
async fn sync_script_icp_env_vars() {
let ctx = TestContext::new();
let project_dir = ctx.create_project_dir("icp");
let wasm = ctx.make_asset("example_icp_mo.wasm");

// canister-a verifies all four env vars; canister-b verifies cross-canister CID visibility.
let pm = formatdoc! {r#"
canisters:
- name: canister-a
build:
steps:
- type: script
command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH"
sync:
steps:
- type: script
command: echo "ENV=$ICP_CLI_ENVIRONMENT NET=$ICP_CLI_NETWORK CID=$ICP_CLI_CID B_CID=$ICP_CLI_CID_CANISTER_B"
- name: canister-b
build:
steps:
- type: script
command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH"
sync:
steps:
- type: script
command: echo "B_SEES_A=$ICP_CLI_CID_CANISTER_A"

{NETWORK_RANDOM_PORT}
{ENVIRONMENT_RANDOM_PORT}
"#};

write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest");

let _g = ctx.start_network_in(&project_dir, "random-network").await;
ctx.ping_until_healthy(&project_dir, "random-network");

clients::icp(&ctx, &project_dir, Some("random-environment".to_string()))
.mint_cycles(10 * TRILLION);

ctx.icp()
.current_dir(&project_dir)
.args([
"deploy",
"--subnet",
common::SUBNET_ID,
"--environment",
"random-environment",
])
.assert()
.success();

let id_mapping: IdMapping = icp::fs::json::load(
&project_dir
.join(".icp")
.join("cache")
.join("mappings")
.join("random-environment.ids.json"),
)
.expect("failed to read ID mapping");

let cid_a = id_mapping
.get("canister-a")
.expect("canister-a ID not found")
.to_text();

let cid_b = id_mapping
.get("canister-b")
.expect("canister-b ID not found")
.to_text();

ctx.icp()
.current_dir(&project_dir)
.args(["--debug", "sync", "--environment", "random-environment"])
.assert()
.success()
.stderr(contains("ENV=random-environment"))
.stderr(contains("NET=random-network"))
.stderr(contains(format!("CID={cid_a}")))
.stderr(contains(format!("B_CID={cid_b}")))
.stderr(contains(format!("B_SEES_A={cid_a}")));
}

#[tokio::test]
async fn sync_plugin_routes_through_proxy() {
let ctx = TestContext::new();
Expand Down
6 changes: 6 additions & 0 deletions crates/icp/src/canister/sync/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::BTreeMap;

use async_trait::async_trait;
use candid::Principal;
use ic_agent::Agent;
Expand All @@ -18,6 +20,10 @@ pub struct Params {
/// Name of the environment being synced (e.g. "local", "production").
/// Passed to sync plugin steps via `SyncExecInput`.
pub environment: String,
/// Name of the network (e.g. "local", "ic").
pub network: String,
/// IDs of all named canisters in the project for this environment.
pub canister_ids: BTreeMap<String, Principal>,
/// Proxy canister to route calls through, if `--proxy` was passed.
pub proxy: Option<Principal>,
}
Expand Down
18 changes: 17 additions & 1 deletion crates/icp/src/canister/sync/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,21 @@ pub(super) async fn sync(
params: &Params,
stdio: Option<Sender<String>>,
) -> Result<(), ScriptError> {
execute(adapter, params.path.as_ref(), &[], stdio).await
let mut envs: Vec<(String, String)> = vec![
("ICP_CLI_ENVIRONMENT".to_owned(), params.environment.clone()),
("ICP_CLI_NETWORK".to_owned(), params.network.clone()),
("ICP_CLI_CID".to_owned(), params.cid.to_text()),
];
for (name, id) in &params.canister_ids {
let key = format!(
"ICP_CLI_CID_{}",
name.to_uppercase()
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect::<String>()
);
envs.push((key, id.to_text()));
Comment thread
lwshang marked this conversation as resolved.
}
let env_refs: Vec<(&str, &str)> = envs.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
execute(adapter, params.path.as_ref(), &env_refs, stdio).await
}
23 changes: 23 additions & 0 deletions docs/concepts/build-deploy-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,29 @@ sync:
dir: dist
```

### Script Sync Steps

You can also run arbitrary shell commands in sync steps:

```yaml
sync:
steps:
- type: script
commands:
- my-tool upload --canister "$ICP_CLI_CID" --env "$ICP_CLI_ENVIRONMENT"
```

### Environment Variables

Script sync steps have access to:

- `ICP_CLI_ENVIRONMENT` — The current environment name (e.g. `local`, `staging`)
- `ICP_CLI_NETWORK` — The current network name (e.g. `local`, `ic`)
- `ICP_CLI_CID` — The canister ID of the canister being synced
- `ICP_CLI_CID_<NAME>` — The canister ID of every canister with a registered ID in the current environment (name uppercased, non-alphanumeric characters replaced with `_`)

See [Environment Variables Reference](../reference/environment-variables.md#sync-script-variables) for full details.

### When Sync Runs

- Automatically after `icp deploy`
Expand Down
17 changes: 17 additions & 0 deletions docs/guides/creating-recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,23 @@ canisters:

Use `{{#if}}` with `{{else}}` for defaults, refer to the examples above.

## Environment Variables

Recipe scripts have access to runtime environment variables set by icp-cli.

**Build script steps** receive:

- `ICP_WASM_OUTPUT_PATH` — Where to write the compiled WASM file

**Sync script steps** receive:

- `ICP_CLI_ENVIRONMENT` — The current environment name (e.g. `local`, `staging`)
- `ICP_CLI_NETWORK` — The current network name (e.g. `local`, `ic`)
- `ICP_CLI_CID` — The canister ID of the canister being synced
- `ICP_CLI_CID_<NAME>` — The canister ID of every canister with a registered ID in the current environment

See [Environment Variables Reference](../reference/environment-variables.md) for full details.

## Testing Recipes

Test your recipe by viewing the expanded configuration:
Expand Down
Loading
Loading