Files
git.stella-ops.org/docs/modules/release-orchestrator/integrations/ci-cd.md

19 KiB

CI/CD Integration

Overview

Release Orchestrator integrates with CI/CD systems to:

  • Receive build completion notifications
  • Trigger additional pipelines during deployment
  • Create releases from CI artifacts
  • Report deployment status back to CI systems

Integration Patterns

Pattern 1: CI Triggers Release

                    CI TRIGGERS RELEASE

  ┌────────────┐     ┌────────────┐     ┌────────────────────┐
  │   CI/CD    │     │  Container │     │ Release            │
  │   System   │     │  Registry  │     │ Orchestrator       │
  └─────┬──────┘     └─────┬──────┘     └─────────┬──────────┘
        │                  │                      │
        │  Build & Push    │                      │
        │─────────────────►│                      │
        │                  │                      │
        │                  │  Webhook: image pushed
        │                  │─────────────────────►│
        │                  │                      │
        │                  │                      │ Create/Update
        │                  │                      │ Version Map
        │                  │                      │
        │                  │                      │ Auto-create
        │                  │                      │ Release (if configured)
        │                  │                      │
        │  API: Create Release (optional)         │
        │────────────────────────────────────────►│
        │                  │                      │
        │                  │                      │ Start Promotion
        │                  │                      │ Workflow
        │                  │                      │

Pattern 2: Orchestrator Triggers CI

                ORCHESTRATOR TRIGGERS CI

  ┌────────────────────┐     ┌────────────┐     ┌────────────┐
  │ Release            │     │   CI/CD    │     │  Target    │
  │ Orchestrator       │     │   System   │     │  Systems   │
  └─────────┬──────────┘     └─────┬──────┘     └─────┬──────┘
            │                      │                  │
            │  Pre-deploy: Trigger │                  │
            │  Integration Tests   │                  │
            │─────────────────────►│                  │
            │                      │                  │
            │                      │  Run Tests      │
            │                      │─────────────────►│
            │                      │                  │
            │  Wait for completion │                  │
            │◄─────────────────────│                  │
            │                      │                  │
            │  If passed: Deploy   │                  │
            │─────────────────────────────────────────►
            │                      │                  │

Pattern 3: Bidirectional Integration

              BIDIRECTIONAL INTEGRATION

  ┌────────────┐                         ┌────────────────────┐
  │   CI/CD    │◄───────────────────────►│ Release            │
  │   System   │                         │ Orchestrator       │
  └─────┬──────┘                         └─────────┬──────────┘
        │                                          │
        │══════════════════════════════════════════│
        │           Events (both directions)       │
        │══════════════════════════════════════════│
        │                                          │
        │ CI Events:                               │
        │  - Pipeline completed                    │
        │  - Tests passed/failed                   │
        │  - Artifacts ready                       │
        │                                          │
        │ Orchestrator Events:                     │
        │  - Deployment started                    │
        │  - Deployment completed                  │
        │  - Rollback initiated                    │
        │                                          │

CI/CD System Configuration

GitLab CI Integration

# .gitlab-ci.yml
stages:
  - build
  - push
  - release

variables:
  STELLA_API_URL: https://stella.example.com/api/v1
  COMPONENT_NAME: myapp

build:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .

push:
  stage: push
  script:
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
  rules:
    - if: $CI_COMMIT_TAG

release:
  stage: release
  image: curlimages/curl:latest
  script:
    - |
      # Get image digest
      DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG | cut -d@ -f2)

      # Create release in Stella
      curl -X POST "$STELLA_API_URL/releases" \
        -H "Authorization: Bearer $STELLA_TOKEN" \
        -H "Content-Type: application/json" \
        -d "{
          \"name\": \"$COMPONENT_NAME-$CI_COMMIT_TAG\",
          \"components\": [{
            \"componentId\": \"$STELLA_COMPONENT_ID\",
            \"digest\": \"$DIGEST\"
          }],
          \"sourceRef\": {
            \"type\": \"git\",
            \"repository\": \"$CI_PROJECT_URL\",
            \"commit\": \"$CI_COMMIT_SHA\",
            \"tag\": \"$CI_COMMIT_TAG\"
          }
        }"
  rules:
    - if: $CI_COMMIT_TAG

GitHub Actions Integration

# .github/workflows/release.yml
name: Release to Stella

on:
  push:
    tags:
      - 'v*'

jobs:
  build-and-release:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.sha }}
            ghcr.io/${{ github.repository }}:${{ github.ref_name }}

      - name: Get image digest
        id: digest
        run: |
          DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/${{ github.repository }}:${{ github.ref_name }} | cut -d@ -f2)
          echo "digest=$DIGEST" >> $GITHUB_OUTPUT

      - name: Create Stella Release
        uses: stella-ops/create-release-action@v1
        with:
          stella-url: ${{ vars.STELLA_API_URL }}
          stella-token: ${{ secrets.STELLA_TOKEN }}
          release-name: ${{ github.event.repository.name }}-${{ github.ref_name }}
          components: |
            - componentId: ${{ vars.STELLA_COMPONENT_ID }}
              digest: ${{ steps.digest.outputs.digest }}
          source-ref: |
            type: git
            repository: ${{ github.server_url }}/${{ github.repository }}
            commit: ${{ github.sha }}
            tag: ${{ github.ref_name }}

Jenkins Integration

// Jenkinsfile
pipeline {
    agent any

    environment {
        STELLA_API_URL = 'https://stella.example.com/api/v1'
        STELLA_TOKEN = credentials('stella-api-token')
        REGISTRY = 'registry.example.com'
        IMAGE_NAME = 'myorg/myapp'
    }

    stages {
        stage('Build') {
            steps {
                script {
                    docker.build("${REGISTRY}/${IMAGE_NAME}:${env.BUILD_TAG}")
                }
            }
        }

        stage('Push') {
            steps {
                script {
                    docker.withRegistry("https://${REGISTRY}", 'registry-creds') {
                        docker.image("${REGISTRY}/${IMAGE_NAME}:${env.BUILD_TAG}").push()
                    }
                }
            }
        }

        stage('Create Release') {
            when {
                tag pattern: "v\\d+\\.\\d+\\.\\d+", comparator: "REGEXP"
            }
            steps {
                script {
                    def digest = sh(
                        script: "docker inspect --format='{{index .RepoDigests 0}}' ${REGISTRY}/${IMAGE_NAME}:${env.TAG_NAME} | cut -d@ -f2",
                        returnStdout: true
                    ).trim()

                    def response = httpRequest(
                        url: "${STELLA_API_URL}/releases",
                        httpMode: 'POST',
                        contentType: 'APPLICATION_JSON',
                        customHeaders: [[name: 'Authorization', value: "Bearer ${STELLA_TOKEN}"]],
                        requestBody: """
                        {
                            "name": "${IMAGE_NAME}-${env.TAG_NAME}",
                            "components": [{
                                "componentId": "${env.STELLA_COMPONENT_ID}",
                                "digest": "${digest}"
                            }],
                            "sourceRef": {
                                "type": "git",
                                "repository": "${env.GIT_URL}",
                                "commit": "${env.GIT_COMMIT}",
                                "tag": "${env.TAG_NAME}"
                            }
                        }
                        """
                    )

                    echo "Release created: ${response.content}"
                }
            }
        }
    }

    post {
        success {
            // Notify Stella of successful build
            httpRequest(
                url: "${STELLA_API_URL}/webhooks/ci-status",
                httpMode: 'POST',
                contentType: 'APPLICATION_JSON',
                customHeaders: [[name: 'Authorization', value: "Bearer ${STELLA_TOKEN}"]],
                requestBody: """
                {
                    "buildId": "${env.BUILD_ID}",
                    "status": "success",
                    "commit": "${env.GIT_COMMIT}"
                }
                """
            )
        }
    }
}

Workflow Step Integration

Trigger CI Pipeline Step

// Step type: trigger-ci
interface TriggerCIConfig {
  integrationId: UUID;         // CI integration reference
  pipelineId: string;          // Pipeline to trigger
  ref?: string;                // Branch/tag reference
  variables?: Record<string, string>;
  waitForCompletion: boolean;
  timeout?: number;
}

class TriggerCIStep implements IStepExecutor {
  async execute(
    inputs: StepInputs,
    config: TriggerCIConfig,
    context: ExecutionContext
  ): Promise<StepOutputs> {
    const connector = await this.getConnector(config.integrationId);

    // Trigger pipeline
    const run = await connector.triggerPipeline(
      config.pipelineId,
      {
        ref: config.ref || context.release?.sourceRef?.tag,
        variables: {
          ...config.variables,
          STELLA_RELEASE_ID: context.release?.id,
          STELLA_PROMOTION_ID: context.promotion?.id,
          STELLA_ENVIRONMENT: context.environment?.name
        }
      }
    );

    if (!config.waitForCompletion) {
      return {
        pipelineRunId: run.id,
        status: run.status,
        webUrl: run.webUrl
      };
    }

    // Wait for completion
    const finalStatus = await this.waitForPipeline(
      connector,
      run.id,
      config.timeout || 3600
    );

    if (finalStatus.status !== "success") {
      throw new StepError(
        `Pipeline failed with status: ${finalStatus.status}`,
        { pipelineRunId: run.id, status: finalStatus }
      );
    }

    return {
      pipelineRunId: run.id,
      status: finalStatus.status,
      webUrl: run.webUrl
    };
  }

  private async waitForPipeline(
    connector: ICICDConnector,
    runId: string,
    timeout: number
  ): Promise<PipelineRun> {
    const deadline = Date.now() + timeout * 1000;

    while (Date.now() < deadline) {
      const run = await connector.getPipelineRun(runId);

      if (run.status === "success" || run.status === "failed" || run.status === "cancelled") {
        return run;
      }

      await sleep(10000); // Poll every 10 seconds
    }

    throw new TimeoutError(`Pipeline did not complete within ${timeout} seconds`);
  }
}

Wait for CI Step

// Step type: wait-ci
interface WaitCIConfig {
  integrationId: UUID;
  runId?: string;              // If known, or from input
  runIdInput?: string;         // Input name containing run ID
  timeout: number;
  failOnError: boolean;
}

class WaitCIStep implements IStepExecutor {
  async execute(
    inputs: StepInputs,
    config: WaitCIConfig,
    context: ExecutionContext
  ): Promise<StepOutputs> {
    const runId = config.runId || inputs[config.runIdInput!];

    if (!runId) {
      throw new StepError("Pipeline run ID not provided");
    }

    const connector = await this.getConnector(config.integrationId);

    const finalStatus = await this.waitForPipeline(
      connector,
      runId,
      config.timeout
    );

    const success = finalStatus.status === "success";

    if (!success && config.failOnError) {
      throw new StepError(
        `Pipeline failed with status: ${finalStatus.status}`,
        { pipelineRunId: runId, status: finalStatus }
      );
    }

    return {
      status: finalStatus.status,
      success,
      pipelineRun: finalStatus
    };
  }
}

Deployment Status Reporting

GitHub Deployment Status

class GitHubStatusReporter {
  async reportDeploymentStart(
    integration: Integration,
    deployment: DeploymentContext
  ): Promise<void> {
    const client = await this.getClient(integration);

    // Create deployment
    const { data: ghDeployment } = await client.repos.createDeployment({
      owner: deployment.repository.owner,
      repo: deployment.repository.name,
      ref: deployment.sourceRef.commit,
      environment: deployment.environment.name,
      auto_merge: false,
      required_contexts: [],
      payload: {
        stellaReleaseId: deployment.release.id,
        stellaPromotionId: deployment.promotion.id
      }
    });

    // Set status to in_progress
    await client.repos.createDeploymentStatus({
      owner: deployment.repository.owner,
      repo: deployment.repository.name,
      deployment_id: ghDeployment.id,
      state: "in_progress",
      log_url: `${this.stellaUrl}/deployments/${deployment.jobId}`,
      description: "Deployment in progress"
    });

    // Store deployment ID for later status update
    await this.storeMapping(deployment.jobId, ghDeployment.id);
  }

  async reportDeploymentComplete(
    integration: Integration,
    deployment: DeploymentContext,
    success: boolean
  ): Promise<void> {
    const client = await this.getClient(integration);
    const ghDeploymentId = await this.getMapping(deployment.jobId);

    await client.repos.createDeploymentStatus({
      owner: deployment.repository.owner,
      repo: deployment.repository.name,
      deployment_id: ghDeploymentId,
      state: success ? "success" : "failure",
      log_url: `${this.stellaUrl}/deployments/${deployment.jobId}`,
      environment_url: deployment.environment.url,
      description: success
        ? "Deployment completed successfully"
        : "Deployment failed"
    });
  }
}

GitLab Pipeline Status

class GitLabStatusReporter {
  async reportDeploymentStatus(
    integration: Integration,
    deployment: DeploymentContext,
    state: "running" | "success" | "failed" | "canceled"
  ): Promise<void> {
    const client = await this.getClient(integration);

    await client.post(
      `/projects/${integration.config.projectId}/statuses/${deployment.sourceRef.commit}`,
      {
        state,
        ref: deployment.sourceRef.tag || deployment.sourceRef.branch,
        name: `stella/${deployment.environment.name}`,
        target_url: `${this.stellaUrl}/deployments/${deployment.jobId}`,
        description: this.getDescription(state, deployment)
      }
    );
  }

  private getDescription(state: string, deployment: DeploymentContext): string {
    switch (state) {
      case "running":
        return `Deploying to ${deployment.environment.name}`;
      case "success":
        return `Deployed to ${deployment.environment.name}`;
      case "failed":
        return `Deployment to ${deployment.environment.name} failed`;
      case "canceled":
        return `Deployment to ${deployment.environment.name} cancelled`;
      default:
        return "";
    }
  }
}

API for CI Systems

Create Release from CI

POST /api/v1/releases
Authorization: Bearer <ci-token>
Content-Type: application/json

{
  "name": "myapp-v1.2.0",
  "components": [
    {
      "componentId": "component-uuid",
      "digest": "sha256:abc123..."
    }
  ],
  "sourceRef": {
    "type": "git",
    "repository": "https://github.com/myorg/myapp",
    "commit": "abc123def456",
    "tag": "v1.2.0",
    "branch": "main"
  },
  "metadata": {
    "buildId": "12345",
    "buildUrl": "https://ci.example.com/builds/12345",
    "triggeredBy": "ci-pipeline"
  }
}

Report Build Status

POST /api/v1/ci-events/build-complete
Authorization: Bearer <ci-token>
Content-Type: application/json

{
  "integrationId": "integration-uuid",
  "buildId": "12345",
  "status": "success",
  "commit": "abc123def456",
  "artifacts": [
    {
      "name": "myapp",
      "digest": "sha256:abc123...",
      "repository": "registry.example.com/myorg/myapp"
    }
  ],
  "testResults": {
    "passed": 150,
    "failed": 0,
    "skipped": 5
  }
}

Service Account for CI

Creating CI Service Account

POST /api/v1/service-accounts
Authorization: Bearer <admin-token>
Content-Type: application/json

{
  "name": "ci-pipeline",
  "description": "Service account for CI/CD integration",
  "roles": ["release-creator"],
  "permissions": [
    { "resource": "release", "action": "create" },
    { "resource": "component", "action": "read" },
    { "resource": "version-map", "action": "read" }
  ],
  "expiresIn": "365d"
}

Response:

{
  "success": true,
  "data": {
    "id": "sa-uuid",
    "name": "ci-pipeline",
    "token": "stella_sa_xxxxxxxxxxxxx",
    "expiresAt": "2027-01-09T00:00:00Z"
  }
}

References