Promotion & Approval APIs
API endpoints for managing promotions, approvals, and gate evaluations.
Status: Planned (not yet implemented) Source: Architecture Advisory Section 6.3.5 Related Modules: Promotion Manager Module, Workflow Promotion
Runtime note: promotion libraries and tests exist, while the API host controllers for these endpoints are tracked in
../promotion-runtime-gap-closure-plan.md. Compatibility note (2026-04-27):/api/v1/release-orchestrator/releases/{id}/promoteand/deployare implemented as transitional release-control endpoints. Release and approval compatibility records are stored inrelease_orchestratorrelease-truth tables. Promote/deploy fail closed when approval, gate-decision, or verified evidence truth is missing.Promotion target references accept the legacy
env-*aliases, environment names, or UUIDs. A resolved target persists its canonical name and configured approval quorum (minimum one); an explicit positive gate/policyrequiredApprovalsvalue overrides it. Unresolved legacy references retain the compatibility quorum of two.
Queue hygiene (as built — Sprint 20260703_002 / DTC-13)
Approval rows are reconciled lazily on read (no hosted sweep job; offline-friendly). Every queue read surface (GET /api/release-orchestrator/approvals, GET /api/v1/approvals, single-approval reads, and the promote path before its dedupe check) funnels rows through ApprovalQueueHygiene (src/ReleaseOrchestrator/__Apps/StellaOps.ReleaseOrchestrator.WebApi/Services/ApprovalQueueHygiene.cs), which transitions and persists:
- Expiry —
pending+ pastexpiresAt⇒expired. Expired rows leave pending counts/badges;POST …/approvals/{id}/approve|rejecton an expired row returns409 { "error": "approval_expired" }. - Supersede — multiple
pendingrows for the same (release, target environment): the newest request survives, older duplicates ⇒superseded(action record names the surviving approval id). A new promote request therefore supersedes older pendings. - Deploying reconcile — an approval stranded in
deploying(the deploying→deployed transition runs only inside the in-process deploy scope, so a service restart mid-deploy strands it) is reconciled against the latest matchingrelease_orchestrator.deploymentsrow:succeeded⇒deployed,failed⇒failed,cancelled⇒cancelled,rolled_back⇒rolled_back; in-flight deployments are left untouched.
Every hygiene transition appends an action record (actor release-orchestrator) so the history stays auditable.
Bulk cancel: POST /api/{v1/}release-orchestrator/approvals/batch-cancel { "ids": [...], "comment": "..." } cancels OPEN rows (pending/expired) and returns { "cancelled": [summaries], "skippedIds": [...] }. Authorization: policy release-orchestrator.approval.cancel — satisfied by release:write or release:publish (any-of).
Requester attribution: requestedBy and decision action records now carry the actor’s display name. The gateway forwards the token’s name/preferred_username/email claims through the identity envelope (they were previously dropped, so services stored the raw sub UUID); ActorResolution.ResolveDisplayName prefers those claims over the X-StellaOps-Actor header (which stays the opaque subject id and is still what audit rows record as actorId). Historical UUID rows are labelled honestly in the console (“Unresolved identity (…)” / “System (…)”).
Overview
The Promotion API provides endpoints for requesting release promotions between environments, managing approvals, and evaluating promotion gates. Promotions enforce separation of duties (SoD) and require configured approvals before deployment proceeds.
Gate contract references:
../../policy/promotion-gate-ownership-contract.md../../evidence-locker/promotion-evidence-contract.md
Promotion Endpoints
Create Promotion Request
Endpoint: POST /api/v1/promotions
Initiates a promotion request for a release to a target environment.
Request:
{
"releaseId": "uuid",
"targetEnvironmentId": "uuid",
"reason": "Deploying v2.3.1 with critical bug fix"
}
Response: 201 Created
{
"id": "uuid",
"releaseId": "uuid",
"releaseName": "myapp-v2.3.1",
"sourceEnvironmentId": "uuid",
"sourceEnvironmentName": "Staging",
"targetEnvironmentId": "uuid",
"targetEnvironmentName": "Production",
"status": "pending",
"requestedBy": "user-uuid",
"requestedAt": "2026-01-10T14:23:45Z",
"reason": "Deploying v2.3.1 with critical bug fix"
}
Status Flow:
pending -> awaiting_approval -> approved -> deploying -> deployed
-> rejected
-> cancelled
-> failed
-> rolled_back
# DTC-13 queue hygiene (lazy-on-read):
pending -> expired (past expiresAt without a decision)
pending -> superseded (newer pending request for the same release + target env)
pending -> cancelled (bulk-cancel from the approvals queue)
List Promotions
Endpoint: GET /api/v1/promotions
Query Parameters:
status(string): Filter by statusreleaseId(UUID): Filter by releaseenvironmentId(UUID): Filter by target environmentpage(number): Page number
Response: 200 OK
{
"data": [
{
"id": "uuid",
"releaseName": "myapp-v2.3.1",
"targetEnvironmentName": "Production",
"status": "awaiting_approval",
"requestedAt": "2026-01-10T14:23:45Z"
}
],
"meta": { "page": 1, "totalCount": 25 }
}
Get Promotion
Endpoint: GET /api/v1/promotions/{id}
Response: 200 OK - Full promotion with decision record and approvals
Approve Promotion
Endpoint: POST /api/v1/promotions/{id}/approve
Request:
{
"comment": "Approved after reviewing security scan results"
}
Response: 200 OK
{
"id": "uuid",
"status": "approved",
"approvalCount": 2,
"requiredApprovals": 2,
"decidedAt": "2026-01-10T14:30:00Z"
}
Notes:
- Separation of Duties (SoD): The user who requested the promotion cannot approve it if
requireSodis enabled on the environment - Multi-party approval: Promotion proceeds when
approvalCount >= requiredApprovals - Approval decisions are idempotent per actor: repeated approvals by the same actor do not increase
approvalCountor create duplicate durable action records.
Reject Promotion
Endpoint: POST /api/v1/promotions/{id}/reject
Request:
{
"reason": "Security vulnerabilities not addressed"
}
Response: 200 OK - Updated promotion with status: rejected
Cancel Promotion
Endpoint: POST /api/v1/promotions/{id}/cancel
Cancels a pending or awaiting_approval promotion.
Response: 200 OK - Updated promotion with status: cancelled
Decision & Evidence Endpoints
Compatibility evidence endpoints are backed by durable release truth. Evidence list/detail/verify/export/raw/timeline routes read release_orchestrator.release_evidence for the calling tenant and return 404 when the packet is not stored there. Promotion and deployment checks use those durable packets plus release_orchestrator.gate_decisions; they fail closed instead of projecting passing decisions from demo rows.
Get Decision Record
Endpoint: GET /api/v1/promotions/{id}/decision
Returns the full decision record including gate evaluations.
Response: 200 OK
{
"promotionId": "uuid",
"decision": "allow",
"decidedAt": "2026-01-10T14:30:00Z",
"gates": [
{
"gateName": "security-gate",
"passed": true,
"details": {
"criticalCount": 0,
"highCount": 3,
"maxCritical": 0,
"maxHigh": 5
}
},
{
"gateName": "freeze-window-gate",
"passed": true,
"details": {
"activeFreezeWindow": null
}
}
],
"approvals": [
{
"approverId": "uuid",
"approverName": "John Doe",
"decision": "approved",
"comment": "LGTM",
"approvedAt": "2026-01-10T14:28:00Z"
}
]
}
Get Approvals
Endpoint: GET /api/v1/promotions/{id}/approvals
Response: 200 OK - Array of approval records
Get Evidence Packet
Endpoint: GET /api/v1/promotions/{id}/evidence
Returns the signed evidence packet for the promotion decision.
Response: 200 OK
{
"id": "uuid",
"type": "release_decision",
"version": "1.0",
"content": { ... },
"contentHash": "sha256:abc...",
"signature": "base64-signature",
"signatureAlgorithm": "ECDSA-P256-SHA256",
"signerKeyRef": "key-id",
"generatedAt": "2026-01-10T14:30:00Z"
}
Gate Preview Endpoints
Preview Gate Evaluation
Endpoint: POST /api/v1/promotions/preview-gates
Evaluates gates without creating a promotion (dry run).
Request:
{
"releaseId": "uuid",
"targetEnvironmentId": "uuid"
}
Response: 200 OK
{
"wouldPass": false,
"gates": [
{
"gateName": "security-gate",
"passed": false,
"blocking": true,
"message": "3 critical vulnerabilities exceed threshold (max: 0)"
},
{
"gateName": "freeze-window-gate",
"passed": true,
"blocking": false,
"message": "No active freeze window"
}
]
}
Approval Policy Endpoints
Approval policies are currently implemented by the Release Orchestrator WebApi under /api/v1/release-orchestrator/approval-policies. They are tenant-scoped, stored in release_orchestrator.approval_policies, and drive promotion gate decisions before deployment. GET requires release.policy.read; create, update, and delete require release.policy.manage. Delete soft-disables the policy (enabled=false) so historical decisions remain replayable.
Create Approval Policy
Endpoint: POST /api/v1/release-orchestrator/approval-policies
Request:
{
"policyId": "SP-001",
"name": "production-policy",
"description": "Production changes require two approvers.",
"sourceEnvironment": "staging",
"targetEnvironment": "production",
"releaseNameGlob": "payments-*",
"mode": "require-manual",
"requiredApprovals": 2,
"reason": "Production promotion requires two human approvals.",
"enabled": true,
"priority": 100
}
Modes: auto-approve, require-manual, deny.
List Approval Policies
Endpoint: GET /api/v1/release-orchestrator/approval-policies
Query Parameters:
sourceEnvironment(string): include policies matching the source or with no source predicatetargetEnvironment(string): include policies matching the target or with no target predicatereleaseName(string): client-side glob match againstreleaseNameGlobname(string): substring match against name or policy idincludeDisabled(boolean): include soft-disabled policies
Get Approval Policy
Endpoint: GET /api/v1/release-orchestrator/approval-policies/{id}
Update Approval Policy
Endpoint: PUT /api/v1/release-orchestrator/approval-policies/{id}
Delete Approval Policy
Endpoint: DELETE /api/v1/release-orchestrator/approval-policies/{id}
Current User Endpoints
Get My Pending Approvals
Endpoint: GET /api/v1/my/pending-approvals
Returns promotions awaiting approval from the current user.
Response: 200 OK - Array of promotions
Error Responses
| Status Code | Description |
|---|---|
400 | Invalid promotion request |
403 | User cannot approve (SoD violation or not in approver list) |
404 | Promotion not found |
409 | Promotion already decided |
422 | Gate evaluation failed |
For transitional /api/v1/release-orchestrator/releases/{id}/promote and /deploy, 409 Conflict is also returned with { blocked: true, reason, message } when the action cannot be proven safe from durable truth: missing approval quorum, missing gate-decision details, non-passing gates, missing verified evidence, missing verified policy-decision evidence for promotion, or evidence hash mismatch.
