Actions runner smoke runbook
This runbook validates the runner-facing Actions path. shithubd-runner
now claims jobs and executes containerized run: steps through Docker or
Podman. The curl flow below remains useful for token/replay debugging.
For host provisioning and the systemd/Ansible path, see runner-deploy.md.
Prereqs:
- Database migrations are current through
0053_runner_jwt_used.sql. SHITHUB_TOTP_KEYorauth.totp_key_b64is set on the web process.- Object storage is configured if testing artifact upload.
- Docker or Podman is installed on the runner host.
- A repo has a workflow under
.shithub/workflows/*.ymlwithruns-on: ubuntu-latest, and a push/dispatch has enqueued a run. S41d PR1 supportsrun:steps; checkout and artifact aliases land in the following S41d slices.
runs-on is a runner-label selector, not a hard-coded image name.
A workflow that says runs-on: ubuntu-latest can be claimed by any
runner advertising the ubuntu-latest label. The container image is
selected by the runner host's engine.default_image setting; the
reproducible Nix-built image is the default, but operators can point it
at another OCI image when they need closer Ubuntu parity.
Register a runner:
shithubd admin runner register \
--name runner-1 \
--labels self-hosted,linux,ubuntu-latest \
--capacity 1
Save the printed token:
export RUNNER_TOKEN='<printed-token>'
export BASE='https://shithub.example'
Run the binary:
shithubd-runner run \
--server-url "$BASE" \
--token "$RUNNER_TOKEN" \
--labels self-hosted,linux,ubuntu-latest \
--workspace-root /var/lib/shithubd-runner/workspaces
Equivalent config file:
[server]
base_url = "https://shithub.example"
[runner]
token = "<printed-token>"
labels = ["self-hosted", "linux", "ubuntu-latest"]
capacity = 1
poll_interval = "5s"
workspace_root = "/var/lib/shithubd-runner/workspaces"
workspace_ttl = "24h"
network_allowlist = [
"api.github.com",
"auth.docker.io",
"codeload.github.com",
"github.com",
"objects.githubusercontent.com",
"production.cloudflare.docker.com",
"registry-1.docker.io",
"*.githubusercontent.com",
]
[engine]
kind = "docker"
default_image = "ghcr.io/shithub/runner-nix:1.0"
network = "bridge"
memory = "2g"
cpus = "2"
seccomp_profile = "/etc/shithubd-runner/seccomp.json"
user = "65534:65534"
pids_limit = 512
dns_servers = []
The config path defaults to /etc/shithubd-runner/config.toml.
Environment variables use the SHITHUB_RUNNER_ prefix, for example
SHITHUB_RUNNER_TOKEN or SHITHUB_RUNNER_SERVER__BASE_URL.
Curl token smoke
Claim a job:
curl -fsS "$BASE/api/v1/runners/heartbeat" \
-H "Authorization: Bearer $RUNNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"labels":["self-hosted","linux","ubuntu-latest"],"capacity":1}' \
| tee /tmp/shithub-claim.json
Extract the job token and id:
export JOB_ID="$(jq -r '.job.id' /tmp/shithub-claim.json)"
export JOB_TOKEN="$(jq -r '.token' /tmp/shithub-claim.json)"
Append a log chunk:
curl -fsS "$BASE/api/v1/jobs/$JOB_ID/logs" \
-H "Authorization: Bearer $JOB_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"seq\":0,\"chunk\":\"$(printf 'hello from curl\n' | base64)\"}" \
| tee /tmp/shithub-log.json
export JOB_TOKEN="$(jq -r '.next_token' /tmp/shithub-log.json)"
Complete the job:
curl -fsS "$BASE/api/v1/jobs/$JOB_ID/status" \
-H "Authorization: Bearer $JOB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"completed","conclusion":"success"}'
Replay check: reusing the log token after the log call must fail with
401 because its jti is already present in runner_jwt_used.
curl -i "$BASE/api/v1/jobs/$JOB_ID/status" \
-H "Authorization: Bearer $(jq -r '.next_token' /tmp/shithub-log.json)" \
-H "Content-Type: application/json" \
-d '{"status":"running"}'
Expected results:
workflow_jobs.status = completedand conclusionsuccess.- The parent
workflow_runsrow rolls up to completed/success when all jobs are terminal. - The PR Checks tab shows the matching check run as success.
/metricsincludes runner registration, heartbeat, and JWT counters.
View source
| 1 | # Actions runner smoke runbook |
| 2 | |
| 3 | This runbook validates the runner-facing Actions path. `shithubd-runner` |
| 4 | now claims jobs and executes containerized `run:` steps through Docker or |
| 5 | Podman. The curl flow below remains useful for token/replay debugging. |
| 6 | |
| 7 | For host provisioning and the systemd/Ansible path, see |
| 8 | [runner-deploy.md](./runner-deploy.md). |
| 9 | |
| 10 | Prereqs: |
| 11 | |
| 12 | - Database migrations are current through `0053_runner_jwt_used.sql`. |
| 13 | - `SHITHUB_TOTP_KEY` or `auth.totp_key_b64` is set on the web process. |
| 14 | - Object storage is configured if testing artifact upload. |
| 15 | - Docker or Podman is installed on the runner host. |
| 16 | - A repo has a workflow under `.shithub/workflows/*.yml` with |
| 17 | `runs-on: ubuntu-latest`, and a push/dispatch has enqueued a run. |
| 18 | S41d PR1 supports `run:` steps; checkout and artifact aliases land in |
| 19 | the following S41d slices. |
| 20 | |
| 21 | `runs-on` is a runner-label selector, not a hard-coded image name. |
| 22 | A workflow that says `runs-on: ubuntu-latest` can be claimed by any |
| 23 | runner advertising the `ubuntu-latest` label. The container image is |
| 24 | selected by the runner host's `engine.default_image` setting; the |
| 25 | reproducible Nix-built image is the default, but operators can point it |
| 26 | at another OCI image when they need closer Ubuntu parity. |
| 27 | |
| 28 | Register a runner: |
| 29 | |
| 30 | ```sh |
| 31 | shithubd admin runner register \ |
| 32 | --name runner-1 \ |
| 33 | --labels self-hosted,linux,ubuntu-latest \ |
| 34 | --capacity 1 |
| 35 | ``` |
| 36 | |
| 37 | Save the printed token: |
| 38 | |
| 39 | ```sh |
| 40 | export RUNNER_TOKEN='<printed-token>' |
| 41 | export BASE='https://shithub.example' |
| 42 | ``` |
| 43 | |
| 44 | Run the binary: |
| 45 | |
| 46 | ```sh |
| 47 | shithubd-runner run \ |
| 48 | --server-url "$BASE" \ |
| 49 | --token "$RUNNER_TOKEN" \ |
| 50 | --labels self-hosted,linux,ubuntu-latest \ |
| 51 | --workspace-root /var/lib/shithubd-runner/workspaces |
| 52 | ``` |
| 53 | |
| 54 | Equivalent config file: |
| 55 | |
| 56 | ```toml |
| 57 | [server] |
| 58 | base_url = "https://shithub.example" |
| 59 | |
| 60 | [runner] |
| 61 | token = "<printed-token>" |
| 62 | labels = ["self-hosted", "linux", "ubuntu-latest"] |
| 63 | capacity = 1 |
| 64 | poll_interval = "5s" |
| 65 | workspace_root = "/var/lib/shithubd-runner/workspaces" |
| 66 | workspace_ttl = "24h" |
| 67 | network_allowlist = [ |
| 68 | "api.github.com", |
| 69 | "auth.docker.io", |
| 70 | "codeload.github.com", |
| 71 | "github.com", |
| 72 | "objects.githubusercontent.com", |
| 73 | "production.cloudflare.docker.com", |
| 74 | "registry-1.docker.io", |
| 75 | "*.githubusercontent.com", |
| 76 | ] |
| 77 | |
| 78 | [engine] |
| 79 | kind = "docker" |
| 80 | default_image = "ghcr.io/shithub/runner-nix:1.0" |
| 81 | network = "bridge" |
| 82 | memory = "2g" |
| 83 | cpus = "2" |
| 84 | seccomp_profile = "/etc/shithubd-runner/seccomp.json" |
| 85 | user = "65534:65534" |
| 86 | pids_limit = 512 |
| 87 | dns_servers = [] |
| 88 | ``` |
| 89 | |
| 90 | The config path defaults to `/etc/shithubd-runner/config.toml`. |
| 91 | Environment variables use the `SHITHUB_RUNNER_` prefix, for example |
| 92 | `SHITHUB_RUNNER_TOKEN` or `SHITHUB_RUNNER_SERVER__BASE_URL`. |
| 93 | |
| 94 | ## Curl token smoke |
| 95 | |
| 96 | Claim a job: |
| 97 | |
| 98 | ```sh |
| 99 | curl -fsS "$BASE/api/v1/runners/heartbeat" \ |
| 100 | -H "Authorization: Bearer $RUNNER_TOKEN" \ |
| 101 | -H "Content-Type: application/json" \ |
| 102 | -d '{"labels":["self-hosted","linux","ubuntu-latest"],"capacity":1}' \ |
| 103 | | tee /tmp/shithub-claim.json |
| 104 | ``` |
| 105 | |
| 106 | Extract the job token and id: |
| 107 | |
| 108 | ```sh |
| 109 | export JOB_ID="$(jq -r '.job.id' /tmp/shithub-claim.json)" |
| 110 | export JOB_TOKEN="$(jq -r '.token' /tmp/shithub-claim.json)" |
| 111 | ``` |
| 112 | |
| 113 | Append a log chunk: |
| 114 | |
| 115 | ```sh |
| 116 | curl -fsS "$BASE/api/v1/jobs/$JOB_ID/logs" \ |
| 117 | -H "Authorization: Bearer $JOB_TOKEN" \ |
| 118 | -H "Content-Type: application/json" \ |
| 119 | -d "{\"seq\":0,\"chunk\":\"$(printf 'hello from curl\n' | base64)\"}" \ |
| 120 | | tee /tmp/shithub-log.json |
| 121 | |
| 122 | export JOB_TOKEN="$(jq -r '.next_token' /tmp/shithub-log.json)" |
| 123 | ``` |
| 124 | |
| 125 | Complete the job: |
| 126 | |
| 127 | ```sh |
| 128 | curl -fsS "$BASE/api/v1/jobs/$JOB_ID/status" \ |
| 129 | -H "Authorization: Bearer $JOB_TOKEN" \ |
| 130 | -H "Content-Type: application/json" \ |
| 131 | -d '{"status":"completed","conclusion":"success"}' |
| 132 | ``` |
| 133 | |
| 134 | Replay check: reusing the log token after the log call must fail with |
| 135 | 401 because its `jti` is already present in `runner_jwt_used`. |
| 136 | |
| 137 | ```sh |
| 138 | curl -i "$BASE/api/v1/jobs/$JOB_ID/status" \ |
| 139 | -H "Authorization: Bearer $(jq -r '.next_token' /tmp/shithub-log.json)" \ |
| 140 | -H "Content-Type: application/json" \ |
| 141 | -d '{"status":"running"}' |
| 142 | ``` |
| 143 | |
| 144 | Expected results: |
| 145 | |
| 146 | - `workflow_jobs.status = completed` and conclusion `success`. |
| 147 | - The parent `workflow_runs` row rolls up to completed/success when all |
| 148 | jobs are terminal. |
| 149 | - The PR Checks tab shows the matching check run as success. |
| 150 | - `/metrics` includes runner registration, heartbeat, and JWT counters. |