This guide shows how to apply custom workload labels to GitHub Actions self-hosted runners so CAST.AI can track and attribute costs per workflow/job.
When using Actions Runner Controller (ARC) on Kubernetes, job pods are ephemeral. By injecting custom labels, CAST.AI can group costs by repository, workflow, job, or branch.
GitHub Actions Workflow
│
▼
ARC Controller
│
▼
Runner Pod (listener)
│
┌────┴────┐
│ pre-run │ ← Injects GitHub env vars into pod template
│ hook │
└────┬────┘
▼
Job Pod with custom labels
│
▼
CAST.AI tracks costs via label:
workloads.cast.ai/custom-workload
This template defines the labels applied to each job pod:
apiVersion: v1
kind: ConfigMap
metadata:
name: k8s-runner-template
namespace: actions-runner-system
data:
template: |
metadata:
labels:
workloads.cast.ai/custom-workload: "$GITHUB_JOB-$GITHUB_REPOSITORY"The label workloads.cast.ai/custom-workload is recognized by CAST.AI for cost allocation.
The hook sanitizes GitHub environment variables (removing invalid characters) and substitutes them into the pod template:
apiVersion: v1
kind: ConfigMap
metadata:
name: runner-hooks
namespace: actions-runner-system
data:
pre-run.sh: |
#!/bin/sh
# Sanitize values for Kubernetes labels (alphanumeric, -, _, . only)
sanitize_label() {
echo "$1" | sed 's/[^a-zA-Z0-9._-]/_/g'
}
SAFE_JOB=$(sanitize_label "$GITHUB_JOB")
SAFE_REPOSITORY=$(sanitize_label "$GITHUB_REPOSITORY")
sed -e 's|$GITHUB_JOB|'"$SAFE_JOB"'|g' \
-e 's|$GITHUB_REPOSITORY|'"$SAFE_REPOSITORY"'|g' \
"$ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE_SRC" > "$ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE"Mount the template and hook into the runner pod:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: arc-runner-set
namespace: actions-runner-system
spec:
values:
githubConfigUrl: "https://github.com/YOUR_ORG"
githubConfigSecret: controller-manager
containerMode:
type: "kubernetes-novolume"
template:
spec:
containers:
- name: runner
image: ghcr.io/actions/actions-runner:latest
command: ["/home/runner/run.sh"]
env:
- name: ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE_SRC
value: /home/runner/pod-template-src/template
- name: ACTIONS_RUNNER_CONTAINER_HOOK_TEMPLATE
value: /home/runner/pod-template/template
- name: ACTIONS_RUNNER_HOOK_JOB_STARTED
value: /home/runner/hooks/pre-run.sh
volumeMounts:
- name: job-pod-template
mountPath: /home/runner/pod-template
- name: pod-template-source
mountPath: /home/runner/pod-template-src
- name: runner-hooks
mountPath: /home/runner/hooks
volumes:
- name: job-pod-template
emptyDir: {}
- name: pod-template-source
configMap:
name: k8s-runner-template
- name: runner-hooks
configMap:
name: runner-hooksYou can include any of these in your labels:
| Variable | Example Value |
|---|---|
$GITHUB_JOB |
build |
$GITHUB_REPOSITORY |
myorg/myrepo |
$GITHUB_WORKFLOW |
CI Pipeline |
$GITHUB_REF_NAME |
main |
$GITHUB_SHA |
abc123... |
$GITHUB_RUN_ID |
12345678 |
Once labels are applied, use the CAST.AI console or API to filter workload costs:
- Navigate to Cost Monitoring > Workloads
- Group by the
workloads.cast.ai/custom-workloadlabel - View costs per repository, job, or workflow
Labels not appearing?
- Verify the pre-run hook has execute permissions
- Check that
ACTIONS_RUNNER_HOOK_JOB_STARTEDenv var is set - Inspect the generated template:
cat /home/runner/pod-template/template
Invalid label characters?
- Kubernetes labels only allow:
a-z,A-Z,0-9,-,_,. - The sanitize function replaces invalid chars with
_