Files
git.stella-ops.org/docs/modules/release-orchestrator/modules/release-manager.md

12 KiB

RELMAN: Release Management

Purpose: Manage components, versions, and release bundles.

Modules

Module: component-registry

Aspect Specification
Responsibility Map image repositories to logical components
Dependencies integration-manager (for registry access)
Data Entities Component, ComponentVersion
Events Produced component.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

Aspect Specification
Responsibility Tag/digest mapping; version rules
Dependencies component-registry, connector-runtime
Data Entities VersionMap, VersionRule, Channel
Events Produced version.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

Aspect Specification
Responsibility Release bundle lifecycle; composition
Dependencies component-registry, version-manager
Data Entities Release, ReleaseComponent
Events Produced release.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:

Mode Description
Full Release All components, latest versions
Partial Release Subset of components updated; others pinned from last deployment
Pinned Release All versions explicitly specified
Channel Release All components from specific channel ("beta")

Module: release-catalog

Aspect Specification
Responsibility Release history, search, comparison
Dependencies release-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:

  • Tags are convenience inputs for resolution
  • Tags are resolved to digests at release creation time
  • All downstream operations (promotion, deployment, rollback) use digests
  • Digest mismatch at pull time = deployment failure (tamper detection)

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