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}/promote and /deploy are implemented as transitional release-control endpoints. Release and approval compatibility records are stored in release_orchestrator release-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/policy requiredApprovals value 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:

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:


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:

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:

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:

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 CodeDescription
400Invalid promotion request
403User cannot approve (SoD violation or not in approver list)
404Promotion not found
409Promotion already decided
422Gate 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.


See Also