{"protocol":"clank-doc/1","frameworkVersion":"0.24.0","slug":"environment-promotions","title":"Artifact promotion across environments","description":"An environment family binds development, staging and production to ordinary independent projects in one workspace. Promotion sends a retained, verified compressed artifact through the target deployment path without rebuilding it. The target","group":{"id":"deploy","title":"Deploy and operate"},"url":"https://docs.clank.run/docs/environment-promotions","source":"docs/environment-promotions.md","headings":["Configure and review","Migration and hosting policies","Retry, failure and recovery","HTTP and consumer types"],"tableOfContents":[{"id":"configure-and-review","title":"Configure and review","level":2},{"id":"migration-and-hosting-policies","title":"Migration and hosting policies","level":2},{"id":"retry-failure-and-recovery","title":"Retry, failure and recovery","level":2},{"id":"http-and-consumer-types","title":"HTTP and consumer types","level":2}],"markdown":"# Artifact promotion across environments\n\nAn environment family binds development, staging and production to ordinary independent\nprojects in one workspace. Promotion sends a retained, verified compressed artifact through\nthe target deployment path without rebuilding it. The target resolves its own current secrets,\ndatabase, bucket namespace, quotas and migration policy. Source data and secrets stay with the\nsource project.\n\nUse **Project → Environments** or `clank environment` in a directory linked to the family root.\nA project can belong to one environment; previews and nested families are rejected. Binding\nversions increase after every configuration change, including unbinding.\n\n## Configure and review\n\nCreate independent projects, then bind their exact IDs:\n\n```sh\nclank environment list --json\nclank environment bind development PROJECT_DEVELOPMENT --expected-version=0\nclank environment bind staging PROJECT_STAGING --expected-version=0 --migration-policy=apply-safe\nclank environment bind production PROJECT_PRODUCTION --expected-version=0 --migration-policy=code-only\n```\n\nWorkspace owners/admins configure bindings. Development/staging promotion requires current\ndeploy permission on the family root and target, plus read permission on the source.\nProduction promotion and direct production deployment/rollback require an owner/admin.\nProject-scoped credentials cannot cross these projects. The platform rechecks sessions,\nmembership, project permissions and binding versions while work runs and when activation commits.\n\nChoose a source release that activated successfully and retains its original upload. Capture\nits immutable release ID and SHA-256, the target binding version and current active release ID.\nThe dashboard's **Review promotion** captures these values before enabling **Promote**.\nNavigation between projects retains a pending request in that browser session. Reloading\nrequires reviewing again; durable history remains on the server.\n\n```sh\nclank environment promote staging --from=development \\\n  --release=SOURCE_RELEASE --digest=EXACT_SHA256 \\\n  --expected-version=1 --expected-active=TARGET_RELEASE \\\n  --key=promotion_request_0001 --json\nclank environment history staging --json\n```\n\nFor an uninitialized target under `apply-safe`, pass `--expected-active=none` explicitly.\nThe CLI never runs a build command for promotion. Signed-release installations require\n`--attestation=target-attestation.json`, binding the unchanged artifact to the target project.\nA source-project attestation cannot authorize the target. The dashboard accepts the same\nbounded signed JSON during review. See [release attestations](release-attestations.md).\n\n## Migration and hosting policies\n\n`apply-safe` applies pending safe target migrations and rejects an artifact that enables unsafe\nmigrations. `code-only` requires an initialized target with no pending or changed migrations.\nProduction defaults to `code-only`; other environments default to `apply-safe`. Policy changes\nincrement the binding version and invalidate previous reviews.\n\nTrusted local process hosting supports both policies and remains explicitly trusted. The\ninitial provider path supports a stable, initialized, co-located loopback provider with\n`code-only` policy and an identical migration manifest. Configure operator-owned certificates\nwhen opening the platform:\n\n```ts\nimport { openPlatform } from \"@clank.run/framework/platform\";\n\nconst platform = await openPlatform({\n  dataDirectory: \"/operator/platform\",\n  publicUrl: \"https://deploy.example.com\",\n  deploymentAgents: { registrationToken: process.env.RUNNER_REGISTRATION_TOKEN },\n  providerPromotionHosts: {\n    certified_node: {\n      directory: \"/operator/certificates/certified_node\",\n      profile: {\n        mode: \"docker-isolated\",\n        image: \"node@sha256:EXACT_IMAGE_SHA256\",\n        user: \"1000:1000\",\n        diskQuota: { mountDirectory: \"/provider-data\", hardBytes: 33554432, hardFiles: 64 },\n        outboundNetwork: { allowCidrs: [] },\n        networkProbe: { deniedAddress: \"9.9.9.9\" },\n      },\n    },\n  },\n});\n```\n\nReplace the image placeholder with an immutable digest. This trusted operator registry must\nmatch the node's actual Docker, XFS and network profile. See [Linux host\ncertification](linux-host-certification.md) for explicit disposable probes. The platform\ninspects the authenticated report before staging and again before activation. Missing, expired,\nblocked or changed reports prevent acceptance. Node labels are not certificates; this is\npoint-in-time admission. Remote provider proof, provider `apply-safe`, and the legacy local\nDocker runner's exact profile proof are unsupported and fail before staging. The registry is\nbounded to 100 node IDs.\n\n## Retry, failure and recovery\n\nKeep the complete request, attestation and key after a lost response. An exact accepted retry\nreturns the same target release without reactivating it; changed requests with that key are\nrejected. Current authorization and binding versions remain required for replay. Each family\nretains at most 1,000 receipts; history returns up to 100 currently authorized records. Release\ncleanup may remove runtime bytes while retaining safe provenance. Older local releases without\ntheir original compressed upload cannot be reconstructed for promotion. New uploads retain and\ncharge those bytes against project storage capacity.\n\nAccepted receipt metadata and target activation commit together. A pending provider request\nresumes its original generation on an exact retry, including after controller restart. A new\nkey cannot bypass an interrupted target's fence. Failed keys are terminal; inspect the target\nbefore creating another request.\n\nFor local migrations, the platform stops prior writers before taking a safety copy. Failed\nhealth or migration restores that copy and verifies the exact prior runtime. Unverifiable\ncleanup/restoration leaves `recovery-required`. Controller death during migration requires\nexplicit recovery before startup, ingress wake-up, deployment or rollback can launch another\nwriter.\n\nProvider code-only recovery quiesces the candidate through the fenced generation and data\njournal, then restores the exact prior artifact on its pinned node. Uncommitted data application\nis recovered. Already committed application writes remain under code-only policy, including\nwrites from an unpublished healthy candidate. Local code-only rollback also preserves committed\ndata. Release rollback does not undo external side effects. Missing host proof or uncertain\ncleanup keeps provider recovery fenced.\n\nInspect history and the active target, then recover with recent authentication and an exact\nconfirmation:\n\n```sh\nclank environment recover staging PROMOTION_KEY \\\n  --confirm=\"recover-promotion TARGET_SLUG PROMOTION_KEY\" --json\nclank environment unbind staging --expected-version=CURRENT_VERSION\n```\n\nRecovery verifies ownership, the prior release, the unchanged authoritative target and required\nsnapshot/host proof. It can cancel an unstaged pending receipt after proving no release exists.\nAccepted/failed receipts remain terminal. Recovery cannot overwrite a target that advanced\nindependently. Unbinding preserves data/releases. Bound projects and families with unresolved\npromotion receipts cannot be deleted.\n\n## HTTP and consumer types\n\nAll paths are beneath `/api/projects/:root/environments`, use existing browser CSRF or bearer\nauthentication, and reject unknown fields:\n\n| Method and suffix | Request / result |\n| --- | --- |\n| `GET /` | `{ environments }`, authorized bindings and unbound tombstones |\n| `PUT /:name` | `{ projectId, expectedVersion, migrationPolicy? }` → `{ environment }` |\n| `DELETE /:name` | `{ expectedVersion }` → unbound versioned `{ environment }` |\n| `POST /:name/promotions` | `{ sourceEnvironment, releaseId, digest, expectedVersion, expectedActiveReleaseId, idempotencyKey }` → `{ promotion, release }`, status 201 |\n| `GET /:name/promotions` | `{ promotions }`, bounded authorized history |\n| `POST /:name/promotions/:key/recover` | `{ confirmation }` → `{ promotion }` |\n\nPass a signed target attestation through `x-clank-release-attestation` using the existing header\nencoding. Receipts have `pending`, `staging`, `accepted`, `failed` or `recovery-required` state.\nStale bindings/targets and changed retries return 409, as do unsupported policy/proof. A still\nconverging provider returns 503 with retry guidance. Failed health/migration returns 422 after\nverified recovery. An unresolved recovery remains fenced and must not be treated as accepted.\n\n`@clank.run/framework/platform` exports `PlatformEnvironmentName`,\n`PlatformEnvironmentMigrationPolicy`, `PlatformEnvironment`,\n`PlatformEnvironmentBindingRequest`, `PlatformPromotionRequest` and `PlatformPromotion`.\nThese describe the HTTP contract: `expectedActiveReleaseId` is required and explicitly nullable;\nreceipt fields are readonly observations.\n\nThe control-store migration adds bindings and receipts without rewriting existing releases.\nRolling back application code disables the endpoints while preserving these tables and project\ndata. Complete unresolved recovery with a compatible controller before rollback; disabling\nendpoints does not undo accepted promotions or resolve staged provider generations.\n\nUse [persistent release channels](release-channels.md) to retain named immutable artifact history,\nreview explicit promotion or rollback, and retire pins without deleting uploads or application data.\n\nAutomatic provider compensation restores the prior active generation’s frozen runtime environment,\nincluding its original secret revisions. Resolving newly edited secrets again could make both\nthe candidate and its prior code fail health checks. New promotions and explicit deployment\ncontinue to resolve current target secrets; compensation restores the runtime authorized before\nthe failed candidate and rechecks its exact generation, node and host admission.\n"}