12 KiB
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:
- Fetch tags from registry (via connector)
- Apply version rules to extract semver
- Resolve each tag to digest
- Store in version map
- 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"
}
]
}