[Feature Request] icp-cli: Per-environment build commands in icp.yaml

Summary

Request for icp.yaml to support per-environment build command overrides, so that icp deploy -e staging and icp deploy -e prod can run different build scripts without workarounds.


Current behavior

icp.yaml supports a single build command for a canister, shared across all environments:

canisters:
  - name: frontend
    recipe:
      type: "@dfinity/asset-canister@v2.1.0"
      configuration:
        dir: frontend/dist
        build:
          - "npm run build:${ICP_ENVIRONMENT:-prod}"

environments:
  - name: staging
    network: ic
    canisters: [frontend, backend]

  - name: prod
    network: ic
    canisters: [frontend, backend]

The intent was to use ${ICP_ENVIRONMENT:-prod} to branch per environment. However, icp-cli does not export ICP_ENVIRONMENT to build subprocess shells — it only reads it as an input flag. The shell variable therefore always falls back to prod, so both icp deploy -e staging and icp deploy -e prod run npm run build:prod.

Confirmed via icp deploy -e staging --debug:

> @yogabuild/amazing@0.14.0 build:prod
> vite build --mode prod
[vite.config] mode: prod | ICP_ENVIRONMENT: undefined

Desired behavior

One of the following (or both):

Option A — Per-environment build overrides in icp.yaml

canisters:
  - name: frontend
    recipe:
      type: "@dfinity/asset-canister@v2.1.0"
      configuration:
        dir: frontend/dist
        build:
          - "npm run build:prod"   # default

environments:
  - name: staging
    network: ic
    canisters:
      - name: frontend
        recipe:
          configuration:
            build:
              - "npm run build:staging"
      - name: backend

  - name: prod
    network: ic
    canisters: [frontend, backend]

Option B — icp-cli exports ICP_ENVIRONMENT to build subprocesses

When running icp deploy -e staging, automatically export ICP_ENVIRONMENT=staging into the shell environment that runs the build commands. This would make ${ICP_ENVIRONMENT:-prod} work as expected, and the current icp.yaml would work without any changes.


Actual workaround

AFAIK my options are to pre-set the variable before calling icp:

ICP_ENVIRONMENT=staging icp deploy frontend -e staging

and have them as an npm command like npm run deploy:staging.

This works, but isn’t exactly elegant :top_hat: … I mean I miss typing icp deploy :sob: :smiley:


Thanks — the environment system is a great step forward over dfx, this would push it a bit further.

Thanks for the feature request. I just forwarded to the team and we will come back with a response soon.

Quick follow-up: this was already discussed before in the team and will be implemented soon! :slight_smile:

Does icp network start and stop delete the canisters ? Getting this response :

• You did it correctly. The result proves the managed ICP/PocketIC network is not persisting the fleet across stop/start in this setup.

  • Canic install artifacts were written around 13:54-13:56.
  • .canic/local/fleets/test.json was written at 13:56:24.
  • The current launcher process started later at 13:57:14.
  • .icp/cache/networks/local/state still has no persisted files.
  • The saved root t63gs-up777-77776-aaaba-cai is not found.
  • The current replica has the Candid UI canister, which matches the fresh-start log.

The big clue in the log is:

Seeding ICP and cycles account balances
Creating canister for Candid UI
Creating canister for proxy

That is a fresh managed network boot, not a resumed one. The huge canisters: { … } block is just ICP CLI’s project config, not live deployed canister state.

So: Canic’s stop/start wrapper is no longer doing anything suspicious, but the ICP CLI managed launcher is
effectively volatile here. The practical Canic change should be to stop implying local replica restart is state-preserving: canic replica stop/start and status stale should warn that local managed PocketIC state may be lost and that fleets need reinstalling after restart.

› so its deleting the canisters each time?

• Effectively, yes, but I don’t see Canic deleting them.

What appears to happen is: on stop, the managed PocketIC instance exits without writing durable canister state into .icp/cache/networks/local/state. On the next start, ICP CLI launches a fresh PocketIC instance, seeds balances, and recreates only its built-in helper canisters. Your old Canic canisters are gone from that new instance.

So from the operator view: yes, stop/start loses the canisters.

From the implementation view: it looks less like an explicit delete and more like “the local replica is ephemeral.”

yes, state is (currently) not preserved when a network is stopped and (re-)started through icp-cli.

Great :slight_smile: !

By the way, have you seen that one?

yes, just responded.