CI Integration¶
kardinal-promoter is triggered by your CI pipeline. After building and pushing a container image, CI creates a Bundle that enters the promotion pipeline.
Bundle Creation Methods¶
HTTP Webhook¶
The controller exposes a /api/v1/bundles endpoint that accepts JSON payloads.
curl -X POST https://kardinal.example.com/api/v1/bundles \
-H "Authorization: Bearer $KARDINAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline": "my-app",
"type": "image",
"images": [
{
"repository": "ghcr.io/myorg/my-app",
"tag": "1.29.0",
"digest": "sha256:a1b2c3d4e5f6..."
}
],
"provenance": {
"commitSHA": "abc123def456",
"ciRunURL": "https://github.com/myorg/my-app/actions/runs/12345",
"author": "engineer-name"
}
}'
The endpoint creates a Bundle CRD in the cluster. Authentication is via Bearer token validated against a Kubernetes Secret. The token is scoped per Pipeline.
GitHub Action¶
The action is at .github/actions/create-bundle/ and uses composite steps (no Docker container required). Authenticate via the KARDINAL_TOKEN environment variable.
Single-image promotion (most common):
name: Build and Promote
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push image
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Create Bundle
id: bundle
uses: ./.github/actions/create-bundle
env:
KARDINAL_TOKEN: ${{ secrets.KARDINAL_TOKEN }}
with:
pipeline: my-app
image: ghcr.io/${{ github.repository }}:${{ github.sha }}
digest: ${{ steps.build.outputs.digest }}
kardinal-url: https://kardinal.example.com
- name: Log bundle URL
run: echo "Promotion started: ${{ steps.bundle.outputs.bundle-status-url }}"
Multi-image promotion (for services with multiple containers):
- name: Create Bundle
uses: ./.github/actions/create-bundle
env:
KARDINAL_TOKEN: ${{ secrets.KARDINAL_TOKEN }}
with:
pipeline: my-app
images: |
ghcr.io/myorg/app:${{ github.sha }}
ghcr.io/myorg/sidecar:${{ github.sha }}
kardinal-url: https://kardinal.example.com
Action inputs:
| Input | Required | Default | Description |
|---|---|---|---|
pipeline | Yes | — | Pipeline name |
image | No | — | Single image (repo:tag or repo@sha256:digest) |
digest | No | — | Override digest for the image input |
images | No | — | Newline-separated list of images (multi-image case) |
namespace | No | default | Kubernetes namespace |
kardinal-url | Yes | — | Controller base URL |
type | No | image | Bundle type (image, config, mixed) |
Action outputs:
| Output | Description |
|---|---|
bundle-name | Name of the created Bundle CRD |
bundle-namespace | Namespace of the created Bundle CRD |
bundle-status-url | Link to the pipeline view in the kardinal UI |
The action retries on transient failures (network errors, HTTP 5xx) up to 3 times with exponential backoff. Permanent errors (HTTP 4xx — bad token, bad input) do not retry. KARDINAL_TOKEN must be set as a secret in your repository settings.
GitLab CI¶
stages:
- build
- promote
build:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
artifacts:
reports:
dotenv: build.env
promote:
stage: promote
script:
- |
curl -X POST https://kardinal.example.com/api/v1/bundles \
-H "Authorization: Bearer $KARDINAL_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"pipeline\": \"my-app\",
\"artifacts\": {
\"images\": [{
\"name\": \"my-app\",
\"reference\": \"$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA\",
\"digest\": \"$IMAGE_DIGEST\"
}]
},
\"provenance\": {
\"commitSHA\": \"$CI_COMMIT_SHA\",
\"ciRunURL\": \"$CI_PIPELINE_URL\",
\"author\": \"$GITLAB_USER_LOGIN\"
}
}"
kubectl (declarative)¶
For teams that prefer a fully declarative approach, the Bundle can be created via kubectl in CI:
# In your CI pipeline
- name: Create Bundle
run: |
cat <<EOF | kubectl apply -f -
apiVersion: kardinal.io/v1alpha1
kind: Bundle
metadata:
name: my-app-${GITHUB_SHA::8}-$(date +%s)
labels:
kardinal.io/pipeline: my-app
spec:
type: image
pipeline: my-app
images:
- repository: ghcr.io/${{ github.repository }}
tag: "${{ github.sha }}"
digest: "${{ steps.build.outputs.digest }}"
provenance:
commitSHA: "${{ github.sha }}"
ciRunURL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
author: "${{ github.actor }}"
timestamp: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
EOF
This requires the CI runner to have kubectl access to the cluster and RBAC permissions to create Bundle CRDs.
Authentication¶
Webhook token¶
The /api/v1/bundles endpoint requires a Bearer token. The token is stored in a Kubernetes Secret and validated by the controller.
kubectl create secret generic kardinal-ci-token \
--namespace=kardinal-system \
--from-literal=token=$(openssl rand -hex 32)
The token is passed to the controller via the --bundle-api-token flag or the KARDINAL_BUNDLE_TOKEN environment variable. The endpoint is only activated when this flag is set.
Rate limiting: 60 requests per minute per token.
kubectl access¶
When using the kubectl approach, CI needs a kubeconfig with a ServiceAccount that has permission to create Bundle CRDs. This is standard Kubernetes RBAC.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: bundle-creator
namespace: default
rules:
- apiGroups: ["kardinal.io"]
resources: ["bundles"]
verbs: ["create", "get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-bundle-creator
namespace: default
subjects:
- kind: ServiceAccount
name: ci-runner
namespace: default
roleRef:
kind: Role
name: bundle-creator
apiGroup: rbac.authorization.k8s.io
Provenance¶
The provenance field on the Bundle is optional but strongly recommended. It enables: - PR evidence showing who built the image and from which commit - PolicyGate expressions that reference provenance (e.g., bundle.provenance.author != "dependabot[bot]") - Audit trail linking deployments back to source changes
| Field | Description | Example |
|---|---|---|
commitSHA | The Git commit that triggered the build | abc123def456 |
ciRunURL | URL of the CI run | https://github.com/.../runs/12345 |
author | Who or what triggered the build | engineer-name, dependabot[bot] |
timestamp | When the image was built (ISO 8601) | 2026-04-09T10:00:00Z |
Multi-Image Bundles¶
A Bundle can contain multiple images for applications that deploy multiple containers together:
{
"pipeline": "my-app",
"type": "image",
"images": [
{
"repository": "ghcr.io/myorg/my-app-api",
"tag": "1.29.0",
"digest": "sha256:aaa..."
},
{
"repository": "ghcr.io/myorg/my-app-worker",
"tag": "1.29.0",
"digest": "sha256:bbb..."
}
]
}
The Kustomize update strategy will run kustomize edit set-image for each image in the Bundle.
Config-Only Bundles¶
To promote configuration changes (resource limits, env vars, feature flags) without an image change, create a config Bundle:
curl -X POST https://kardinal.example.com/api/v1/bundles \
-H "Authorization: Bearer $KARDINAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline": "my-app",
"type": "config",
"configRef": {
"gitRepo": "https://github.com/myorg/app-config",
"commitSHA": "abc123def456"
},
"provenance": {
"commitSHA": "abc123def456",
"ciRunURL": "https://github.com/myorg/app-config/actions/runs/67890",
"author": "platform-team"
}
}'
Config Bundles go through the same Pipeline, PolicyGates, and PR flow as image Bundles. The only difference is the update step: instead of kustomize-set-image, the controller uses config-merge to apply the referenced commit's changes.
Bundle Intent¶
When creating a Bundle from CI, you can specify the promotion intent:
{
"pipeline": "my-app",
"type": "image",
"images": [ ... ],
"provenance": { ... },
"intent": {
"targetEnvironment": "staging"
}
}
targetEnvironment: prod(default): promote through all environments up to and including prodtargetEnvironment: staging: stop after staging (useful for testing)skipEnvironments: ["staging"]: skip staging (requires SkipPermission PolicyGate)
Webhook Endpoint Reference¶
URL: POST /api/v1/bundles
Headers: | Header | Required | Description | |---|---|---| | Authorization | Yes | Bearer <token> | | Content-Type | Yes | application/json | | X-Kardinal-Signature | No | HMAC-SHA256 signature for request body verification |
Response codes: | Code | Meaning | |---|---| | 201 | Bundle created successfully | | 400 | Invalid request body | | 401 | Invalid or missing token | | 404 | Pipeline not found | | 409 | Bundle with same version already exists (idempotent) | | 429 | Rate limit exceeded |