Custom Promotion Steps¶
Custom steps let you inject arbitrary HTTP webhook calls into the promotion sequence. Any step uses: value that is not a built-in step name is dispatched as an HTTP POST to the configured webhook URL.
Quick start¶
- Write a small HTTP server that implements the contract below.
- Add the step to your Pipeline environment's
steps:list. - Deploy the server to your cluster.
# Pipeline snippet
spec:
environments:
- name: prod
steps:
- uses: my-team/version-gate # non-built-in → custom step
webhook:
url: http://custom-step-server.my-ns.svc.cluster.local/step
timeoutSeconds: 30
secretRef:
name: custom-step-token # optional: K8s Secret with Authorization header
- uses: git-clone # built-in steps follow
- uses: kustomize-set-image
- uses: git-commit
- uses: git-push
- uses: open-pr
- uses: wait-for-merge
- uses: health-check
Contract¶
Request¶
POST <webhook.url>
Headers: - Content-Type: application/json - Authorization: <value from secretRef> (only if secretRef is configured)
Body:
{
"bundle": {
"type": "image",
"images": [{"repository": "ghcr.io/myorg/app", "tag": "v2.0.0"}]
},
"environment": "prod",
"inputs": {
"webhook.url": "http://...",
"webhook.timeoutSeconds": "30"
},
"outputs_so_far": {
"branch": "kardinal/my-app-v2-0-0/prod"
}
}
Response¶
| Field | Type | Required | Description |
|---|---|---|---|
result | "pass" | "fail" | Yes | "pass" continues the pipeline; "fail" marks the PromotionStep as Failed |
outputs | map[string]string | No | Key/value pairs merged into PromotionStep.status.outputs and available to subsequent steps |
message | string | No | Human-readable explanation shown by kardinal explain |
Status codes¶
| Status | Behaviour |
|---|---|
| 2xx | Parse response body, use result field |
| 4xx | Mark step Failed immediately (no retry) |
| 5xx | Retry up to 3 times with 30-second backoff, then fail |
| Timeout | If the server does not respond within timeoutSeconds, mark step Failed |
Pipeline steps: field¶
When spec.environments[*].steps is set, it replaces the default step sequence for that environment. List both custom and built-in steps in the order you want them to execute.
If steps: is not set, the default sequence is used (see Built-in steps).
Built-in steps¶
| Step name | Description |
|---|---|
git-clone | Clone the GitOps repository |
kustomize-set-image | Update image tag in kustomization.yaml |
helm-set-image | Update image tag in values.yaml |
kustomize-build | Render manifests (layout: branch) |
config-merge | Apply config-only overlay (type: config bundles) |
git-commit | Commit changes to a promotion branch |
git-push | Push the promotion branch |
open-pr | Open a pull request |
wait-for-merge | Poll until the PR is merged |
health-check | Verify deployment health via the configured health adapter |
Authentication¶
The webhook can be authenticated with a Kubernetes Secret containing the Authorization header value.
webhook:
url: https://my-server/step
secretRef:
name: my-custom-step-secret
namespace: default # optional; defaults to the Pipeline namespace
The Secret must contain an Authorization key:
kubectl create secret generic my-custom-step-secret \
--from-literal=Authorization="Bearer my-token"
The kardinal controller reads the Secret at execution time and injects the header into the POST request.
Idempotency requirement¶
The controller may call your webhook multiple times for the same step execution (for example, after a controller restart). Your server must be idempotent: calling it twice with the same request must produce the same result.
A common pattern: use the bundle.provenance.commitSHA and environment from the request body as a cache key to detect and deduplicate re-runs.
Step outputs and data flow¶
Outputs from a custom step are merged into PromotionStep.status.outputs and passed to all subsequent steps (including built-in steps) via outputs_so_far.
Example: a security scan step returns {"scan_report_url": "https://..."}. A subsequent built-in open-pr step will include that URL in the PR body if it is present in outputs_so_far.
Example server¶
A complete example custom step server is in examples/custom-step/.
# Run locally
go run examples/custom-step/server.go
# Apply the example Pipeline
kubectl apply -f examples/custom-step/pipeline.yaml
The example implements a version gate: pre-release image tags (containing alpha, beta, rc, snapshot, or dev) are rejected in the prod environment.
Troubleshooting¶
Step stays in Running state¶
Check that your server is reachable from the controller pod:
kubectl exec -n kardinal-system deploy/kardinal-controller -- \
curl -s http://custom-step-server.my-ns.svc.cluster.local/healthz
Step fails with "missing input webhook.url"¶
The webhook.url field in the Pipeline step spec is required for custom steps. Verify your Pipeline YAML contains the webhook: block under the custom step.