Quickstart¶
This guide walks you through setting up your first promotion pipeline with kardinal-promoter. By the end, you will have a working pipeline that promotes the kardinal-test-app through test, uat, and prod environments using Git pull requests.
What you will build¶
graph LR
CI["CI pushes image<br/>kardinal-test-app:sha-abc1234"] --> Bundle["Bundle created"]
Bundle --> Test["test\n(auto-promote)"]
Test --> UAT["uat\n(auto-promote)"]
UAT --> Gate["no-weekend-deploys\nCEL gate"]
Gate --> Prod["prod\n(PR review required)"]
Prod --> Verified["All Verified ✅"]
style Test fill:#22c55e,color:#fff
style UAT fill:#22c55e,color:#fff
style Gate fill:#f59e0b,color:#fff
style Prod fill:#3b82f6,color:#fff - Test and uat promote automatically when the upstream environment is verified.
- Prod requires a human to review and merge a PR.
- The PR includes promotion evidence: what image is being deployed, who built it, and what upstream verification looked like.
Test application
This quickstart uses pnz1990/kardinal-test-app and the pnz1990/kardinal-demo GitOps repository. These are the reference applications used in kardinal's own CI validation.
Prerequisites¶
- A Kubernetes cluster (kind, Docker Desktop, EKS, GKE, or any distribution)
- kardinal-promoter installed
- Argo CD installed (or Flux; this guide uses Argo CD)
- A GitHub account with a personal access token (PAT) that has repo write access
- A GitOps repository with Kustomize overlays (see Set Up Your GitOps Repo)
Install kardinal-promoter¶
Starting with v0.6.0, kardinal-promoter bundles the krocodile Graph controller directly in the Helm chart. A single helm install installs everything — no separate krocodile step needed.
# Option A: reference an existing Secret (recommended for production)
kubectl create secret generic github-token \
--namespace kardinal-system \
--from-literal=token=$GITHUB_PAT \
--dry-run=client -o yaml | kubectl apply -f - --namespace kardinal-system 2>/dev/null || \
kubectl create namespace kardinal-system && \
kubectl create secret generic github-token \
--namespace kardinal-system \
--from-literal=token=$GITHUB_PAT
helm install kardinal-promoter oci://ghcr.io/pnz1990/charts/kardinal-promoter \
--namespace kardinal-system --create-namespace \
--set github.secretRef.name=github-token
# Option B: pass the token directly (development/testing only)
helm install kardinal-promoter oci://ghcr.io/pnz1990/charts/kardinal-promoter \
--namespace kardinal-system --create-namespace \
--set github.token=$GITHUB_PAT
Verify the installation:
kubectl get pods -n kardinal-system
# NAME READY STATUS RESTARTS AGE
# kardinal-promoter-controller-7f8d9c-xxxxx 1/1 Running 0 30s
kubectl get pods -n kro-system
# NAME READY STATUS RESTARTS AGE
# graph-controller-7d4b8f9f5-xk2pq 1/1 Running 0 30s
kardinal version
# CLI: v0.1.0
# Controller: v0.1.0
Set up your GitOps repo¶
This quickstart uses pnz1990/kardinal-demo as the GitOps target repository. It already has the required directory structure with environment branches. Fork it or use it directly.
The repository structure:
environments/
test/
kustomization.yaml # patches for test
uat/
kustomization.yaml # patches for uat
prod/
kustomization.yaml # patches for prod
Create Argo CD Applications¶
Create an Argo CD ApplicationSet to manage the three environments:
cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: kardinal-test-app
namespace: argocd
spec:
generators:
- list:
elements:
- env: test
- env: uat
- env: prod
template:
metadata:
name: kardinal-test-app-{{env}}
spec:
project: default
source:
repoURL: https://github.com/pnz1990/kardinal-demo
targetRevision: main
path: environments/{{env}}
destination:
server: https://kubernetes.default.svc
namespace: kardinal-test-app-{{env}}
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
EOF
Create the Pipeline¶
First, create a Secret with your GitHub token:
You can generate a Pipeline YAML using kardinal init:
kardinal init
# Application name [my-app]: kardinal-test-app
# Namespace [default]: default
# Environments (comma-separated) [test,uat,prod]: test,uat,prod
# Git repository URL: https://github.com/pnz1990/kardinal-demo
# Base branch [main]: main
# Update strategy (kustomize/helm) [kustomize]: kustomize
# Pipeline YAML written to pipeline.yaml
# Apply with: kubectl apply -f pipeline.yaml
Then apply it:
Or apply examples/quickstart/pipeline.yaml directly:
cat <<EOF | kubectl apply -f -
apiVersion: kardinal.io/v1alpha1
kind: Pipeline
metadata:
name: kardinal-test-app
spec:
git:
url: https://github.com/pnz1990/kardinal-demo
branch: main
layout: directory
provider: github
secretRef:
name: github-token
environments:
- name: test
path: environments/test
update:
strategy: kustomize
approval: auto
health:
type: argocd
argocd:
name: kardinal-test-app-test
- name: uat
path: environments/uat
update:
strategy: kustomize
approval: auto
health:
type: argocd
argocd:
name: kardinal-test-app-uat
- name: prod
path: environments/prod
update:
strategy: kustomize
approval: pr-review
health:
type: argocd
argocd:
name: kardinal-test-app-prod
EOF
Verify the Pipeline was created:
Troubleshooting: Pipeline not appearing
If the pipeline doesn't appear, check that the controller is running:
Create your first Bundle¶
In a real setup, your CI pipeline creates Bundles after building and pushing images. For this quickstart, use the latest kardinal-test-app image:
# Get the latest image SHA from the test app repository
LATEST_SHA=$(gh api repos/pnz1990/kardinal-test-app/commits/main --jq '.sha[:7]')
TEST_IMAGE="ghcr.io/pnz1990/kardinal-test-app:sha-${LATEST_SHA}"
echo "Using image: $TEST_IMAGE"
# Create the Bundle
kardinal create bundle kardinal-test-app --image $TEST_IMAGE
Or equivalently with kubectl:
cat <<EOF | kubectl apply -f -
apiVersion: kardinal.io/v1alpha1
kind: Bundle
metadata:
name: kardinal-test-app-sha-${LATEST_SHA}
labels:
kardinal.io/pipeline: kardinal-test-app
spec:
type: image
pipeline: kardinal-test-app
images:
- repository: ghcr.io/pnz1990/kardinal-test-app
tag: "sha-${LATEST_SHA}"
provenance:
commitSHA: "${LATEST_SHA}"
author: "quickstart"
EOF
Watch the promotion¶
The promotion starts immediately. kardinal-promoter generates a Graph and begins promoting through environments.
# Watch the pipeline status
kardinal get pipelines
# PIPELINE BUNDLE TEST UAT PROD AGE
# kardinal-test-app sha-abc1234 Verified Promoting Waiting 2m
# See individual steps
kardinal get steps kardinal-test-app
# STEP TYPE STATE ENV
# kardinal-test-app-sha-abc1234-test PromotionStep Verified test
# kardinal-test-app-sha-abc1234-uat PromotionStep HealthChecking uat
# kardinal-test-app-sha-abc1234-prod PromotionStep Pending prod
# Check why prod hasn't started
kardinal explain kardinal-test-app --env prod
# PROMOTION: kardinal-test-app / prod
# Bundle: sha-abc1234
#
# RESULT: WAITING
# Upstream environment "uat" has not been verified yet.
Troubleshooting: Test stuck in HealthChecking
If the test environment stays in HealthChecking for more than a few minutes:
Once uat is verified, the prod PromotionStep is created. Since prod uses approval: pr-review, kardinal-promoter opens a PR:
kardinal get steps kardinal-test-app
# STEP TYPE STATE ENV
# kardinal-test-app-sha-abc1234-test PromotionStep Verified test
# kardinal-test-app-sha-abc1234-uat PromotionStep Verified uat
# kardinal-test-app-sha-abc1234-prod PromotionStep WaitingForMerge prod
Go to your GitHub repo (pnz1990/kardinal-demo). You will see a PR titled:
[kardinal] Promote kardinal-test-app sha-abc1234 to prod
The PR body contains: - The artifact being promoted (image reference, digest) - Build provenance (commit SHA, CI run link, author) - Upstream verification status (test and uat verified timestamps) - Policy gate compliance (if any gates are configured)
Merge the PR. kardinal-promoter detects the merge via webhook, Argo CD syncs the prod Application, and the health adapter verifies it.
kardinal get pipelines
# PIPELINE BUNDLE TEST UAT PROD AGE
# kardinal-test-app sha-abc1234 Verified Verified Verified 8m
The promotion is complete.
Adding policy gates (optional)¶
To add a no-weekend-deploys gate to prod, create a PolicyGate in the platform-policies namespace:
kubectl create namespace platform-policies 2>/dev/null
cat <<EOF | kubectl apply -f -
apiVersion: kardinal.io/v1alpha1
kind: PolicyGate
metadata:
name: no-weekend-deploys
namespace: platform-policies
labels:
kardinal.io/scope: org
kardinal.io/applies-to: prod
kardinal.io/type: gate
spec:
expression: "!schedule.isWeekend"
message: "Production deployments are blocked on weekends"
recheckInterval: 5m
EOF
The next Bundle promoted to prod will have this gate injected into its Graph. If it is a weekend, the gate blocks the promotion and kardinal explain shows why.
Adding to your CI pipeline¶
Add a step to your CI pipeline that creates a Bundle after building and pushing your image.
GitHub Actions example:
- name: Create Bundle
run: |
curl -X POST https://kardinal.example.com/api/v1/bundles \
-H "Authorization: Bearer ${{ secrets.KARDINAL_TOKEN }}" \
-d '{
"pipeline": "kardinal-test-app",
"type": "image",
"images": [{"repository": "ghcr.io/${{ github.repository }}", "tag": "sha-${{ 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 }}"
}
}'
Next steps¶
- Core Concepts: deeper dive into Bundles, Pipelines, PolicyGates, and health adapters
- Multi-Cluster Fleet Example: parallel prod regions with Argo Rollouts canary
- Design Document: full technical design