release orchestrator pivot, architecture and planning
This commit is contained in:
433
docs/modules/release-orchestrator/modules/promotion-manager.md
Normal file
433
docs/modules/release-orchestrator/modules/promotion-manager.md
Normal file
@@ -0,0 +1,433 @@
|
||||
# PROMOT: Promotion & Approval Manager
|
||||
|
||||
**Purpose**: Manage promotion requests, approvals, gates, and decision records.
|
||||
|
||||
## Modules
|
||||
|
||||
### Module: `promotion-manager`
|
||||
|
||||
| Aspect | Specification |
|
||||
|--------|---------------|
|
||||
| **Responsibility** | Promotion request lifecycle; state management |
|
||||
| **Dependencies** | `release-manager`, `environment-manager`, `workflow-engine` |
|
||||
| **Data Entities** | `Promotion`, `PromotionState` |
|
||||
| **Events Produced** | `promotion.requested`, `promotion.approved`, `promotion.rejected`, `promotion.started`, `promotion.completed`, `promotion.failed`, `promotion.rolled_back` |
|
||||
|
||||
**Key Operations**:
|
||||
```
|
||||
RequestPromotion(releaseId, targetEnvironmentId, reason) → Promotion
|
||||
ApprovePromotion(promotionId, comment) → Promotion
|
||||
RejectPromotion(promotionId, reason) → Promotion
|
||||
CancelPromotion(promotionId) → Promotion
|
||||
GetPromotionStatus(promotionId) → PromotionState
|
||||
GetDecisionRecord(promotionId) → DecisionRecord
|
||||
```
|
||||
|
||||
**Promotion Entity**:
|
||||
```typescript
|
||||
interface Promotion {
|
||||
id: UUID;
|
||||
tenantId: UUID;
|
||||
releaseId: UUID;
|
||||
sourceEnvironmentId: UUID | null; // null for first deployment
|
||||
targetEnvironmentId: UUID;
|
||||
status: PromotionStatus;
|
||||
decisionRecord: DecisionRecord;
|
||||
workflowRunId: UUID | null;
|
||||
requestedAt: DateTime;
|
||||
requestedBy: UUID;
|
||||
requestReason: string;
|
||||
decidedAt: DateTime | null;
|
||||
startedAt: DateTime | null;
|
||||
completedAt: DateTime | null;
|
||||
evidencePacketId: UUID | null;
|
||||
}
|
||||
|
||||
type PromotionStatus =
|
||||
| "pending_approval" // Waiting for human approval
|
||||
| "pending_gate" // Waiting for gate evaluation
|
||||
| "approved" // Ready for deployment
|
||||
| "rejected" // Blocked by approval or gate
|
||||
| "deploying" // Deployment in progress
|
||||
| "deployed" // Successfully deployed
|
||||
| "failed" // Deployment failed
|
||||
| "cancelled" // User cancelled
|
||||
| "rolled_back"; // Rolled back after failure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Module: `approval-gateway`
|
||||
|
||||
| Aspect | Specification |
|
||||
|--------|---------------|
|
||||
| **Responsibility** | Approval collection; separation of duties enforcement |
|
||||
| **Dependencies** | `authority` (for user/group lookup) |
|
||||
| **Data Entities** | `Approval`, `ApprovalPolicy` |
|
||||
| **Events Produced** | `approval.granted`, `approval.denied` |
|
||||
|
||||
**Approval Policy Entity**:
|
||||
```typescript
|
||||
interface ApprovalPolicy {
|
||||
id: UUID;
|
||||
tenantId: UUID;
|
||||
environmentId: UUID;
|
||||
requiredCount: number; // Minimum approvals required
|
||||
requiredRoles: string[]; // At least one approver must have role
|
||||
requiredGroups: string[]; // At least one approver must be in group
|
||||
requireSeparationOfDuties: boolean; // Requester cannot approve
|
||||
allowSelfApproval: boolean; // Override SoD for specific users
|
||||
expirationMinutes: number; // Approval expires after N minutes
|
||||
}
|
||||
|
||||
interface Approval {
|
||||
id: UUID;
|
||||
tenantId: UUID;
|
||||
promotionId: UUID;
|
||||
approverId: UUID;
|
||||
action: "approved" | "rejected";
|
||||
comment: string;
|
||||
approvedAt: DateTime;
|
||||
approverRole: string;
|
||||
approverGroups: string[];
|
||||
}
|
||||
```
|
||||
|
||||
**Separation of Duties (SoD) Rules**:
|
||||
1. Requester cannot approve their own promotion (if `requireSeparationOfDuties` is true)
|
||||
2. Same user cannot approve twice
|
||||
3. At least N different users must approve (based on `requiredCount`)
|
||||
4. At least one approver must match `requiredRoles` if specified
|
||||
5. At least one approver must be in `requiredGroups` if specified
|
||||
|
||||
---
|
||||
|
||||
### Module: `decision-engine`
|
||||
|
||||
| Aspect | Specification |
|
||||
|--------|---------------|
|
||||
| **Responsibility** | Gate evaluation; policy integration; decision record generation |
|
||||
| **Dependencies** | `gate-registry`, `policy` (OPA integration), `scanner` (security data) |
|
||||
| **Data Entities** | `DecisionRecord`, `GateResult` |
|
||||
| **Events Produced** | `decision.evaluated`, `decision.recorded` |
|
||||
|
||||
**Decision Record Structure**:
|
||||
```typescript
|
||||
interface DecisionRecord {
|
||||
promotionId: UUID;
|
||||
evaluatedAt: DateTime;
|
||||
decision: "allow" | "deny" | "pending";
|
||||
|
||||
// What was evaluated
|
||||
release: {
|
||||
id: UUID;
|
||||
name: string;
|
||||
components: Array<{
|
||||
name: string;
|
||||
digest: string;
|
||||
semver: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
environment: {
|
||||
id: UUID;
|
||||
name: string;
|
||||
requiredApprovals: number;
|
||||
freezeWindow: boolean;
|
||||
};
|
||||
|
||||
// Gate evaluation results
|
||||
gates: GateResult[];
|
||||
|
||||
// Approval status
|
||||
approvalStatus: {
|
||||
required: number;
|
||||
received: number;
|
||||
approvers: Array<{
|
||||
userId: UUID;
|
||||
action: string;
|
||||
at: DateTime;
|
||||
}>;
|
||||
sodViolation: boolean;
|
||||
};
|
||||
|
||||
// Reason for decision
|
||||
reasons: string[];
|
||||
|
||||
// Hash of all inputs for replay verification
|
||||
inputsHash: string;
|
||||
}
|
||||
|
||||
interface GateResult {
|
||||
gateType: string;
|
||||
gateName: string;
|
||||
status: "passed" | "failed" | "warning" | "skipped";
|
||||
message: string;
|
||||
details: Record<string, any>;
|
||||
evaluatedAt: DateTime;
|
||||
durationMs: number;
|
||||
}
|
||||
```
|
||||
|
||||
**Gate Evaluation Order**:
|
||||
1. **Freeze Window Check**: Is environment in freeze?
|
||||
2. **Approval Check**: All required approvals received?
|
||||
3. **Security Gate**: No blocking vulnerabilities?
|
||||
4. **Custom Policy Gates**: All OPA policies pass?
|
||||
5. **Integration Gates**: External system checks pass?
|
||||
|
||||
---
|
||||
|
||||
### Module: `gate-registry`
|
||||
|
||||
| Aspect | Specification |
|
||||
|--------|---------------|
|
||||
| **Responsibility** | Built-in + custom gate registration |
|
||||
| **Dependencies** | `plugin-registry` |
|
||||
| **Data Entities** | `GateDefinition`, `GateConfig` |
|
||||
|
||||
**Built-in Gates**:
|
||||
|
||||
| Gate Type | Description |
|
||||
|-----------|-------------|
|
||||
| `freeze-window` | Check if environment is in freeze |
|
||||
| `approval` | Check if required approvals received |
|
||||
| `security-scan` | Check for blocking vulnerabilities |
|
||||
| `scan-freshness` | Check if scan is recent enough |
|
||||
| `digest-verification` | Verify digests haven't changed |
|
||||
| `environment-sequence` | Enforce promotion order |
|
||||
| `custom-opa` | Custom OPA/Rego policy |
|
||||
| `webhook` | External webhook gate |
|
||||
|
||||
**Gate Definition**:
|
||||
```typescript
|
||||
interface GateDefinition {
|
||||
type: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
configSchema: JSONSchema;
|
||||
evaluator: "builtin" | UUID; // builtin or plugin ID
|
||||
blocking: boolean; // Can block promotion
|
||||
cacheable: boolean; // Can cache result
|
||||
cacheTtlSeconds: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Promotion State Machine
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ PROMOTION STATE MACHINE │
|
||||
│ │
|
||||
│ ┌───────────────┐ │
|
||||
│ │ REQUESTED │ ◄──── User requests promotion │
|
||||
│ └───────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ PENDING │─────►│ REJECTED │ ◄──── Approver rejects │
|
||||
│ │ APPROVAL │ └───────────────┘ │
|
||||
│ └───────┬───────┘ │
|
||||
│ │ approval received │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ PENDING │─────►│ REJECTED │ ◄──── Gate fails │
|
||||
│ │ GATE │ └───────────────┘ │
|
||||
│ └───────┬───────┘ │
|
||||
│ │ all gates pass │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ │
|
||||
│ │ APPROVED │ ◄──── Ready for deployment │
|
||||
│ └───────┬───────┘ │
|
||||
│ │ workflow starts │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ DEPLOYING │─────►│ FAILED │─────►│ ROLLED_BACK │ │
|
||||
│ └───────┬───────┘ └───────────────┘ └───────────────┘ │
|
||||
│ │ │
|
||||
│ │ deployment complete │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ │
|
||||
│ │ DEPLOYED │ ◄──── Success! │
|
||||
│ └───────────────┘ │
|
||||
│ │
|
||||
│ Additional transitions: │
|
||||
│ - Any non-terminal → CANCELLED: user cancels │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
-- Promotions
|
||||
CREATE TABLE release.promotions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
release_id UUID NOT NULL REFERENCES release.releases(id),
|
||||
source_environment_id UUID REFERENCES release.environments(id),
|
||||
target_environment_id UUID NOT NULL REFERENCES release.environments(id),
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'pending_approval' CHECK (status IN (
|
||||
'pending_approval', 'pending_gate', 'approved', 'rejected',
|
||||
'deploying', 'deployed', 'failed', 'cancelled', 'rolled_back'
|
||||
)),
|
||||
decision_record JSONB,
|
||||
workflow_run_id UUID REFERENCES release.workflow_runs(id),
|
||||
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
requested_by UUID NOT NULL REFERENCES users(id),
|
||||
request_reason TEXT,
|
||||
decided_at TIMESTAMPTZ,
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
evidence_packet_id UUID
|
||||
);
|
||||
|
||||
CREATE INDEX idx_promotions_tenant ON release.promotions(tenant_id);
|
||||
CREATE INDEX idx_promotions_release ON release.promotions(release_id);
|
||||
CREATE INDEX idx_promotions_status ON release.promotions(status);
|
||||
CREATE INDEX idx_promotions_target_env ON release.promotions(target_environment_id);
|
||||
|
||||
-- Approvals
|
||||
CREATE TABLE release.approvals (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
promotion_id UUID NOT NULL REFERENCES release.promotions(id) ON DELETE CASCADE,
|
||||
approver_id UUID NOT NULL REFERENCES users(id),
|
||||
action VARCHAR(50) NOT NULL CHECK (action IN ('approved', 'rejected')),
|
||||
comment TEXT,
|
||||
approved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
approver_role VARCHAR(255),
|
||||
approver_groups JSONB NOT NULL DEFAULT '[]'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_approvals_promotion ON release.approvals(promotion_id);
|
||||
CREATE INDEX idx_approvals_approver ON release.approvals(approver_id);
|
||||
|
||||
-- Approval Policies
|
||||
CREATE TABLE release.approval_policies (
|
||||
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,
|
||||
required_count INTEGER NOT NULL DEFAULT 1,
|
||||
required_roles JSONB NOT NULL DEFAULT '[]',
|
||||
required_groups JSONB NOT NULL DEFAULT '[]',
|
||||
require_sod BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
allow_self_approval BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
expiration_minutes INTEGER NOT NULL DEFAULT 1440,
|
||||
UNIQUE (tenant_id, environment_id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
```yaml
|
||||
# Promotions
|
||||
POST /api/v1/promotions
|
||||
Body: { releaseId, targetEnvironmentId, reason? }
|
||||
Response: Promotion
|
||||
|
||||
GET /api/v1/promotions
|
||||
Query: ?status={status}&releaseId={uuid}&environmentId={uuid}&page={n}
|
||||
Response: { data: Promotion[], meta: PaginationMeta }
|
||||
|
||||
GET /api/v1/promotions/{id}
|
||||
Response: Promotion (with decision record, approvals)
|
||||
|
||||
POST /api/v1/promotions/{id}/approve
|
||||
Body: { comment? }
|
||||
Response: Promotion
|
||||
|
||||
POST /api/v1/promotions/{id}/reject
|
||||
Body: { reason }
|
||||
Response: Promotion
|
||||
|
||||
POST /api/v1/promotions/{id}/cancel
|
||||
Response: Promotion
|
||||
|
||||
GET /api/v1/promotions/{id}/decision
|
||||
Response: DecisionRecord
|
||||
|
||||
GET /api/v1/promotions/{id}/approvals
|
||||
Response: Approval[]
|
||||
|
||||
GET /api/v1/promotions/{id}/evidence
|
||||
Response: EvidencePacket
|
||||
|
||||
# Gate Evaluation Preview
|
||||
POST /api/v1/promotions/preview-gates
|
||||
Body: { releaseId, targetEnvironmentId }
|
||||
Response: { wouldPass: boolean, gates: GateResult[] }
|
||||
|
||||
# Approval Policies
|
||||
POST /api/v1/approval-policies
|
||||
GET /api/v1/approval-policies
|
||||
GET /api/v1/approval-policies/{id}
|
||||
PUT /api/v1/approval-policies/{id}
|
||||
DELETE /api/v1/approval-policies/{id}
|
||||
|
||||
# Pending Approvals (for current user)
|
||||
GET /api/v1/my/pending-approvals
|
||||
Response: Promotion[]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Gate Integration
|
||||
|
||||
The security gate evaluates the release against vulnerability data from the Scanner module:
|
||||
|
||||
```typescript
|
||||
interface SecurityGateConfig {
|
||||
blockOnCritical: boolean; // Block if any critical severity
|
||||
blockOnHigh: boolean; // Block if any high severity
|
||||
maxCritical: number; // Max allowed critical (0 for strict)
|
||||
maxHigh: number; // Max allowed high
|
||||
requireFreshScan: boolean; // Require scan within N hours
|
||||
scanFreshnessHours: number; // How recent scan must be
|
||||
allowExceptions: boolean; // Allow VEX exceptions
|
||||
requireVexJustification: boolean; // Require VEX for exceptions
|
||||
}
|
||||
|
||||
interface SecurityGateResult {
|
||||
passed: boolean;
|
||||
summary: {
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
};
|
||||
blocking: Array<{
|
||||
cve: string;
|
||||
severity: string;
|
||||
component: string;
|
||||
digest: string;
|
||||
fixAvailable: boolean;
|
||||
}>;
|
||||
exceptions: Array<{
|
||||
cve: string;
|
||||
vexStatus: string;
|
||||
justification: string;
|
||||
}>;
|
||||
scanAge: {
|
||||
component: string;
|
||||
scannedAt: DateTime;
|
||||
ageHours: number;
|
||||
fresh: boolean;
|
||||
}[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Module Overview](overview.md)
|
||||
- [Workflow Engine](workflow-engine.md)
|
||||
- [Security Architecture](../security/overview.md)
|
||||
- [API Documentation](../api/promotions.md)
|
||||
Reference in New Issue
Block a user