DEPLOY: Deployment Execution

Purpose: Orchestrate deployment jobs, execute on targets, manage rollbacks, and generate artifacts.

Modules

Module: deploy-orchestrator

AspectSpecification
ResponsibilityDeployment job coordination; strategy execution
Dependenciestarget-executor, artifact-generator, agent-manager
Data EntitiesDeploymentJob, DeploymentTask
Events Produceddeployment.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

AspectSpecification
ResponsibilityTarget-specific deployment logic
Dependenciesagent-manager, connector-runtime
ProtocolgRPC for agents, SSH/WinRM for agentless

Executor Types:

TypeTransportUse Case
agent-dockergRPCDocker hosts with agent
agent-composegRPCCompose hosts with agent
ssh-remoteSSHAgentless Linux hosts
winrm-remoteWinRMAgentless Windows hosts
ecs-apiAWS APIAWS ECS services
nomad-apiNomad APIHashiCorp Nomad jobs

Module: runner-executor

AspectSpecification
ResponsibilityScript/hook execution in sandbox
Dependenciesplugin-sandbox
Supported ScriptsC# (.csx), Bash, PowerShell

Hook Types:


Module: artifact-generator

AspectSpecification
ResponsibilityGenerate immutable deployment artifacts
Dependenciesrelease-manager, environment-manager
Data EntitiesGeneratedArtifact, ComposeLock, VersionSticker

Generated Artifacts:

Artifact TypeDescription
compose_lockcompose.stella.lock.yml - Pinned digests
scriptCompiled deployment script
stickerstella.version.json - Version marker
evidenceDecision and execution evidence
configEnvironment-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

AspectSpecification
ResponsibilityRollback orchestration; previous state recovery
Dependenciesdeploy-orchestrator, target-registry

Rollback Strategies:

StrategyDescription
to-previousRoll back to last successful deployment
to-releaseRoll back to specific release ID
to-stickerRoll back to version in sticker on target

Rollback Flow:

  1. Identify rollback target (previous release or specified)
  2. Create rollback deployment job
  3. Execute deployment with rollback artifacts
  4. Update target state and sticker
  5. 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)

References