release orchestrator pivot, architecture and planning
This commit is contained in:
643
docs/modules/release-orchestrator/integrations/ci-cd.md
Normal file
643
docs/modules/release-orchestrator/integrations/ci-cd.md
Normal file
@@ -0,0 +1,643 @@
|
||||
# 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
|
||||
|
||||
```yaml
|
||||
# .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
|
||||
|
||||
```yaml
|
||||
# .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
|
||||
|
||||
```groovy
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```http
|
||||
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
|
||||
|
||||
```http
|
||||
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
|
||||
|
||||
```http
|
||||
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:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "sa-uuid",
|
||||
"name": "ci-pipeline",
|
||||
"token": "stella_sa_xxxxxxxxxxxxx",
|
||||
"expiresAt": "2027-01-09T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Integrations Overview](overview.md)
|
||||
- [Connectors](connectors.md)
|
||||
- [Webhooks](webhooks.md)
|
||||
- [Workflow Templates](../workflow/templates.md)
|
||||
Reference in New Issue
Block a user