ChainLaunch

Chaincode CI/CD with the CLI

The chainlaunch fabric chaincode lifecycle subtree shown on this page ships with ChainLaunch Pro. The OSS chaindeploy binary exposes only the legacy fabric…

Pro feature

The chainlaunch fabric chaincode lifecycle subtree shown on this page ships with ChainLaunch Pro. The OSS chaindeploy binary exposes only the legacy fabric install / invoke / query commands (which require a network.yaml and TLS material on disk). Learn more about Pro features.

This guide shows the three flows that matter for continuous delivery of chaincode on Hyperledger Fabric using the chainlaunch fabric chaincode CLI:

  1. Initial deploy -- create a chaincode definition and deploy the container.
  2. Invoke / query -- write transactions and read state through the CLI (no Fabric SDK required).
  3. Image bump -- ship a new container image without changing the endorsement policy or sequence.

All three flows talk to the ChainLaunch REST API (/api/v1/sc/fabric/...), so the same commands work against any reachable instance -- local, staging, or production -- by switching CHAINLAUNCH_API_URL.

The examples below use kfsoftware/external-chaincode (a Fabric asset-transfer chaincode running as a CCaaS service) to make the invoke/query payloads concrete.

Concepts in 30 seconds

A chaincode in ChainLaunch has two layers:

Layer What it holds When it changes
Chaincode The logical name on a channel Once, at chaincode birth
Definition Version, sequence, endorsement policy, docker image, chaincode address Sequence bumps when policy/sequence-affecting fields change. Image-only changes reuse the row.

The CLI resolves a chaincode by --channel + --chaincode name, so you never need to remember IDs in CI scripts. Peers and orderers can be referenced by slug (recommended for CI) or numeric ID. For approvals you can also pass --org Org1MSP and let the CLI auto-pick a peer of that organization.

Prerequisites

  • The channel exists and the participating peers have already joined it.
  • You have a ChainLaunch user with the CHAINCODE_CREATE and CHAINCODE_EXECUTE permissions (or use an admin token).
  • Your chaincode docker image is published to a registry the ChainLaunch host can pull from.

Common environment

export CHAINLAUNCH_API_URL="http://localhost:8100/api/v1"
export CHAINLAUNCH_USER="admin"
export CHAINLAUNCH_PASSWORD="adminpw"
 
export CHANNEL="vox"
export CC_NAME="asset-channel"
export CC_IMAGE="kfsoftware/external-chaincode:v1.0"
export CC_POLICY="AND('Org1MSP.peer','Org2MSP.peer')"
 
# Resolved once below; KEY_ID is the signing identity used by invoke/query.
export KEY_ID=""

The CLI reads CHAINLAUNCH_API_URL, CHAINLAUNCH_USER, and CHAINLAUNCH_PASSWORD automatically -- the same convention used by the rest of the chainlaunch CLI. Credentials can also be passed via the root command's persistent --auth-username / --auth-password (or --auth-bearer) flags, which take precedence over the environment. There is no CHAINLAUNCH_API_URL equivalent on the command line today -- set the env var to point at a non-default API URL.

a) Create a new chaincode definition + deploy the image

Run this once per chaincode and channel pair. Each step is idempotent: re-running them on a healthy environment does not break state.

1. Register the chaincode on the channel

chainlaunch fabric chaincode create \
  --channel "$CHANNEL" --chaincode "$CC_NAME"

If the chaincode already exists on the channel, the CLI prints the existing record and exits cleanly.

2. Create the initial definition

chainlaunch fabric chaincode definition create \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --image  "$CC_IMAGE" \
  --policy "$CC_POLICY"

Sequence is auto-set: max(highest local definition row, on-ledger committed sequence) + 1, or 1 for the first definition. The on-ledger sequence is queried through any joined peer of the channel; pass --peer slug-or-id to choose one explicitly. This means a fresh ChainLaunch instance attaching to a channel that already has chaincode at sequence 5 will create the next definition at sequence 6 -- not 1, which would fail to commit. Override the computed value with --sequence N if you really need to.

The version defaults to 1.0 for the first definition and is otherwise carried forward.

To preview the next sequence without creating anything (useful when scripting):

NEXT=$(chainlaunch fabric chaincode definition next-sequence \
  --channel "$CHANNEL" --chaincode "$CC_NAME")
echo "next sequence is $NEXT"

3. Install on the peers that will run the chaincode

Pass peer slugs (preferred for CI) or numeric IDs, repeatable or comma-separated:

chainlaunch fabric chaincode install \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --peer org1-peer0,org2-peer0

4. Approve from each organization

The CLI can auto-pick any peer attached to the channel for the given MSP:

chainlaunch fabric chaincode approve \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --org Org1MSP
 
chainlaunch fabric chaincode approve \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --org Org2MSP

Or target a specific peer with --peer org1-peer0.

5. Commit the definition to the channel

A single commit per definition. Use any peer that can submit the commit transaction:

chainlaunch fabric chaincode commit \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --org Org1MSP

6. Start the chaincode container

chainlaunch fabric chaincode deploy \
  --channel "$CHANNEL" --chaincode "$CC_NAME"

Pass --env KEY=VALUE (repeatable) for extra container environment variables.

7. Verify

chainlaunch fabric chaincode timeline \
  --channel "$CHANNEL" --chaincode "$CC_NAME"
 
chainlaunch fabric chaincode docker-info \
  --channel "$CHANNEL" --chaincode "$CC_NAME"
 
chainlaunch fabric chaincode logs --tail 50 \
  --channel "$CHANNEL" --chaincode "$CC_NAME"

b) Invoke and query the chaincode

After deploy, the same CLI can write transactions and read state via the API -- no Fabric SDK or peer binary needed. Both commands resolve the chaincode by --channel + --chaincode and require a server-side signing identity key ID (--key-id).

Resolve the signing key ID

The chainlaunch keys list command resolves an organization's identities by MSP ID. The --first --output id combination prints just the numeric key ID for shell capture:

export KEY_ID=$(chainlaunch keys list \
  --org Org1MSP --type client --first --output id)
 
echo "Using key id $KEY_ID"

Drop --first --output id to see the full list (JSON) with key names, algorithms, and statuses:

chainlaunch keys list --org Org1MSP --type client

Use --type admin or --type peer to filter for those identity classes. Pick an identity that belongs to an organization which has joined the channel.

Invoke (write)

InitLedger seeds the asset world state for the kfsoftware/external-chaincode example:

chainlaunch fabric chaincode invoke \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --fcn InitLedger --key-id "$KEY_ID"

Pass arguments with -a (repeatable). Add transient data with --transient KEY=VALUE -- binary payloads can be encoded as KEY=base64:<base64>:

chainlaunch fabric chaincode invoke \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --fcn CreateAsset --key-id "$KEY_ID" \
  -a asset-100 -a blue -a 5 -a Tomoko -a 300

Query (read)

GetAllAssets returns the full asset list as JSON:

chainlaunch fabric chaincode query \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --fcn GetAllAssets --key-id "$KEY_ID"

ReadAsset returns one asset by key:

chainlaunch fabric chaincode query \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --fcn ReadAsset --key-id "$KEY_ID" \
  -a asset1

By default the CLI prints the full envelope (status, message, result). Use --raw to print just the result payload (with surrounding quotes stripped) -- handy for piping into jq or capturing in shell variables:

asset=$(chainlaunch fabric chaincode query \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --fcn ReadAsset --key-id "$KEY_ID" \
  -a asset1 --raw)
echo "$asset" | jq '.color'

Use it as a CI smoke-test

Right after deploy, run a deterministic query and assert on the result. Failing the assertion fails the pipeline:

chainlaunch fabric chaincode invoke \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --fcn InitLedger --key-id "$KEY_ID"
 
count=$(chainlaunch fabric chaincode query \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --fcn GetAllAssets --key-id "$KEY_ID" --raw | jq 'length')
 
if [[ "$count" -lt 1 ]]; then
  echo "smoke-test failed: GetAllAssets returned $count assets after InitLedger" >&2
  exit 1
fi

c) Update the docker image (no policy or sequence change)

When the only change is a new container image, the existing definition is reused. The endorsement policy, version, and sequence persist; only the docker_image field is overwritten and the container is restarted.

export CC_IMAGE="kfsoftware/external-chaincode:v2.0"
 
chainlaunch fabric chaincode deploy \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --image "$CC_IMAGE"

That single call performs:

  1. PUT /api/v1/sc/fabric/definitions/{id} -- updates the latest definition's docker_image only.
  2. POST /api/v1/sc/fabric/definitions/{id}/deploy -- redeploys the container with the new image.

There is no install / approve / commit on this path because the on-channel definition is unchanged from a Fabric lifecycle standpoint.

If you want the image swap and the redeploy to be separate steps in your pipeline (so you can stop, run migrations, then start), use:

chainlaunch fabric chaincode definition update --image "$CC_IMAGE" \
  --channel "$CHANNEL" --chaincode "$CC_NAME"
 
chainlaunch fabric chaincode undeploy \
  --channel "$CHANNEL" --chaincode "$CC_NAME"
 
# ... pipeline steps that need the chaincode stopped ...
 
chainlaunch fabric chaincode deploy \
  --channel "$CHANNEL" --chaincode "$CC_NAME"

Environment-variable merge semantics

definition update --env KEY=VALUE (repeatable) merges with the existing encrypted env vars on the definition: keys you pass overwrite, keys you don't pass are preserved. To wipe everything, pass --clear-env. Passing neither leaves the existing env vars untouched.

# Override only LOG_LEVEL; everything else on the definition is kept.
chainlaunch fabric chaincode definition update \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --env LOG_LEVEL=debug
 
# Wipe all env vars and set a fresh pair.
chainlaunch fabric chaincode definition update \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --clear-env --env LOG_LEVEL=info --env REGION=eu-west-1

Targeting a non-latest definition

Every lifecycle and operations command (install, approve, commit, deploy, undeploy, logs, timeline, docker-info, definition update) accepts --definition-id N to act on a specific row instead of the latest one. Useful for rolling back, inspecting history, or coordinating staged rollouts:

chainlaunch fabric chaincode definition list \
  --channel "$CHANNEL" --chaincode "$CC_NAME"
 
chainlaunch fabric chaincode timeline \
  --channel "$CHANNEL" --chaincode "$CC_NAME" --definition-id 42

When you do need a new definition

If the endorsement policy changes, or you want to bump the version string or any field that affects on-channel state, create a new definition (sequence auto-bumps) and run install/approve/commit/deploy again:

chainlaunch fabric chaincode definition create \
  --channel "$CHANNEL" --chaincode "$CC_NAME" \
  --image  "$CC_IMAGE" \
  --policy "AND('Org1MSP.peer','Org2MSP.peer','Org3MSP.peer')" \
  --version "2.0"
 
# then: install (new package on each peer) -> approve per org -> commit -> deploy

Putting it in a pipeline

Drop this into a CI job (GitHub Actions, GitLab CI, anything that can run a shell). The first deploy uses the full lifecycle; subsequent runs short-circuit to image bumps because steps 1 and 2 are idempotent.

#!/usr/bin/env bash
set -euo pipefail
 
: "${CC_IMAGE:?CC_IMAGE must be set, e.g. kfsoftware/external-chaincode:${GIT_SHA}}"
 
# CHAINLAUNCH_API_URL / CHAINLAUNCH_USER / CHAINLAUNCH_PASSWORD are read from env
CL="chainlaunch fabric chaincode"
SCOPE=(--channel "$CHANNEL" --chaincode "$CC_NAME")
 
# Resolve the signing key once and reuse for invoke / query.
KEY_ID=$(chainlaunch keys list --org Org1MSP --type client --first --output id)
 
# Idempotent setup
$CL create     "${SCOPE[@]}"
 
# If a definition already exists this no-ops (the CLI will skip duplicate creates
# when the definition is unchanged; on first run it creates sequence=1 with the policy).
$CL definition create "${SCOPE[@]}" --image "$CC_IMAGE" --policy "$CC_POLICY" \
   || echo "definition already exists, continuing"
 
# Install / approve / commit run only on the first deployment; for image-only
# bumps they are no-ops or report 'redefine uncommitted' which the server tolerates.
$CL install    "${SCOPE[@]}" --peer org1-peer0,org2-peer0 || true
$CL approve    "${SCOPE[@]}" --org Org1MSP                || true
$CL approve    "${SCOPE[@]}" --org Org2MSP                || true
$CL commit     "${SCOPE[@]}" --org Org1MSP                || true
 
# Image bump + restart on every run
$CL deploy     "${SCOPE[@]}" --image "$CC_IMAGE"
 
# Verify
$CL docker-info "${SCOPE[@]}"
$CL logs --tail 50 "${SCOPE[@]}"
 
# Smoke-test: seed the world state and assert it's non-empty
$CL invoke "${SCOPE[@]}" --fcn InitLedger --key-id "$KEY_ID" || true
count=$($CL query "${SCOPE[@]}" --fcn GetAllAssets --key-id "$KEY_ID" --raw | jq 'length')
if [[ "$count" -lt 1 ]]; then
  echo "smoke-test failed: GetAllAssets returned $count assets" >&2
  exit 1
fi

If you want strict pipelines without the || true tolerance, branch on whether the definition exists (definition list) and skip install/approve/commit when no on-channel state has changed.

Troubleshooting

Symptom Likely cause Fix
no chaincode named "X" on channel "Y" Channel name typo, or chaincode never created Run chaincode create first
no peer of org "Org1MSP" is attached to network <id> Peer hasn't joined the channel yet Add the peer to the channel from the UI or API
Deploy succeeds but docker-info shows the old image Container running with cached image tag docker pull the new tag on the host or use immutable tags (:sha-abc123) in CI
redefine uncommitted warnings during approve An earlier approve already recorded for that org -- harmless Continue; the CLI tolerates this

See also