Full notes: ARC Kubernetes Mode - Lifecycle Hooks Deep Dive →

Key Concepts

What “Lifecycle Hooks” Means Here

These are NOT Kubernetes postStart/preStop hooks. They are a GitHub Actions Runner feature — an internal plugin system called “container hooks.” At specific points in a job’s lifecycle, the runner agent calls Node.js scripts (index.js) baked into the runner image.

HookWhen It FiresWhat It Does
prepareJobBefore any step runsCreates the job pod, tar+streams workspace TO it
runScriptStepFor each run: stepExecs the script inside a container in the job pod
runContainerStepFor each uses: docker:// stepRuns a container step in the job pod
cleanupJobAfter all stepsTar+streams workspace BACK, deletes the job pod

API Access and Authentication

The runner pod’s service account provides all K8s API access. The ServiceAccount (created by the Helm chart) has a Role with permissions to create/delete pods, create pods/exec, get pods/log, and manage secrets. The auto-mounted token at /var/run/secrets/kubernetes.io/serviceaccount/token authenticates calls. The hook scripts use the @kubernetes/client-node npm package (not the kubectl CLI) to talk to the K8s API programmatically.

How Data Moves Between Pods (No Shared Volumes)

There is no shared volume — two pods with emptyDir cannot see each other’s data. Instead, data moves via tar-over-websocket through the K8s API:

prepareJob (Runner Job Pod): Runner creates the job pod via POST /api/v1/pods, waits for it to reach Running, then execs tar xzf - in the job pod and streams the workspace archive over the websocket stdin. The job pod extracts it into _work/.

runScriptStep: Runner execs each run: step inside the job pod via POST /pods/{job-pod}/exec, streams stdout/stderr back, and uploads logs to the GitHub API.

cleanupJob (Job Pod Runner): Runner execs tar czf - in the job pod, streams the workspace archive back to itself via websocket stdout, extracts locally, then deletes the job pod. After that, the runner uploads artifacts/cache to GitHub from its local _work/.

Runner Pod                        Job Pod
  |                                  |
  |-- prepareJob: create pod ------->|
  |-- tar czf - | tar xzf - ------->|  (workspace transfer)
  |                                  |
  |-- runScriptStep: exec ---------->|  (each run: step)
  |<--- stdout/stderr ---------------|
  |                                  |
  |<-- tar czf - | tar xzf - -------|  (workspace back)
  |-- cleanupJob: delete pod ------->X

The Tar Pipe Explained

Conceptually equivalent to kubectl exec runner-pod -- tar czf - -C /home/runner/_work . | kubectl exec -i job-pod -- tar xzf - -C /home/runner/_work. The left side creates a gzip tar of _work/ and streams it to stdout (f - means stdout). The pipe feeds stdout as stdin to the right side. The right side extracts from stdin (f - means stdin) into _work/. The -i flag on kubectl exec is critical — without it, the tar stream cannot enter the pod. No intermediate file ever lands on disk; it is a pure streaming copy through the K8s API websocket.

RBAC Permissions

The Helm chart creates a Role for the runner’s ServiceAccount with minimal permissions:

ResourceVerbs
podscreate, delete, get, list, watch
pods/execcreate
pods/logget
secretscreate, delete, get

Where the Hook Code Lives

Inside the runner image at /home/runner/k8s/: index.js (entry point, ACTIONS_RUNNER_CONTAINER_HOOKS points here), prepareJob.js, runScriptStep.js, runContainerStep.js, cleanupJob.js, and lifecycle-hooks/container-hook-template.yaml (pod spec template). All baked into ghcr.io/actions/actions-runner — nothing to build or provide yourself.

Quick Reference

Auth chain: Runner pod ServiceAccount RBAC Role auto-mounted token @kubernetes/client-node npm package K8s API

Data transfer: tar gzip stream over K8s API websocket (no shared volumes, no intermediate files on disk)

Hook code: /home/runner/k8s/index.js (baked into official runner image)

Key Takeaways

  • The runner pod orchestrates everything — it creates, execs into, and deletes the job pod
  • Data transfer is tar-over-websocket through the K8s API, no shared volumes or intermediate files on disk
  • RBAC permissions are minimal: create/delete pods, exec, get logs, manage secrets
  • The hooks use @kubernetes/client-node npm package, not the kubectl CLI binary
  • All hook scripts ship inside the official runner image — nothing to build or provide yourself
  • The -i flag on exec is critical for the tar stream to enter the receiving pod