Environment Management APIs
API endpoints for managing environments, targets, agents, freeze windows, and inventory.
Status: Planned (not yet implemented) Source: Architecture Advisory Section 6.3.2 Related Modules: Environment Manager, Agents
Runtime note: environment APIs are documented here; controller implementation sequencing is tracked in
../promotion-runtime-gap-closure-plan.md.
Overview
The Environment Management API provides CRUD operations for environments, target groups, deployment targets, agents, freeze windows, and inventory synchronization. All endpoints require authentication and respect tenant isolation via Row-Level Security.
Environment Endpoints
Create Environment
Endpoint: POST /api/v1/environments
Request:
{
"name": "production",
"displayName": "Production",
"orderIndex": 3,
"config": {
"deploymentTimeout": 600,
"healthCheckInterval": 30
},
"requiredApprovals": 2,
"requireSod": true,
"promotionPolicy": "default"
}
Response: 201 Created
{
"id": "uuid",
"name": "production",
"displayName": "Production",
"orderIndex": 3,
"isProduction": true,
"requiredApprovals": 2,
"requireSeparationOfDuties": true,
"createdAt": "2026-01-10T14:23:45Z"
}
List Environments
Endpoint: GET /api/v1/environments
Query Parameters:
includeState(boolean): Include current release state
Response: 200 OK
[
{
"id": "uuid",
"name": "development",
"displayName": "Development",
"orderIndex": 1,
"currentRelease": {
"id": "release-uuid",
"name": "myapp-v2.3.1",
"deployedAt": "2026-01-09T10:00:00Z"
}
}
]
Get Environment
Endpoint: GET /api/v1/environments/{id}
Response: 200 OK - Full environment details
Update Environment
Endpoint: PUT /api/v1/environments/{id}
Request: Partial environment object
Response: 200 OK - Updated environment
Delete Environment
Endpoint: DELETE /api/v1/environments/{id}
Response: 200 OK
{ "deleted": true }
Set Environment Lifecycle (archive / dispose / restore)
Sprint 20260717 (G9); reviewed/hardened in SPRINT_20260720_006. Explicit, owner-confirmed lifecycle transition. This is the only way an environment is demoted from the estate matrix, readiness table, and release/verify/custody pickers — the state is never inferred from the environment name (a name heuristic was rejected as dishonest).
Endpoint: PUT /api/v1/release-orchestrator/environments/{id}/lifecycle
Request:
{ "state": "archived" }
state is one of:
active— first-class estate member; shown and ranked normally everywhere (default for every row).archived— retired but retained; demoted out of the default estate/matrix/picker views, still resolvable by id and listable when archived rows are explicitly requested.disposable— ephemeral e2e / proof-run debris; hidden by default AND owner-confirmed safe to clean up.
Response: 200 OK — the updated environment (includes the lifecycleState field). Emits the set_environment_lifecycle platform audit event.
Every environment read now carries lifecycleState (Active | Archived | Disposable). The list endpoint accepts an optional ?lifecycle= filter (all [default], active, demoted, archived, disposable) so pickers can request the active estate directly instead of demoting client-side.
Endpoint (target): PUT /api/v1/release-orchestrator/targets/{id}/lifecycle — same body/semantics for a single deployment target; every target read carries lifecycleState and the transition emits the set_target_lifecycle platform audit event. Backed by two idempotent, forward-only embedded-SQL migrations: 006_environment_lifecycle_state.sql adds lifecycle_state to release.environments and release.targets, backfills every existing row to active (no row is auto-demoted), and adds partial active-only indexes; 007_environment_lifecycle_state_constraints.sql adds a fail-closed CHECK (lifecycle_state IN ('active', 'archived', 'disposable')) on both tables so a hand-edited/unknown token can never widen an estate view. Both endpoints require the Operate scope and validate the token (400 invalid_lifecycle_state otherwise); a missing target returns 404 target_not_found (tenant-scoped, so a cross-tenant id is indistinguishable from a missing one).
Console consumption (2026-07-21, iteration-2 estate-cleanup)
The console is wired to this lifecycle end-to-end:
- Archive action: each card on
/releases/environmentscarries Archive / Mark disposable / Restore in its “…” menu (Operate scope); the store PUTs this endpoint and refreshes the shared estate-wide lifecycle index (EnvironmentLifecycleService, fed by one un-enrichedlistEnvironmentLifecyclesread). - Default demotion with disclosure: archived/disposable environments are demoted out of the environment cards page, the promote-wizard target picker, the approval/bind + policy-simulation suggests, the promotion-readiness matrix, the estate matrix (
/evidence/estate+ dashboard embed — demoted columns AND service rows that exist only in them), the topology graph (/topologyand/environments/overview— nodes, hosts, edges, summary counts), and the verify picker (releases whosetargetEnvironmentis demoted). Every demotion renders the hidden count with a one-click “show archived” reveal — hiding is never silent. - Fail-open honesty: an unknown/missing
lifecycleState, an ambiguous cross-registry key, or a failed lifecycle read demotes nothing — uncertainty never hides estate. Deep links naming an archived environment/release auto-reveal it (identity beats demotion). - The user list analog: Console Admin → Users defaults to an Active facet (Disable = the demote action) with Active/Disabled/All counts always visible; search spans all statuses.
Freeze Window Endpoints
Create Freeze Window
Endpoint: POST /api/v1/environments/{envId}/freeze-windows
Request:
{
"start": "2026-01-15T00:00:00Z",
"end": "2026-01-20T00:00:00Z",
"reason": "Holiday freeze",
"exceptions": ["user-uuid-1", "user-uuid-2"]
}
Response: 201 Created
{
"id": "uuid",
"environmentId": "env-uuid",
"start": "2026-01-15T00:00:00Z",
"end": "2026-01-20T00:00:00Z",
"reason": "Holiday freeze",
"createdBy": "user-uuid"
}
List Freeze Windows
Endpoint: GET /api/v1/environments/{envId}/freeze-windows
Query Parameters:
active(boolean): Filter to active freeze windows only
Response: 200 OK - Array of freeze windows
Delete Freeze Window
Endpoint: DELETE /api/v1/environments/{envId}/freeze-windows/{windowId}
Response: 200 OK
{ "deleted": true }
Target Group Endpoints
Create Target Group
Endpoint: POST /api/v1/environments/{envId}/target-groups
List Target Groups
Endpoint: GET /api/v1/environments/{envId}/target-groups
Get Target Group
Endpoint: GET /api/v1/target-groups/{id}
Update Target Group
Endpoint: PUT /api/v1/target-groups/{id}
Delete Target Group
Endpoint: DELETE /api/v1/target-groups/{id}
Target Endpoints
Create Target
Endpoint: POST /api/v1/targets
Request:
{
"environmentId": "env-uuid",
"targetGroupId": "group-uuid",
"name": "prod-web-01",
"targetType": "docker_host",
"connection": {
"host": "192.168.1.100",
"port": 2375,
"tlsEnabled": true
},
"labels": {
"role": "web",
"datacenter": "us-east-1"
},
"deploymentDirectory": "/opt/deployments"
}
Response: 201 Created
{
"id": "uuid",
"name": "prod-web-01",
"targetType": "docker_host",
"healthStatus": "unknown",
"createdAt": "2026-01-10T14:23:45Z"
}
List Targets
Endpoint: GET /api/v1/targets
Query Parameters:
environmentId(UUID): Filter by environmenttargetType(string): Filter by type (docker_host,compose_host,ecs_service,nomad_job)labels(JSON): Filter by labelshealthStatus(string): Filter by health status
Response: 200 OK - Array of targets
Get Target
Endpoint: GET /api/v1/targets/{id}
Update Target
Endpoint: PUT /api/v1/targets/{id}
Delete Target
Endpoint: DELETE /api/v1/targets/{id}
Trigger Health Check
Endpoint: POST /api/v1/targets/{id}/health-check
Response: 200 OK
{
"status": "healthy",
"message": "Docker daemon responding",
"checkedAt": "2026-01-10T14:23:45Z"
}
Get Version Sticker
Endpoint: GET /api/v1/targets/{id}/sticker
Response: 200 OK
{
"releaseId": "uuid",
"releaseName": "myapp-v2.3.1",
"components": [
{
"componentId": "uuid",
"componentName": "api",
"digest": "sha256:abc123..."
}
],
"deployedAt": "2026-01-09T10:00:00Z",
"deployedBy": "user-uuid"
}
Check Drift
Endpoint: GET /api/v1/targets/{id}/drift
Response: 200 OK
{
"hasDrift": true,
"expected": { "releaseId": "uuid", "digest": "sha256:abc..." },
"actual": { "digest": "sha256:def..." },
"differences": [
{ "component": "api", "expected": "sha256:abc...", "actual": "sha256:def..." }
]
}
