release orchestrator pivot, architecture and planning
This commit is contained in:
406
docs/modules/release-orchestrator/modules/release-manager.md
Normal file
406
docs/modules/release-orchestrator/modules/release-manager.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# 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**:
|
||||
```typescript
|
||||
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**:
|
||||
```typescript
|
||||
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**:
|
||||
```typescript
|
||||
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**:
|
||||
```typescript
|
||||
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
|
||||
|
||||
```sql
|
||||
-- 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
|
||||
|
||||
```yaml
|
||||
# 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**:
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
- [Module Overview](overview.md)
|
||||
- [Design Principles](../design/principles.md)
|
||||
- [API Documentation](../api/releases.md)
|
||||
- [Promotion Manager](promotion-manager.md)
|
||||
Reference in New Issue
Block a user