GitHub Actions
Automate cluster provisioning and configuration sync with GitHub Actions.
The quick path — the reusable workflow
lok8s ships a ready-made workflow that provisions a committed cluster and, when the cluster opts into kubehz visibility (spec.kubehz.access is registered or managed), registers it and prints the claim fingerprint to the job summary (the claim key lo uploads to your Hetzner project; see Claiming). It needs exactly one secret:
# .github/workflows/spinup.yml
name: Spin up cluster
on: workflow_dispatch
jobs:
spinup:
uses: kernpilot/lok8s/.github/workflows/spinup.yml@main
with:
domain: my-cluster.example.com
secrets:
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}@main is a moving reference
This calls the reusable workflow at @main, so it changes when lok8s changes. That is convenient while lok8s is young, but it means a run can behave differently from the last one without you changing anything. Substituting a commit SHA (spinup.yml@<sha>) pins the behaviour you tested and is the safer choice for anything that provisions real infrastructure. Resolve the SHA main points at right now, then paste it into uses::
gh api repos/kernpilot/lok8s/commits/main --jq .shaPinning to the current release (@v0.2.0, August 2026) is not recommended yet: the workflow has gained claim-key provisioning since that tag, so the release is behind what this page documents.
The cluster.lok8s.yaml for the domain must be committed under clusters/<domain>/ in your repo. No platform token is needed: ownership is proven later, interactively, when you claim the cluster.
Prefer this unless you need custom steps; the workflows below show the full manual setup.
Writing your own workflows
Prerequisites
- A repository containing your
clusters/<domain>/cluster.lok8s.yaml - GitHub repository secrets configured (below)
Required secrets
Add these to your repository settings under Settings > Secrets and variables > Actions:
| Secret | Description |
|---|---|
HCLOUD_TOKEN | Hetzner Cloud API token |
SSH_PRIVATE_KEY | SSH private key matching sshPrivateKey in your provider descriptor (node access) |
KUBECONFIG | Base64-encoded kubeconfig (for sync workflows) |
Installing lok8s in CI
lok8s is distributed as a b environment, so a runner needs b first, then the pinned toolchain. If your repo already commits .bin/b.yaml (the lok8s installer creates it), b install alone restores the exact same toolchain; skip the b env add line.
- name: Install b (binary manager)
run: |
curl -fsSL https://raw.githubusercontent.com/fentas/b/v4.18.4/install.sh \
-o /tmp/b-install.sh
B_INSTALL_DIR="$HOME/.local/bin" bash /tmp/b-install.sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install the lok8s toolchain
env:
# avoids GitHub API rate limits on binary downloads
GITHUB_TOKEN: ${{ github.token }}
run: |
b env add github.com/kernpilot/lok8s#kubeone
b install
echo "$PWD/.bin" >> "$GITHUB_PATH"Provision workflow
Create .github/workflows/provision.yml:
name: Provision Cluster
on:
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: true
default: 'provision'
type: choice
options:
- provision
- destroy
jobs:
provision:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install b (binary manager)
run: |
curl -fsSL https://raw.githubusercontent.com/fentas/b/v4.18.4/install.sh \
-o /tmp/b-install.sh
B_INSTALL_DIR="$HOME/.local/bin" bash /tmp/b-install.sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install the lok8s toolchain
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
b env add github.com/kernpilot/lok8s#kubeone
b install
echo "$PWD/.bin" >> "$GITHUB_PATH"
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
- name: Run lo
env:
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
run: lo --domain my-cluster.example.com ${{ inputs.action }}The dispatch input selects lo provision or lo destroy, so one workflow covers spin-up and teardown.
Sync workflow
Create .github/workflows/sync.yml to apply configuration changes on push:
name: Sync Cluster
on:
push:
branches: [main]
paths:
- 'clusters/**'
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install b (binary manager)
run: |
curl -fsSL https://raw.githubusercontent.com/fentas/b/v4.18.4/install.sh \
-o /tmp/b-install.sh
B_INSTALL_DIR="$HOME/.local/bin" bash /tmp/b-install.sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install the lok8s toolchain
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
b env add github.com/kernpilot/lok8s#kubeone
b install
echo "$PWD/.bin" >> "$GITHUB_PATH"
- name: Setup kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
- name: Reconcile infrastructure
env:
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
run: lo --domain my-cluster.example.com provision
- name: Render the manifests
run: lo --domain my-cluster.example.com build
- name: Deploy platform
run: lo --domain my-cluster.example.com deploylo provision is idempotent: re-running it reconciles the cluster to your spec. lo build renders your services and addons into clusters/<domain>/artifacts.yaml, and lo deploy applies that file. lo deploy refuses to run before lo build has produced it.
Next steps
- Registration: register with the kubehz dashboard
- KubeOne: provisioner details
- CAPI: Cluster API provisioner
Doc status
| Aspect | Detail |
|---|---|
| Last reviewed | 5 September 2026 |