Release Management APIs

API endpoints for managing components, versions, and release bundles.

Status: Implemented for the component registry/version-map surface under /api/v1/components and the release-truth compatibility surface under /api/v1/release-orchestrator/releases. Release truth remains the current mutation authority for release rows, release components, events, approvals, gate decisions, and deployment handoff. Source: Architecture Advisory Section 6.3.3 Related Modules: Release Manager Module, Integration Hub

Overview

The Release Management API provides endpoints for managing container components, version tracking, and release bundle creation. All releases are identified by immutable OCI digests, ensuring cryptographic verification throughout the deployment pipeline.

Design Principle: Release identity is established via digest, not tag. Tags are human-friendly aliases; digests are the source of truth.

The implemented Release Orchestrator release-truth API enforces that a non-draft release is immutable. Updating mutable component configuration on a non-draft release remains allowed, but changing component artifact identity fields such as imageRef, digest, tag, version, or type returns a version-creation suggestion instead of mutating the source release.


Implemented Release-Truth Artifact Update Flow

Detect Changed Artifact on Existing Release

Endpoint: PATCH /api/v1/release-orchestrator/releases/{releaseId}/components/{componentId}

Use this endpoint for component edits. When the release is still draft, artifact fields can be updated in place. When the release is no longer draft and the patch changes artifact identity, the API returns 409 Conflict with a successor-release suggestion.

Request:

{
  "imageRef": "registry.local/stella/checkout-api",
  "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "tag": "1.2.4",
  "version": "1.2.4",
  "type": "container"
}

Conflict response:

{
  "error": "release_artifact_update_requires_new_version",
  "message": "This release is no longer mutable. Create a new release version from the previous release and replace only the changed artifact digest.",
  "suggestion": {
    "sourceReleaseId": "rel-checkout",
    "sourceReleaseName": "Checkout API",
    "sourceVersion": "1.2.3",
    "suggestedReleaseName": "Checkout API",
    "suggestedVersion": "1.2.4",
    "targetEnvironment": "env-dev",
    "componentId": "comp-api",
    "componentName": "checkout-api",
    "currentImageRef": "registry.local/stella/checkout-api",
    "candidateImageRef": "registry.local/stella/checkout-api",
    "currentDigest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "candidateDigest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "actionEndpoint": "/api/v1/release-orchestrator/releases/rel-checkout/versions/from-artifact-update",
    "copiedComponents": [
      {
        "componentId": "comp-api",
        "name": "checkout-api",
        "imageRef": "registry.local/stella/checkout-api",
        "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
        "tag": "1.2.3",
        "version": "1.2.3",
        "type": "container",
        "changed": true
      }
    ]
  }
}

Create Successor Release from Changed Artifact

Endpoint: POST /api/v1/release-orchestrator/releases/{releaseId}/versions/from-artifact-update

Creates a new draft release by copying the source release metadata and all source components, replacing only the selected component artifact fields. This is the canonical path when Stella detects that a release artifact digest has been updated after the source release became immutable.

Request:

{
  "componentId": "comp-api",
  "imageRef": "registry.local/stella/checkout-api",
  "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "tag": "1.2.4",
  "version": "1.2.4",
  "releaseVersion": "1.2.4",
  "targetEnvironment": "env-dev"
}

Response: 201 Created

{
  "release": {
    "id": "rel-abc123def",
    "name": "Checkout API",
    "version": "1.2.4",
    "status": "draft",
    "targetEnvironment": "env-dev",
    "componentCount": 2
  },
  "components": [
    {
      "id": "comp-api",
      "name": "checkout-api",
      "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    },
    {
      "id": "comp-worker",
      "name": "worker",
      "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
    }
  ],
  "suggestion": {
    "sourceReleaseId": "rel-checkout",
    "suggestedVersion": "1.2.4",
    "componentId": "comp-api",
    "candidateDigest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
  }
}

The endpoint emits a release event with type artifact_update_version_created and event metadata for the source release, source version, changed component, current digest, and candidate digest.

Developer principals are also checked against the developer self-service ABAC contract before this endpoint creates a release version. A developer must be allowed for the service/image, target environment, and immutable digest. Operator and admin principals continue to use normal release write authorization.

Approval Replay Status

Endpoint: GET /api/v1/approvals/{approvalId}/replay

Returns the deterministic replay status for the v2 approval detail surface. The endpoint is intentionally separate from the decision and evidence detail calls: the UI uses it to prove that the stored approval decision digest is still replayable without mutating the approval.

Response: 200 OK

{
  "approvalId": "apr-dev-001",
  "verdict": "not_available",
  "decisionDigest": null,
  "replayLogReference": "replay://approvals/apr-dev-001/decision",
  "evidencePacket": {
    "decisionDigest": null,
    "policyDecisionDsse": null
  }
}

verdict is verified only when the approval has a real signed decision digest available in the approval row or attached evidence packet. It is not_available when no operator-decision envelope has been persisted yet. The API must not synthesize sha256:decision-* or policy-decision-*dsse placeholders for unsigned approvals. Missing approval ids return 404 Not Found; callers must treat that as a broken approval reference rather than a replay mismatch.

Administrator Environment Limits

An administrator can constrain release creation and artifact-derived version creation to named environments with a tenant-stored release environment limit. Stored settings are the normal operator control. Release Orchestrator configuration is still supported as bootstrap/fallback for fresh installs and air-gapped deployment overlays before a tenant setting exists.

Read effective limit

GET /api/v1/release-orchestrator/release-environment-limit

{
  "requireTargetEnvironment": true,
  "allowedEnvironments": ["env-dev", "env-stage"],
  "source": "stored",
  "persisted": true,
  "createdAt": "2026-05-24T16:20:00Z",
  "updatedAt": "2026-05-24T16:20:00Z",
  "createdBy": "admin",
  "updatedBy": "admin"
}

Update tenant limit

PUT /api/v1/release-orchestrator/release-environment-limit

{
  "requireTargetEnvironment": true,
  "allowedEnvironments": ["env-dev", "env-stage"]
}

The update endpoint normalizes environment names case-insensitively and rejects requireTargetEnvironment=true with an empty allow-list. Both the read and update endpoints require the ui.admin scope and an administrator role (admin, role/console-admin, or console-admin). Developer and operator principals cannot manage the tenant release environment limit even if a broad local-dev token includes UI administration scopes.

If no stored setting exists, the effective limit is read from configuration:

{
  "ReleaseOrchestrator": {
    "ReleaseEnvironmentLimit": {
      "RequireTargetEnvironment": true,
      "AllowedEnvironments": ["env-dev", "env-stage"]
    }
  }
}

Equivalent flat keys are also supported:

When the limit is active, release creation, release update, clone, and artifact successor creation reject a missing target environment with release_target_environment_required and reject an environment outside the allow-list with release_environment_not_allowed.


Component Endpoints

Create Component

Endpoint: POST /api/v1/components

Registers a new container component for release management.

Request:

{
  "name": "api",
  "displayName": "API Service",
  "imageRepository": "myorg/api",
  "registryIntegrationId": "registry-primary",
  "versioningStrategy": "branch-tag",
  "defaultChannel": "main",
  "defaultVersionLabel": "operator-default",
  "environmentRegistryOverrides": {
    "prod": {
      "registryIntegrationId": "registry-prod",
      "imageRepository": "mirror/api"
    }
  }
}

Response: 201 Created

{
  "id": "uuid",
  "name": "api",
  "displayName": "API Service",
  "imageRepository": "myorg/api",
  "registryIntegrationId": "registry-primary",
  "versioningStrategy": "branch-tag",
  "status": "active",
  "createdAt": "2026-01-10T14:23:45Z"
}

versioningStrategy supports:

Per-environment registry overrides affect sync and release authoring for that environment; historical version rows still keep their pinned digest.

List Components

Endpoint: GET /api/v1/components

Response: 200 OK - Array of components

Get Component

Endpoint: GET /api/v1/components/{id}

Update Component

Endpoint: PATCH /api/v1/components/{id}

Retire Component

Endpoint: DELETE /api/v1/components/{id}

Sync Versions

Endpoint: POST /api/v1/components/{id}/sync-versions

Triggers a refresh of available versions from the container registry.

Request:

{
  "environment": "prod",
  "channel": "main",
  "forceRefresh": true
}

Response: 200 OK

{
  "synced": 2,
  "created": 2,
  "updated": 0,
  "versions": [
    {
      "id": "uuid",
      "componentId": "uuid",
      "tag": "main",
      "digest": "sha256:abc123...",
      "versionLabel": "main-abcdef123456",
      "channel": "main",
      "sourceCommitSha": "abcdef1234567890",
      "sourceRepoUrl": "https://gitlab.example.com/internal-tenant/app/api",
      "sourceBranch": "main",
      "scmManuallySet": false,
      "discoveredAt": "2026-05-25T13:00:00Z",
      "builtAt": "2026-05-25T12:58:00Z"
    }
  ]
}

sync-versions consumes the Platform registry digest contract (GET /api/v1/registries/images/digests) through the ReleaseOrchestrator PlatformRegistry HTTP client. Platform digest entries include registry label metadata when the upstream registry exposes artifact labels, annotations, or image config labels. For OCI Distribution v2 registries, Release Orchestrator also reads the image config blob referenced by the manifest and merges config.Labels over manifest annotations. sync-versions copies those labels into version metadata and uses CI_COMMIT_SHA, CI_PROJECT_URL, CI_COMMIT_REF_NAME, CI_COMMIT_TIMESTAMP, stella.version, and related OCI labels to populate sourceCommitSha, sourceRepoUrl, sourceBranch, builtAt, and derived version labels. Tests use deterministic offline fixtures for tag -> digest and OCI label coverage. Registries with mutable tags are safe: each distinct (componentId, tag, digest) becomes a separate immutable version row.

After a digest-pinned version row is upserted, Release Orchestrator probes Scanner’s GET /api/v1/sbom-readiness endpoint for the same tenant and digest. If Scanner returns absent, it submits one POST /api/v1/scans request with the resolved registry image reference and component/version metadata. Existing pending, scanning, ready, or failed states are not re-enqueued; the promotion gate uses those states to render either a waiting SBOM gate, a passing gate, or an explicit Scanner failure.

Connected Deployment-Decision Referrers

Promotion gate decisions emit one signed deployment-decision OCI referrer per valid component digest. In connected deployments, set Scanner:BaseUri and Deployment:Registry:MetaGatewayBaseUri to Scanner’s HTTP surface so Release Orchestrator writes to POST /api/v1/oci-referrers and reads back through GET /v2/{repo}/referrers/{digest}. The Scanner calls carry X-StellaOps-TenantId and, when service identity is enabled, a stellaops-release-dispatch bearer token with scanner:read scanner:scan scanner:write. Those scopes are required for off-bypass deployments where Scanner cannot rely on trusted service networks.

List Component Versions

Endpoint: GET /api/v1/components/{id}/versions

Query Parameters:

Response: 200 OK - Array of version maps


Version Map Endpoints

Version maps are exposed through GET /api/v1/components/{id}/versions and populated by POST /api/v1/components/{id}/sync-versions. Each row records tag, immutable digest, operator-facing versionLabel, channel, sourceCommitSha, sourceRepoUrl, sourceBranch, scmManuallySet, discoveredAt, optional builtAt, and metadata copied from registry/OCI labels.

Update Component Version SCM

Endpoint: PATCH /api/v1/components/{componentId}/versions/{versionId}

Adds or corrects SCM provenance on a digest-pinned component version. This is used when the image exposes a commit label but not a repository URL, or when the operator resolves the repository from a CI pipeline trace. Manual values set scmManuallySet=true and survive future registry re-syncs. When a version first has both sourceRepoUrl and sourceCommitSha, the orchestrator emits ComponentVersionScmInfoAvailable for downstream call-path analysis.

Request:

{
  "sourceRepoUrl": "https://gitlab.example.com/internal-tenant/app/api",
  "sourceCommitSha": "abcdef1234567890",
  "sourceBranch": "main"
}

Response: 200 OK - Updated component-version row.


Release Endpoints

Create Release

Endpoint: POST /api/v1/release-orchestrator/releases

Creates a new release bundle with specified component versions.

Request:

{
  "name": "myapp-v2.3.1",
  "version": "2.3.1",
  "targetEnvironment": "staging",
  "components": [
    { "componentId": "uuid", "componentVersionId": "uuid" },
    { "componentId": "uuid", "versionLabel": "1.2.3", "channel": "stable" },
    { "componentId": "uuid", "version": "from-latest", "channel": "main" }
  ]
}

Release creation and POST /api/v1/release-orchestrator/releases/{releaseId}/components both accept component-version selections. If digest is supplied, the legacy explicit digest path is preserved. If componentVersionId, versionLabel, or from-latest is supplied, Release Orchestrator resolves the recorded version row and stores the immutable digest on the release component. The version label remains operator metadata; the digest is the release identity.

Response: 201 Created

{
  "id": "uuid",
  "name": "myapp-v2.3.1",
  "version": "2.3.1",
  "status": "draft",
  "componentCount": 3,
  "createdAt": "2026-01-10T14:23:45Z",
  "createdBy": "user-uuid"
}

Create Release from Latest

Endpoint: POST /api/v1/releases/from-latest

Convenience endpoint to create a release from the latest versions of all (or specified) components.

Request:

{
  "name": "myapp-latest",
  "channel": "stable",
  "componentIds": ["uuid1", "uuid2"],
  "pinFrom": {
    "environmentId": "uuid"
  }
}

Response: 201 Created - Release with resolved digests

List Releases

Endpoint: GET /api/v1/releases

Query Parameters:

Response: 200 OK

{
  "data": [
    {
      "id": "uuid",
      "name": "myapp-v2.3.1",
      "status": "deployed",
      "componentCount": 3,
      "createdAt": "2026-01-10T14:23:45Z"
    }
  ],
  "meta": {
    "page": 1,
    "pageSize": 20,
    "totalCount": 150,
    "totalPages": 8
  }
}

Get Release

Endpoint: GET /api/v1/releases/{id}

Response: 200 OK - Full release with component details

Update Release

Endpoint: PUT /api/v1/releases/{id}

Request:

{
  "displayName": "Updated Display Name",
  "metadata": { "key": "value" },
  "status": "ready"
}

Delete Release

Endpoint: DELETE /api/v1/releases/{id}

Get Release State

Endpoint: GET /api/v1/releases/{id}/state

Returns the deployment state of a release across environments.

Response: 200 OK

{
  "environments": [
    {
      "environmentId": "uuid",
      "environmentName": "Development",
      "status": "deployed",
      "deployedAt": "2026-01-09T10:00:00Z"
    },
    {
      "environmentId": "uuid",
      "environmentName": "Staging",
      "status": "deployed",
      "deployedAt": "2026-01-10T08:00:00Z"
    },
    {
      "environmentId": "uuid",
      "environmentName": "Production",
      "status": "not_deployed"
    }
  ]
}

Deprecate Release

Endpoint: POST /api/v1/releases/{id}/deprecate

Marks a release as deprecated, preventing new promotions.

Response: 200 OK - Updated release with status: deprecated

Compare Releases

Endpoint: GET /api/v1/releases/{id}/compare/{otherId}

Compares two releases to identify component differences.

Response: 200 OK

{
  "added": [
    { "componentId": "uuid", "componentName": "worker" }
  ],
  "removed": [
    { "componentId": "uuid", "componentName": "legacy-service" }
  ],
  "changed": [
    {
      "component": "api",
      "fromVersion": "2.3.0",
      "toVersion": "2.3.1",
      "fromDigest": "sha256:old...",
      "toDigest": "sha256:new..."
    }
  ]
}

Error Responses

Status CodeDescription
400Invalid release configuration
404Release or component not found
409Release name already exists
422Cannot resolve component version

See Also