PR Preview Clusters with ksail-cluster Action
KSail can provision short-lived, per-PR preview Kubernetes clusters in GitHub Actions using:
ksail-clustercomposite action — one-step cluster provisioning with caching and GitOps CI supportvalidateinput — validates manifests before cluster creationdelete: trueinput — always-on cluster cleanup, even on failure
This pattern positions KSail as a lightweight alternative to tools like mirrord-preview for teams who want GitOps-native preview environments backed by real Kubernetes clusters.
Minimal Example
Section titled “Minimal Example”The following workflow creates an ephemeral cluster on every pull request, validates manifests, deploys via GitOps, runs smoke tests, then tears the cluster down:
name: PR Preview Cluster
on: pull_request: branches: [main]
jobs: preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Provision preview cluster id: cluster uses: devantler-tech/ksail/.github/actions/ksail-cluster@main with: validate: "true" # validate manifests before cluster creation push: "true" # push manifests to OCI registry for GitOps reconcile: "true" # trigger GitOps reconcile and wait delete: "true" # delete cluster when done (runs even on failure)
- name: Run smoke tests env: KUBECONFIG: ${{ steps.cluster.outputs.kubeconfig }} run: | kubectl get pods -A # ... your smoke/integration tests hereComplete GitOps CI Workflow
Section titled “Complete GitOps CI Workflow”For teams using Flux or ArgoCD, the full GitOps preview pattern includes manifest validation, OCI push, and reconcile:
name: PR Preview Cluster
on: pull_request: branches: [main] paths: - "k8s/**" - "ksail.yaml"
jobs: preview: runs-on: ubuntu-latest permissions: contents: read packages: write # required if pushing to GHCR
steps: - uses: actions/checkout@v4
- name: Provision preview cluster with GitOps id: cluster uses: devantler-tech/ksail/.github/actions/ksail-cluster@main with: distribution: K3s # K3s starts fastest; swap for Vanilla, Talos, or VCluster validate: "true" # fail fast if manifests are invalid push: "true" # push manifests to the cluster's local OCI registry reconcile: "true" # wait for Flux/ArgoCD to reconcile successfully delete: "true" # always clean up sops-age-key: ${{ secrets.SOPS_AGE_KEY }} # optional: decrypt SOPS secrets
- name: Run integration tests env: KUBECONFIG: ${{ steps.cluster.outputs.kubeconfig }} run: | # Tests run against the fully reconciled cluster kubectl get pods -n my-app -l app=my-app curl -f http://localhost:8080/healthz || exit 1Multi-Environment PR Preview
Section titled “Multi-Environment PR Preview”Combine --config with the preview pattern to test against a staging-like config:
- name: Provision staging-like preview cluster id: cluster uses: devantler-tech/ksail/.github/actions/ksail-cluster@main with: config: ksail.staging.yaml # use staging config (Talos + Cilium + Kyverno) init: "false" # config already exists in the repo, skip reinit validate: "true" push: "true" reconcile: "true" delete: "true"See Multi-Environment Workflows for the full --config guide.
Failure Path: Manual Cleanup
Section titled “Failure Path: Manual Cleanup”When running without delete: true (e.g., you want to keep the cluster alive for debugging a failed CI run), add an explicit cleanup step using if: always():
steps: - uses: actions/checkout@v4
- name: Provision preview cluster id: cluster uses: devantler-tech/ksail/.github/actions/ksail-cluster@main with: push: "true" reconcile: "true" # NOTE: delete is intentionally omitted here — handled below
- name: Run smoke tests env: KUBECONFIG: ${{ steps.cluster.outputs.kubeconfig }} run: ./scripts/smoke-test.sh
- name: Delete preview cluster if: always() # runs even when smoke tests fail run: ksail cluster deleteKubeconfig Access
Section titled “Kubeconfig Access”The action outputs the kubeconfig file path in steps.<id>.outputs.kubeconfig. Pass it to subsequent steps via KUBECONFIG:
- name: Deploy additional resources env: KUBECONFIG: ${{ steps.cluster.outputs.kubeconfig }} run: | kubectl apply -f manifests/smoke-test-job.yaml kubectl wait job/smoke-test --for=condition=complete --timeout=120s kubectl logs job/smoke-testYou can also set it globally for all subsequent steps:
- name: Set KUBECONFIG globally run: echo "KUBECONFIG=${{ steps.cluster.outputs.kubeconfig }}" >> "$GITHUB_ENV"Action Inputs Reference
Section titled “Action Inputs Reference”| Input | Default | Description |
|---|---|---|
distribution | Vanilla | Cluster distribution (Vanilla, K3s, Talos, VCluster) |
provider | Docker | Infrastructure provider (Docker, Hetzner) |
config | "" | Path to alternate ksail.yaml (e.g. ksail.staging.yaml) |
validate | false | Run ksail workload validate before cluster creation (after init, when enabled) |
push | false | Run ksail workload push after creation |
reconcile | false | Run ksail workload reconcile after push |
delete | false | Delete cluster at the end (if: always()) |
args | "" | Extra args passed to cluster init (and cluster create when init is skipped) |
init | true | Run ksail cluster init before create |
install | true | Install KSail from releases (set to false if already in PATH) |
cache | true | Cache Helm charts and mirror registry volumes |
ksail-version | latest | KSail version to install (e.g. 5.55.0) |
sops-age-key | "" | SOPS Age private key for secret decryption |
hcloud-token | "" | Hetzner Cloud API token (required when provider is Hetzner) |
hosts-file | "" | Path to a hosts file to append to /etc/hosts |
root-ca-cert-file | "" | Path to a root CA certificate to install as trusted |
Action output: kubeconfig — path to the kubeconfig file for the provisioned cluster.
-
K3s starts fastest — use
distribution: K3sfor PRs where spin-up time matters. -
Pin the action version using a tag (e.g.
@v5.73.0) for reproducible CI, or@mainfor bleeding-edge. -
Cache hits speed up CI significantly — the
cache: truedefault restores Helm charts and mirror registry volumes, avoiding repeated Docker Hub pulls. -
Combine with path filters (
paths:inon.pull_request) to skip the preview cluster when only non-Kubernetes files changed. -
Validate early — set
validate: trueto fail fast on invalid manifests before spending time on cluster provisioning. -
Export logs before deletion — if tests fail, capture pod logs before the cluster is deleted:
- name: Capture logs on failureif: failure()env:KUBECONFIG: ${{ steps.cluster.outputs.kubeconfig }}run: kubectl logs -n my-app -l app=my-app --tail=100 || true
Related Guides
Section titled “Related Guides”- Ephemeral Clusters (
--ttl) — full TTL guide for local and CI use cases - Multi-Environment Workflows — using
--configfor dev/staging/prod - Testing in CI/CD — broader CI testing patterns with KSail