DEPLOY: Deployment Execution
Purpose: Orchestrate deployment jobs, execute on targets, manage rollbacks, and generate artifacts.
Modules
Module: deploy-orchestrator
| Aspect | Specification |
|---|---|
| Responsibility | Deployment job coordination; strategy execution |
| Dependencies | target-executor, artifact-generator, agent-manager |
| Data Entities | DeploymentJob, DeploymentTask |
| Events Produced | deployment.started, deployment.task_started, deployment.task_completed, deployment.completed, deployment.failed |
Deployment Job Entity:
interface DeploymentJob {
id: UUID;
tenantId: UUID;
promotionId: UUID;
releaseId: UUID;
environmentId: UUID;
status: DeploymentStatus;
strategy: DeploymentStrategy;
startedAt: DateTime;
completedAt: DateTime;
artifacts: GeneratedArtifact[];
rollbackOf: UUID | null; // If this is a rollback job
tasks: DeploymentTask[];
}
type DeploymentStatus =
| "pending" // Waiting to start
| "running" // Deployment in progress
| "succeeded" // All tasks succeeded
| "failed" // One or more tasks failed
| "cancelled" // User cancelled
| "rolling_back" // Rollback in progress
| "rolled_back"; // Rollback complete
interface DeploymentTask {
id: UUID;
jobId: UUID;
targetId: UUID;
digest: string;
status: TaskStatus;
agentId: UUID | null;
startedAt: DateTime;
completedAt: DateTime;
exitCode: number | null;
logs: string;
previousDigest: string | null;
stickerWritten: boolean;
}
type TaskStatus =
| "pending"
| "running"
| "succeeded"
| "failed"
| "cancelled"
| "skipped";
Module: target-executor
| Aspect | Specification |
|---|---|
| Responsibility | Target-specific deployment logic |
| Dependencies | agent-manager, connector-runtime |
| Protocol | gRPC for agents, SSH/WinRM for agentless |
Executor Types:
| Type | Transport | Use Case |
|---|---|---|
agent-docker | gRPC | Docker hosts with agent |
agent-compose | gRPC | Compose hosts with agent |
ssh-remote | SSH | Agentless Linux hosts |
winrm-remote | WinRM | Agentless Windows hosts |
ecs-api | AWS API | AWS ECS services |
nomad-api | Nomad API | HashiCorp Nomad jobs |
Module: runner-executor
| Aspect | Specification |
|---|---|
| Responsibility | Script/hook execution in sandbox |
| Dependencies | plugin-sandbox |
| Supported Scripts | C# (.csx), Bash, PowerShell |
Hook Types:
pre-deploy: Run before deployment startspost-deploy: Run after deployment succeedson-failure: Run when deployment failson-rollback: Run during rollback
Module: artifact-generator
| Aspect | Specification |
|---|---|
| Responsibility | Generate immutable deployment artifacts |
| Dependencies | release-manager, environment-manager |
| Data Entities | GeneratedArtifact, ComposeLock, VersionSticker |
Generated Artifacts:
| Artifact Type | Description |
|---|---|
compose_lock | compose.stella.lock.yml - Pinned digests |
script | Compiled deployment script |
sticker | stella.version.json - Version marker |
evidence | Decision and execution evidence |
config | Environment-specific config files |
Compose Lock File Generation:
class ComposeLockGenerator {
async generate(
release: Release,
environment: Environment,
targets: Target[]
): Promise<GeneratedArtifact> {
const services: Record<string, any> = {};
for (const component of release.components) {
services[component.componentName] = {
// CRITICAL: Always use digest, never tag
image: `${component.imageRepository}@${component.digest}`,
// Environment variables
environment: this.mergeEnvironment(
environment.config.variables,
this.buildStellaEnv(release, environment)
),
// Labels for Stella tracking
labels: {
"stella.release.id": release.id,
"stella.release.name": release.name,
"stella.component.name": component.componentName,
"stella.component.digest": component.digest,
"stella.environment": environment.name,
"stella.deployed.at": new Date().toISOString(),
},
};
}
const composeLock = {
version: "3.8",
services,
"x-stella": {
release_id: release.id,
release_name: release.name,
environment: environment.name,
generated_at: new Date().toISOString(),
inputs_hash: this.computeInputsHash(release, environment),
components: release.components.map(c => ({
name: c.componentName,
digest: c.digest,
semver: c.semver,
})),
},
};
const content = yaml.stringify(composeLock);
const hash = crypto.createHash("sha256").update(content).digest("hex");
return {
type: "compose_lock",
name: "compose.stella.lock.yml",
content: Buffer.from(content),
contentHash: `sha256:${hash}`,
};
}
}
Version Sticker Generation:
interface VersionSticker {
stella_version: "1.0";
release_id: UUID;
release_name: string;
components: Array<{
name: string;
digest: string;
semver: string;
tag: string;
image_repository: string;
}>;
environment: string;
environment_id: UUID;
deployed_at: string;
deployed_by: UUID;
promotion_id: UUID;
workflow_run_id: UUID;
evidence_packet_id: UUID;
evidence_packet_hash: string;
orchestrator_version: string;
source_ref?: {
commit_sha: string;
branch: string;
repository: string;
};
}
Module: rollback-manager
| Aspect | Specification |
|---|---|
| Responsibility | Rollback orchestration; previous state recovery |
| Dependencies | deploy-orchestrator, target-registry |
Rollback Strategies:
| Strategy | Description |
|---|---|
to-previous | Roll back to last successful deployment |
to-release | Roll back to specific release ID |
to-sticker | Roll back to version in sticker on target |
Rollback Flow:
- Identify rollback target (previous release or specified)
- Create rollback deployment job
- Execute deployment with rollback artifacts
- Update target state and sticker
- Record rollback evidence
Deployment Strategies
All-at-Once
Deploy to all targets simultaneously.
interface AllAtOnceConfig {
parallelism: number; // Max concurrent deployments (0 = unlimited)
continueOnFailure: boolean; // Continue if some targets fail
failureThreshold: number; // Max failures before abort
}
Rolling
Deploy to targets sequentially with health checks.
interface RollingConfig {
batchSize: number; // Targets per batch
batchDelay: number; // Seconds between batches
healthCheckBetweenBatches: boolean;
rollbackOnFailure: boolean;
maxUnavailable: number; // Max targets unavailable at once
}
Canary
Deploy to subset, verify, then proceed.
interface CanaryConfig {
canaryTargets: number; // Number or percentage for canary
canaryDuration: number; // Seconds to run canary
healthThreshold: number; // Required health percentage
autoPromote: boolean; // Auto-proceed if healthy
requireApproval: boolean; // Require manual approval
}
Blue-Green
Deploy to B, switch traffic, retire A.
interface BlueGreenConfig {
targetGroupA: UUID; // Current (blue) target group
targetGroupB: UUID; // New (green) target group
trafficShiftType: "instant" | "gradual";
gradualShiftSteps?: number[]; // e.g., [10, 25, 50, 100]
rollbackOnHealthFailure: boolean;
}
Rolling Deployment Algorithm
class RollingDeploymentExecutor:
def execute(self, job: DeploymentJob, config: RollingConfig) -> DeploymentResult:
targets = self.get_targets(job.environment_id)
batches = self.create_batches(targets, config.batch_size)
deployed_targets = []
failed_targets = []
for batch_index, batch in enumerate(batches):
self.log(f"Starting batch {batch_index + 1} of {len(batches)}")
# Deploy batch in parallel
batch_results = self.deploy_batch(job, batch)
for target, result in batch_results:
if result.success:
deployed_targets.append(target)
# Write version sticker
self.write_sticker(target, job.release)
else:
failed_targets.append(target)
if config.rollback_on_failure:
# Rollback all deployed targets
self.rollback_targets(deployed_targets, job.previous_release)
return DeploymentResult(
success=False,
error=f"Batch {batch_index + 1} failed, rolled back",
deployed=deployed_targets,
failed=failed_targets,
rolled_back=deployed_targets
)
# Health check between batches
if config.health_check_between_batches and batch_index < len(batches) - 1:
health_result = self.check_batch_health(deployed_targets[-len(batch):])
if not health_result.healthy:
if config.rollback_on_failure:
self.rollback_targets(deployed_targets, job.previous_release)
return DeploymentResult(
success=False,
error=f"Health check failed after batch {batch_index + 1}",
deployed=deployed_targets,
failed=failed_targets,
rolled_back=deployed_targets
)
# Delay between batches
if config.batch_delay > 0 and batch_index < len(batches) - 1:
time.sleep(config.batch_delay)
return DeploymentResult(
success=len(failed_targets) == 0,
deployed=deployed_targets,
failed=failed_targets
)
Database Schema
-- Deployment Jobs
CREATE TABLE release.deployment_jobs (
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),
release_id UUID NOT NULL REFERENCES release.releases(id),
environment_id UUID NOT NULL REFERENCES release.environments(id),
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN (
'pending', 'running', 'succeeded', 'failed', 'cancelled', 'rolling_back', 'rolled_back'
)),
strategy VARCHAR(50) NOT NULL DEFAULT 'all-at-once',
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
artifacts JSONB NOT NULL DEFAULT '[]',
rollback_of UUID REFERENCES release.deployment_jobs(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_deployment_jobs_promotion ON release.deployment_jobs(promotion_id);
CREATE INDEX idx_deployment_jobs_status ON release.deployment_jobs(status);
-- Deployment Tasks
CREATE TABLE release.deployment_tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id UUID NOT NULL REFERENCES release.deployment_jobs(id) ON DELETE CASCADE,
target_id UUID NOT NULL REFERENCES release.targets(id),
digest VARCHAR(100) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN (
'pending', 'running', 'succeeded', 'failed', 'cancelled', 'skipped'
)),
agent_id UUID REFERENCES release.agents(id),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
exit_code INTEGER,
logs TEXT,
previous_digest VARCHAR(100),
sticker_written BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_deployment_tasks_job ON release.deployment_tasks(job_id);
CREATE INDEX idx_deployment_tasks_target ON release.deployment_tasks(target_id);
CREATE INDEX idx_deployment_tasks_status ON release.deployment_tasks(status);
-- Generated Artifacts
CREATE TABLE release.generated_artifacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
deployment_job_id UUID REFERENCES release.deployment_jobs(id) ON DELETE CASCADE,
artifact_type VARCHAR(50) NOT NULL CHECK (artifact_type IN (
'compose_lock', 'script', 'sticker', 'evidence', 'config'
)),
name VARCHAR(255) NOT NULL,
content_hash VARCHAR(100) NOT NULL,
content BYTEA, -- for small artifacts
storage_ref VARCHAR(500), -- for large artifacts (S3, etc.)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_generated_artifacts_job ON release.generated_artifacts(deployment_job_id);
API Endpoints
# Deployment Jobs (mostly read-only; created by promotions)
GET /api/v1/deployment-jobs
Query: ?promotionId={uuid}&status={status}&environmentId={uuid}
Response: DeploymentJob[]
GET /api/v1/deployment-jobs/{id}
Response: DeploymentJob (with tasks)
GET /api/v1/deployment-jobs/{id}/tasks
Response: DeploymentTask[]
GET /api/v1/deployment-jobs/{id}/tasks/{taskId}
Response: DeploymentTask (with logs)
GET /api/v1/deployment-jobs/{id}/tasks/{taskId}/logs
Query: ?follow=true
Response: string | SSE stream
GET /api/v1/deployment-jobs/{id}/artifacts
Response: GeneratedArtifact[]
GET /api/v1/deployment-jobs/{id}/artifacts/{artifactId}
Response: binary (download)
# Rollback
POST /api/v1/rollbacks
Body: {
environmentId: UUID,
strategy: "to-previous" | "to-release" | "to-sticker",
targetReleaseId?: UUID # for to-release strategy
}
Response: DeploymentJob (rollback job)
GET /api/v1/rollbacks
Query: ?environmentId={uuid}
Response: DeploymentJob[] (rollback jobs only)
