RELMAN: Release Management

Purpose: Manage components, versions, and release bundles.

Modules

Module: component-registry

AspectSpecification
ResponsibilityMap image repositories to logical components
Dependenciesintegration-manager (for registry access)
Data EntitiesComponent, ComponentVersion
Events Producedcomponent.created, component.updated, component.deleted

Key Operations:

CreateComponent(name, displayName, imageRepository, registryId) → Component
UpdateComponent(id, config) → Component
DeleteComponent(id) → void
SyncVersions(componentId, forceRefresh) → VersionMap[]
ListComponents(tenantId) → Component[]

Component Entity:

interface Component {
  id: UUID;
  tenantId: UUID;
  name: string;                        // "api", "worker", "frontend"
  displayName: string;                 // "API Service"
  imageRepository: string;             // "registry.example.com/myapp/api"
  registryIntegrationId: UUID;         // which registry integration
  versioningStrategy: VersionStrategy;
  deploymentTemplate: string;          // which workflow template to use
  defaultChannel: string;              // "stable", "beta"
  metadata: Record<string, string>;
}

interface VersionStrategy {
  type: "semver" | "date" | "sequential" | "manual";
  tagPattern?: string;                 // regex for tag extraction
  semverExtract?: string;              // regex capture group
}

Module: version-manager

AspectSpecification
ResponsibilityTag/digest mapping; version rules
Dependenciescomponent-registry, connector-runtime
Data EntitiesVersionMap, VersionRule, Channel
Events Producedversion.resolved, version.updated

Version Resolution:

interface VersionMap {
  id: UUID;
  componentId: UUID;
  tag: string;              // "v2.3.1"
  digest: string;           // "sha256:abc123..."
  semver: string;           // "2.3.1"
  channel: string;          // "stable"
  prerelease: boolean;
  buildMetadata: string;
  resolvedAt: DateTime;
  source: "auto" | "manual";
}

interface VersionRule {
  id: UUID;
  componentId: UUID;
  pattern: string;          // "^v(\\d+\\.\\d+\\.\\d+)$"
  channel: string;          // "stable"
  prereleasePattern: string;// ".*-(alpha|beta|rc).*"
}

Version Resolution Algorithm:

  1. Fetch tags from registry (via connector)
  2. Apply version rules to extract semver
  3. Resolve each tag to digest
  4. Store in version map
  5. Update channels (“latest stable”, “latest beta”)

Module: release-manager

AspectSpecification
ResponsibilityRelease bundle lifecycle; composition
Dependenciescomponent-registry, version-manager
Data EntitiesRelease, ReleaseComponent
Events Producedrelease.created, release.promoted, release.deprecated

Release Entity:

interface Release {
  id: UUID;
  tenantId: UUID;
  name: string;                        // "myapp-v2.3.1"
  displayName: string;                 // "MyApp 2.3.1"
  components: ReleaseComponent[];
  sourceRef: SourceReference;
  status: ReleaseStatus;
  createdAt: DateTime;
  createdBy: UUID;
  deployedEnvironments: UUID[];        // where currently deployed
  metadata: Record<string, string>;
}

interface ReleaseComponent {
  componentId: UUID;
  componentName: string;
  digest: string;                      // sha256:...
  semver: string;                      // resolved semver
  tag: string;                         // original tag (for display)
  role: "primary" | "sidecar" | "init" | "migration";
}

interface SourceReference {
  scmIntegrationId?: UUID;
  commitSha?: string;
  branch?: string;
  ciIntegrationId?: UUID;
  buildId?: string;
  pipelineUrl?: string;
}

type ReleaseStatus =
  | "draft"           // being composed
  | "ready"           // ready for promotion
  | "promoting"       // promotion in progress
  | "deployed"        // deployed to at least one env
  | "deprecated"      // marked as deprecated
  | "archived";       // no longer active

Release Creation Modes:

ModeDescription
Full ReleaseAll components, latest versions
Partial ReleaseSubset of components updated; others pinned from last deployment
Pinned ReleaseAll versions explicitly specified
Channel ReleaseAll components from specific channel (“beta”)

Artifact update successor rule:

Once a release leaves draft, its artifact identity is immutable. If Stella detects that a component artifact changed for a non-draft release, the Release Manager must not patch the existing release in place. It returns a structured successor suggestion that names the source release, changed component, current digest, candidate digest, suggested next version, target environment, and action endpoint. The caller then creates a new draft version with POST /api/v1/release-orchestrator/releases/{releaseId}/versions/from-artifact-update.

The successor release copies all source components and metadata, replaces only the selected component’s artifact fields, resets runtime state to draft, and records an artifact_update_version_created event. This is the expected path for developer-owned service image updates, CI/registry drift detection, and any operator workflow that discovers a new digest after a release has already been prepared or deployed.

Administrator environment limits are evaluated before the successor release is created. Tenant-stored settings from /api/v1/release-orchestrator/release-environment-limit override deployment configuration. When the effective limit requires a target environment, a new, updated, cloned, or artifact-derived release must include a target environment and the target must be in the allow-list. The Platform Setup Defaults & Guardrails page is the administrator UI for this control; deployment configuration remains the bootstrap fallback when no tenant setting exists.


Module: release-catalog

AspectSpecification
ResponsibilityRelease history, search, comparison
Dependenciesrelease-manager

Key Operations:

SearchReleases(filter, pagination) → Release[]
CompareReleases(releaseA, releaseB) → ReleaseDiff
GetReleaseHistory(componentId) → Release[]
GetReleaseLineage(releaseId) → ReleaseLineage  // promotion path

Release Comparison:

interface ReleaseDiff {
  releaseA: UUID;
  releaseB: UUID;
  added: ComponentDiff[];      // Components in B not in A
  removed: ComponentDiff[];    // Components in A not in B
  changed: ComponentChange[];  // Components with different versions
  unchanged: ComponentDiff[];  // Components with same version
}

interface ComponentChange {
  componentId: UUID;
  componentName: string;
  fromVersion: string;
  toVersion: string;
  fromDigest: string;
  toDigest: string;
}

Database Schema

-- Components
CREATE TABLE release.components (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name VARCHAR(255) NOT NULL,
    display_name VARCHAR(255) NOT NULL,
    image_repository VARCHAR(500) NOT NULL,
    registry_integration_id UUID REFERENCES release.integrations(id),
    versioning_strategy JSONB NOT NULL DEFAULT '{"type": "semver"}',
    deployment_template VARCHAR(255),
    default_channel VARCHAR(50) NOT NULL DEFAULT 'stable',
    metadata JSONB NOT NULL DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (tenant_id, name)
);

CREATE INDEX idx_components_tenant ON release.components(tenant_id);

-- Version Maps
CREATE TABLE release.version_maps (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    component_id UUID NOT NULL REFERENCES release.components(id) ON DELETE CASCADE,
    tag VARCHAR(255) NOT NULL,
    digest VARCHAR(100) NOT NULL,
    semver VARCHAR(50),
    channel VARCHAR(50) NOT NULL DEFAULT 'stable',
    prerelease BOOLEAN NOT NULL DEFAULT FALSE,
    build_metadata VARCHAR(255),
    resolved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    source VARCHAR(50) NOT NULL DEFAULT 'auto',
    UNIQUE (tenant_id, component_id, digest)
);

CREATE INDEX idx_version_maps_component ON release.version_maps(component_id);
CREATE INDEX idx_version_maps_digest ON release.version_maps(digest);
CREATE INDEX idx_version_maps_semver ON release.version_maps(semver);

-- Releases
CREATE TABLE release.releases (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name VARCHAR(255) NOT NULL,
    display_name VARCHAR(255) NOT NULL,
    components JSONB NOT NULL,  -- [{componentId, digest, semver, tag, role}]
    source_ref JSONB,           -- {scmIntegrationId, commitSha, ciIntegrationId, buildId}
    status VARCHAR(50) NOT NULL DEFAULT 'draft',
    metadata JSONB NOT NULL DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID REFERENCES users(id),
    UNIQUE (tenant_id, name)
);

CREATE INDEX idx_releases_tenant ON release.releases(tenant_id);
CREATE INDEX idx_releases_status ON release.releases(status);
CREATE INDEX idx_releases_created ON release.releases(created_at DESC);

-- Release Environment State
CREATE TABLE release.release_environment_state (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    environment_id UUID NOT NULL REFERENCES release.environments(id) ON DELETE CASCADE,
    release_id UUID NOT NULL REFERENCES release.releases(id),
    status VARCHAR(50) NOT NULL,
    deployed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deployed_by UUID REFERENCES users(id),
    promotion_id UUID,
    evidence_ref VARCHAR(255),
    UNIQUE (tenant_id, environment_id)
);

CREATE INDEX idx_release_env_state_env ON release.release_environment_state(environment_id);
CREATE INDEX idx_release_env_state_release ON release.release_environment_state(release_id);

API Endpoints

# Components
POST   /api/v1/components
       Body: { name, displayName, imageRepository, registryIntegrationId, versioningStrategy?, defaultChannel? }
       Response: Component

GET    /api/v1/components
       Response: Component[]

GET    /api/v1/components/{id}
       Response: Component

PUT    /api/v1/components/{id}
       Response: Component

DELETE /api/v1/components/{id}
       Response: { deleted: true }

POST   /api/v1/components/{id}/sync-versions
       Body: { forceRefresh?: boolean }
       Response: { synced: number, versions: VersionMap[] }

GET    /api/v1/components/{id}/versions
       Query: ?channel={stable|beta}&limit={n}
       Response: VersionMap[]

# Version Maps
POST   /api/v1/version-maps
       Body: { componentId, tag, semver, channel }  # manual version assignment
       Response: VersionMap

GET    /api/v1/version-maps
       Query: ?componentId={uuid}&channel={channel}
       Response: VersionMap[]

# Releases
POST   /api/v1/releases
       Body: {
         name: string,
         displayName?: string,
         components: [
           { componentId: UUID, version?: string, digest?: string, channel?: string }
         ],
         sourceRef?: SourceReference
       }
       Response: Release

GET    /api/v1/releases
       Query: ?status={status}&componentId={uuid}&page={n}&pageSize={n}
       Response: { data: Release[], meta: PaginationMeta }

GET    /api/v1/releases/{id}
       Response: Release (with full component details)

PUT    /api/v1/releases/{id}
       Body: { displayName?, metadata?, status? }
       Response: Release

DELETE /api/v1/releases/{id}
       Response: { deleted: true }

GET    /api/v1/releases/{id}/state
       Response: { environments: [{ environmentId, status, deployedAt }] }

POST   /api/v1/releases/{id}/deprecate
       Response: Release

GET    /api/v1/releases/{id}/compare/{otherId}
       Response: ReleaseDiff

# Quick release creation
POST   /api/v1/releases/from-latest
       Body: {
         name: string,
         channel?: string,           # default: stable
         componentIds?: UUID[],      # default: all
         pinFrom?: { environmentId: UUID }  # for partial release
       }
       Response: Release

Release Identity: Digest-First Principle

A core design invariant of the Release Orchestrator:

INVARIANT: A release is a set of OCI image digests (component -> digest mapping), never tags.

Implementation Requirements:

Example:

{
  "id": "release-uuid",
  "name": "myapp-v2.3.1",
  "components": [
    {
      "componentId": "api-component-uuid",
      "componentName": "api",
      "tag": "v2.3.1",
      "digest": "sha256:abc123def456...",
      "semver": "2.3.1",
      "role": "primary"
    },
    {
      "componentId": "worker-component-uuid",
      "componentName": "worker",
      "tag": "v2.3.1",
      "digest": "sha256:789xyz123abc...",
      "semver": "2.3.1",
      "role": "primary"
    }
  ]
}

References