Artifact promotion across environments

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

6 min read1,114 wordsClank 0.24.0
On this page

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 resolves its own current secrets, database, bucket namespace, quotas and migration policy. Source data and secrets stay with the source project.

Use Project → Environments or clank environment in a directory linked to the family root. A project can belong to one environment; previews and nested families are rejected. Binding versions increase after every configuration change, including unbinding.

Configure and review

Create independent projects, then bind their exact IDs:

sh
clank environment list --json
clank environment bind development PROJECT_DEVELOPMENT --expected-version=0
clank environment bind staging PROJECT_STAGING --expected-version=0 --migration-policy=apply-safe
clank environment bind production PROJECT_PRODUCTION --expected-version=0 --migration-policy=code-only

Workspace owners/admins configure bindings. Development/staging promotion requires current deploy permission on the family root and target, plus read permission on the source. Production promotion and direct production deployment/rollback require an owner/admin. Project-scoped credentials cannot cross these projects. The platform rechecks sessions, membership, project permissions and binding versions while work runs and when activation commits.

Choose a source release that activated successfully and retains its original upload. Capture its immutable release ID and SHA-256, the target binding version and current active release ID. The dashboard's Review promotion captures these values before enabling Promote. Navigation between projects retains a pending request in that browser session. Reloading requires reviewing again; durable history remains on the server.

sh
clank environment promote staging --from=development \
  --release=SOURCE_RELEASE --digest=EXACT_SHA256 \
  --expected-version=1 --expected-active=TARGET_RELEASE \
  --key=promotion_request_0001 --json
clank environment history staging --json

For an uninitialized target under apply-safe, pass --expected-active=none explicitly. The CLI never runs a build command for promotion. Signed-release installations require --attestation=target-attestation.json, binding the unchanged artifact to the target project. A source-project attestation cannot authorize the target. The dashboard accepts the same bounded signed JSON during review. See release attestations.

Migration and hosting policies

apply-safe applies pending safe target migrations and rejects an artifact that enables unsafe migrations. code-only requires an initialized target with no pending or changed migrations. Production defaults to code-only; other environments default to apply-safe. Policy changes increment the binding version and invalidate previous reviews.

Trusted local process hosting supports both policies and remains explicitly trusted. The initial provider path supports a stable, initialized, co-located loopback provider with code-only policy and an identical migration manifest. Configure operator-owned certificates when opening the platform:

ts
import { openPlatform } from "@clank.run/framework/platform";

const platform = await openPlatform({
  dataDirectory: "/operator/platform",
  publicUrl: "https://deploy.example.com",
  deploymentAgents: { registrationToken: process.env.RUNNER_REGISTRATION_TOKEN },
  providerPromotionHosts: {
    certified_node: {
      directory: "/operator/certificates/certified_node",
      profile: {
        mode: "docker-isolated",
        image: "node@sha256:EXACT_IMAGE_SHA256",
        user: "1000:1000",
        diskQuota: { mountDirectory: "/provider-data", hardBytes: 33554432, hardFiles: 64 },
        outboundNetwork: { allowCidrs: [] },
        networkProbe: { deniedAddress: "9.9.9.9" },
      },
    },
  },
});

Replace the image placeholder with an immutable digest. This trusted operator registry must match the node's actual Docker, XFS and network profile. See Linux host certification for explicit disposable probes. The platform inspects the authenticated report before staging and again before activation. Missing, expired, blocked or changed reports prevent acceptance. Node labels are not certificates; this is point-in-time admission. Remote provider proof, provider apply-safe, and the legacy local Docker runner's exact profile proof are unsupported and fail before staging. The registry is bounded to 100 node IDs.

Retry, failure and recovery

Keep the complete request, attestation and key after a lost response. An exact accepted retry returns the same target release without reactivating it; changed requests with that key are rejected. Current authorization and binding versions remain required for replay. Each family retains at most 1,000 receipts; history returns up to 100 currently authorized records. Release cleanup may remove runtime bytes while retaining safe provenance. Older local releases without their original compressed upload cannot be reconstructed for promotion. New uploads retain and charge those bytes against project storage capacity.

Accepted receipt metadata and target activation commit together. A pending provider request resumes its original generation on an exact retry, including after controller restart. A new key cannot bypass an interrupted target's fence. Failed keys are terminal; inspect the target before creating another request.

For local migrations, the platform stops prior writers before taking a safety copy. Failed health or migration restores that copy and verifies the exact prior runtime. Unverifiable cleanup/restoration leaves recovery-required. Controller death during migration requires explicit recovery before startup, ingress wake-up, deployment or rollback can launch another writer.

Provider code-only recovery quiesces the candidate through the fenced generation and data journal, then restores the exact prior artifact on its pinned node. Uncommitted data application is recovered. Already committed application writes remain under code-only policy, including writes from an unpublished healthy candidate. Local code-only rollback also preserves committed data. Release rollback does not undo external side effects. Missing host proof or uncertain cleanup keeps provider recovery fenced.

Inspect history and the active target, then recover with recent authentication and an exact confirmation:

sh
clank environment recover staging PROMOTION_KEY \
  --confirm="recover-promotion TARGET_SLUG PROMOTION_KEY" --json
clank environment unbind staging --expected-version=CURRENT_VERSION

Recovery verifies ownership, the prior release, the unchanged authoritative target and required snapshot/host proof. It can cancel an unstaged pending receipt after proving no release exists. Accepted/failed receipts remain terminal. Recovery cannot overwrite a target that advanced independently. Unbinding preserves data/releases. Bound projects and families with unresolved promotion receipts cannot be deleted.

HTTP and consumer types

All paths are beneath /api/projects/:root/environments, use existing browser CSRF or bearer authentication, and reject unknown fields:

Method and suffixRequest / result
GET /{ environments }, authorized bindings and unbound tombstones
PUT /:name{ projectId, expectedVersion, migrationPolicy? } → { environment }
DELETE /:name{ expectedVersion } → unbound versioned { environment }
POST /:name/promotions{ sourceEnvironment, releaseId, digest, expectedVersion, expectedActiveReleaseId, idempotencyKey } → { promotion, release }, status 201
GET /:name/promotions{ promotions }, bounded authorized history
POST /:name/promotions/:key/recover{ confirmation } → { promotion }

Pass a signed target attestation through x-clank-release-attestation using the existing header encoding. Receipts have pending, staging, accepted, failed or recovery-required state. Stale bindings/targets and changed retries return 409, as do unsupported policy/proof. A still converging provider returns 503 with retry guidance. Failed health/migration returns 422 after verified recovery. An unresolved recovery remains fenced and must not be treated as accepted.

@clank.run/framework/platform exports PlatformEnvironmentName, PlatformEnvironmentMigrationPolicy, PlatformEnvironment, PlatformEnvironmentBindingRequest, PlatformPromotionRequest and PlatformPromotion. These describe the HTTP contract: expectedActiveReleaseId is required and explicitly nullable; receipt fields are readonly observations.

The control-store migration adds bindings and receipts without rewriting existing releases. Rolling back application code disables the endpoints while preserving these tables and project data. Complete unresolved recovery with a compatible controller before rollback; disabling endpoints does not undo accepted promotions or resolve staged provider generations.

Use persistent release channels to retain named immutable artifact history, review explicit promotion or rollback, and retire pins without deleting uploads or application data.

Automatic provider compensation restores the prior active generation’s frozen runtime environment, including its original secret revisions. Resolving newly edited secrets again could make both the candidate and its prior code fail health checks. New promotions and explicit deployment continue to resolve current target secrets; compensation restores the runtime authorized before the failed candidate and rechecks its exact generation, node and host admission.