19 KiB
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"
}
}