Promotes workloads across dev/staging/production on Control Plane. Use when the user asks about environment promotion, org-per-environment, cross-org image pulls, image promotion, deploying to production, or rollback.
10 stars
0 votes
0 copies
2 views
Added August 31, 2026
devopsgobashdockerterraformgitapici/cd
Works with
cli
api
mcp
Security analysis
A92/100
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Environment Promotion?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/controlplane-com-environment-promotion)
---
name: environment-promotion
description: "Promotes workloads across dev/staging/production on Control Plane. Use when the user asks about environment promotion, org-per-environment, cross-org image pulls, image promotion, deploying to production, or rollback."
---
# Environment Promotion
Control Plane has **no built-in promote or rollback primitive** — promotion is applying the same artifacts (image + manifests) to the next environment. Two topologies exist: **org-per-environment (the documented best practice)** and GVC-per-environment. The recurring failure is image access: a staging/prod org cannot pull the dev org's images until you either copy the image or wire up a cross-org pull secret.
## Choosing a topology
| Topology | Isolation | Image sharing | Best for |
|---|---|---|---|
| **Org per environment** (recommended) | Strongest — policies, secrets, users, audit fully separate | `cpln image copy` or cross-org pull secret | Production, compliance-sensitive teams |
| **GVC per environment** (one org) | Weak — shared org policies and access | Same org registry; no pull secret needed | Small teams, rapid iteration |
With org-per-environment, GVCs and workloads keep **identical names** in every org, so the same manifests apply unchanged — no environment suffixes in resource names. Org creation is account-level: Console, or `cpln org create --accountId ID --invitee EMAIL`.
## Promoting manifests
Keep manifests in git and apply them per environment — `cpln apply` is idempotent (PUT upsert) and resolves resource ordering:
```bash
cpln apply --file ./manifests/ --org my-org-staging --gvc my-gvc --ready # org-per-env: same files, next org
cpln apply --file ./manifests/ --org my-org --gvc staging-gvc --ready # gvc-per-env: same files, next GVC
```
- Bootstrap manifests from a live environment with `cpln <resource> get REF -o yaml-slim` (plain `yaml` output breaks apply).
- Environment differences (env vars, scaling, firewall) belong in the manifests per environment — or patch after apply with `mcp__cpln__update_workload` / `mcp__cpln__update_gvc` (both PATCH semantics).
- For IaC-based promotion, export live resources to Terraform: `mcp__cpln__export_terraform` (one self link, or bulk by path depth — a whole GVC or org), `mcp__cpln__convert_to_terraform` (manifest to HCL, dry-run validated). An unsupported kind is rejected with the supported list.
- **One workload into another GVC:** `mcp__cpln__promote_workload` (`workload`, `gvc`, `targetGvc`, optional `image` and `env` overrides) copies the spec without its identity or `cpln/` tags, creates or updates the target, grants its secret references, and waits. A volume set mount is refused: create the storage in the target GVC first.
## Sharing images across orgs
### Option A — copy the image (one-time promotions)
```bash
cpln image copy my-app:abc1234 --to-org my-org-prod # same credentials for both orgs
cpln image copy my-app:abc1234 --to-org my-org-prod --to-profile prod-profile --cleanup
```
CLI-only (no MCP tool) and **still requires a running Docker daemon — `copy` has no `--remote` mode** — it docker-logins both registries, then pulls, tags, pushes. `--to-name` renames during copy; `--cleanup` removes the local images (use in CI). Needs `pull` permission on the source image and `create` on images in the target org. After the copy the target references it as `//image/my-app:abc1234` — no pull secret.
### Option B — cross-org pull secret (continuous access)
The target org pulls directly from the source org's registry. Four steps:
1. **Source org — puller credentials**: `mcp__cpln__add_key_to_service_account` (full profile; creates the service account if missing; the key is shown **once**).
2. **Source org — grant pull**: `mcp__cpln__create_policy` with `targetKind: image`, `targetAll: true` (or `targetQuery` by repository), `addPermissions: ["pull"]`, `addServiceAccounts: [LINK]` — bindings go in the create call.
3. **Target org — docker secret**: have the user create a `docker` secret with this `dockerConfigJson` — offer a manifest scaffold (`data` is this JSON as one string; `setup-secret` skill); the username is the **literal string `<token>`** (the registry rejects anything else; the password is the service-account key):
```json
{ "auths": { "my-org-dev.registry.cpln.io": { "username": "<token>", "password": "SERVICE_ACCOUNT_KEY" } } }
```
4. **Target org — attach to the GVC**: `mcp__cpln__update_gvc` with `pullSecretLinks: ["//secret/dev-registry-pull"]` (merged with existing), then reference the image by its **full registry hostname** in the workload spec:
```yaml
spec:
containers:
- name: main
image: my-org-dev.registry.cpln.io/my-app:abc1234
```
CLI fallback for the same four steps:
```bash
cpln serviceaccount create --name image-puller --org my-org-dev # CLI does NOT auto-create on add-key
cpln serviceaccount add-key image-puller --description "cross-org pull" --org my-org-dev # save the key
cpln policy create --name image-pull --target-kind image --all --org my-org-dev
cpln policy add-binding image-pull --serviceaccount image-puller --permission pull --org my-org-dev
# the user creates the dev-registry-pull docker secret in my-org-prod, then:
cpln gvc update my-gvc --set 'spec.pullSecretLinks+=//secret/dev-registry-pull' --org my-org-prod
```
Same-org images never need a pull secret — the platform injects a default registry credential for the org's own registry automatically.
## Image tags across environments
- **Promote immutable tags** (git SHA `my-app:abc1234` or semver `my-app:v1.2.3`) — promote the exact artifact you tested; mutable tags (`latest`, `staging`) make rollback unreliable. Digest pins (`my-app@sha256:...`) are maximally reproducible.
- **`supportDynamicTags`** (workload spec, default `false`): redeploys the workload automatically when a tag's underlying digest changes (within ~5 minutes) — useful for dev environments on mutable tags, wrong for production promotion.
## CI/CD promotion pipeline
The CLI is the primary interface in pipelines; `CPLN_TOKEN` alone is enough (no profile needed — see the `cpln` skill). The shape that works:
```yaml
# Build once in dev, then per stage: copy the image + apply the manifests
- run: cpln image build --name my-app:${{ github.sha }} --push # CPLN_TOKEN + CPLN_ORG=my-org-dev
- run: cpln apply --file ./manifests/ --gvc my-gvc --ready
# staging / prod stages (gate each with environment approvals):
- run: cpln image copy my-app:${{ github.sha }} --to-org my-org-prod --cleanup # dev-org token
- run: cpln apply --file ./manifests/ --gvc my-gvc --org my-org-prod --ready # prod-org token
```
- **One service-account token per org** — a dev-org token must not be able to touch prod; the copy step runs with source-org credentials plus a `--to-profile` (or pre-run `cpln image docker-login`) for the target.
- **`--ready` gates promotion** — it polls until workloads are healthy (5s interval, up to 5 min) and fails the job otherwise.
- Approval gates (GitHub environments, GitLab manual jobs) go between stages. Full pipeline setup, npm install (`@controlplane/cli`), runners: `gitops-cicd` skill.
## Rollback
There is no deployment-history rollback — rolling back means **re-pointing the workload at the previous known-good image** (keep the previous tag in git history or your pipeline metadata):
```bash
cpln workload update my-app --set spec.containers.main.image=//image/my-app:v1.1.0 --gvc my-gvc --org my-org
cpln workload get-deployments my-app --gvc my-gvc --org my-org # verify every location reports ready
```
- MCP path: `mcp__cpln__rollback_workload` (`name`, optional `image`) re-points the first container at the newest earlier image the deployments recorded, or at the image passed, and waits.
- Org-per-environment: confirm the older image still exists in **this** org's registry first (`mcp__cpln__list_resources` kind="image") — it may only have been copied forward once.
- **Restart without changing the image**: `mcp__cpln__restart_workload`, or `cpln workload force-redeployment my-app --gvc GVC`; both stamp the `cpln/deployTimestamp` tag, a rolling restart.
- Helm-managed releases are the exception with real revision history: `cpln helm rollback RELEASE [REVISION]`.