loki-mode 9.17.2 → 9.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/loki +439 -2
- package/autonomy/run.sh +215 -11
- package/autonomy/trigger-server.py +518 -17
- package/bin/loki +7 -1
- package/dashboard/__init__.py +1 -1
- package/docs/COMPETITIVE-SCORECARD.md +38 -0
- package/docs/COMPETITOR-DEPLOYMENT-MODELS.md +475 -0
- package/docs/DEPLOYMENT.md +542 -0
- package/docs/STALE-STATE-AUDIT.md +174 -0
- package/docs/VERIFICATION-COST.md +31 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
# Deploying Loki Mode as a build service
|
|
2
|
+
|
|
3
|
+
You deployed it. Now what starts a build?
|
|
4
|
+
|
|
5
|
+
That is the question this document answers, because it is the one a fresh
|
|
6
|
+
deployment does not answer for you. The pods are running, `/health` returns
|
|
7
|
+
200, and nothing is happening. Three things can start a build, and you have to
|
|
8
|
+
set up at least one of them deliberately.
|
|
9
|
+
|
|
10
|
+
- [The shape of a deployment](#the-shape-of-a-deployment)
|
|
11
|
+
- [Path 1: a GitHub webhook](#path-1-a-github-webhook)
|
|
12
|
+
- [Path 2: manual enqueue](#path-2-manual-enqueue)
|
|
13
|
+
- [Path 3: the API endpoint](#path-3-the-api-endpoint)
|
|
14
|
+
- [Triggers that do not exist yet](#triggers-that-do-not-exist-yet)
|
|
15
|
+
- [Kubernetes (Helm)](#kubernetes-helm)
|
|
16
|
+
- [Single node (Docker Compose)](#single-node-docker-compose)
|
|
17
|
+
- [Things that will bite you](#things-that-will-bite-you)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## The shape of a deployment
|
|
22
|
+
|
|
23
|
+
Two processes and a queue.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
GitHub --webhook--> receiver --enqueue--> Redis --pop--> worker --> loki start
|
|
27
|
+
(trigger-server.py) (queue-consumer.sh)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The **receiver** (`autonomy/trigger-server.py`) validates an HMAC signature and
|
|
31
|
+
pushes an item onto a queue. It never runs a build, and it holds no provider
|
|
32
|
+
credential. If someone compromises the receiver, they cannot spend your
|
|
33
|
+
provider budget.
|
|
34
|
+
|
|
35
|
+
The **worker** (`autonomy/queue-consumer.sh`) pulls one item at a time and runs
|
|
36
|
+
`loki start <spec>`. This is where builds happen, where the money is spent, and
|
|
37
|
+
where your source code gets checked out. Worker replica count is the scaling
|
|
38
|
+
knob.
|
|
39
|
+
|
|
40
|
+
Separating them is what lets a webhook storm cost you a full queue instead of a
|
|
41
|
+
hundred concurrent builds.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Path 1: a GitHub webhook
|
|
46
|
+
|
|
47
|
+
This is the default path and the one most deployments want.
|
|
48
|
+
|
|
49
|
+
### 1. Get a secret into the deployment
|
|
50
|
+
|
|
51
|
+
The receiver requires a webhook secret. Without one it starts (so `/health`
|
|
52
|
+
stays up for your probes) and rejects **every** webhook with 503. It never
|
|
53
|
+
silently accepts unauthenticated builds. Generate one:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
openssl rand -hex 32
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Helm:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
helm upgrade --install loki helm/loki-mode \
|
|
63
|
+
--set secrets.githubWebhookSecret='<that value>' \
|
|
64
|
+
--set secrets.anthropicApiKey='<your key>'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Better, for anything real: set `secrets.create=false` and
|
|
68
|
+
`secrets.existingSecret=<name>` and manage the Secret with sealed-secrets,
|
|
69
|
+
External Secrets Operator or a Vault injector. Values passed with `--set` land
|
|
70
|
+
in Helm release history and often in a git-tracked values file.
|
|
71
|
+
|
|
72
|
+
Compose: put both in `.env` next to `docker-compose.yml`.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
GITHUB_WEBHOOK_SECRET=<that value>
|
|
76
|
+
ANTHROPIC_API_KEY=<your key>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Expose the receiver to GitHub
|
|
80
|
+
|
|
81
|
+
GitHub has to reach `POST /webhook` from the internet. The chart ships an
|
|
82
|
+
Ingress, off by default:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
helm upgrade --install loki helm/loki-mode \
|
|
86
|
+
--set receiver.ingress.enabled=true \
|
|
87
|
+
--set receiver.ingress.host=loki.example.com \
|
|
88
|
+
--set receiver.ingress.className=nginx
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Terminate TLS there. On Compose the receiver binds to `127.0.0.1:7373`
|
|
92
|
+
deliberately: put a reverse proxy in front rather than publishing an
|
|
93
|
+
unencrypted webhook endpoint on every interface the host has.
|
|
94
|
+
|
|
95
|
+
### 3. Create the webhook
|
|
96
|
+
|
|
97
|
+
In the repository: **Settings -> Webhooks -> Add webhook**.
|
|
98
|
+
|
|
99
|
+
| Field | Value |
|
|
100
|
+
|---|---|
|
|
101
|
+
| Payload URL | `https://loki.example.com/webhook` |
|
|
102
|
+
| Content type | `application/json` |
|
|
103
|
+
| Secret | the value from step 1 |
|
|
104
|
+
| SSL verification | Enable |
|
|
105
|
+
| Events | **Let me select individual events** |
|
|
106
|
+
|
|
107
|
+
Now the part that decides what your deployment costs. Tick:
|
|
108
|
+
|
|
109
|
+
- **Issues** -- an issue being opened starts a build.
|
|
110
|
+
- **Workflow runs** -- a failed CI run starts a repair build.
|
|
111
|
+
|
|
112
|
+
Leave **Pull requests** unticked unless you have decided you want it. See
|
|
113
|
+
below.
|
|
114
|
+
|
|
115
|
+
### 4. What actually fires
|
|
116
|
+
|
|
117
|
+
Only these three combinations do anything. Every other event and action is
|
|
118
|
+
logged and ignored.
|
|
119
|
+
|
|
120
|
+
| Event | Action | What runs |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `issues` | `opened` | `loki start owner/repo#N --pr --detach` |
|
|
123
|
+
| `pull_request` | `synchronize` | `loki start owner/repo#N --detach` |
|
|
124
|
+
| `workflow_run` | `completed` with `conclusion=failure` | `loki start --detach` |
|
|
125
|
+
|
|
126
|
+
Note what is **not** there. Labelling an issue does nothing. Commenting on an
|
|
127
|
+
issue does nothing. Opening a pull request does nothing -- for PRs it is
|
|
128
|
+
`synchronize`, meaning new commits pushed to an existing PR.
|
|
129
|
+
|
|
130
|
+
### Why the PR trigger is off by default
|
|
131
|
+
|
|
132
|
+
`pull_request`/`synchronize` fires on **every push to every open PR**. On a
|
|
133
|
+
repository with a handful of active PRs and a normal rebase-and-force-push
|
|
134
|
+
habit, that is dozens of builds a day that nobody asked for. It is the easiest
|
|
135
|
+
way to surprise a team with a bill.
|
|
136
|
+
|
|
137
|
+
Turn it on when you have decided you want it:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
helm upgrade --install loki helm/loki-mode --set triggers.pullRequest=true
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Read this carefully, because the value alone does not enforce anything.** The
|
|
144
|
+
receiver dispatches on whatever GitHub delivers; it does not consult an
|
|
145
|
+
allowlist before acting. `triggers.pullRequest` renders a ConfigMap that
|
|
146
|
+
records your intent and gives you something to diff when someone asks why a
|
|
147
|
+
build started. What actually keeps PR builds from firing is **not subscribing
|
|
148
|
+
the webhook to Pull request events** in step 3.
|
|
149
|
+
|
|
150
|
+
If you tick "Pull requests" in GitHub, PR builds will run no matter what
|
|
151
|
+
`triggers.pullRequest` says.
|
|
152
|
+
|
|
153
|
+
### 5. Confirm it works
|
|
154
|
+
|
|
155
|
+
GitHub records every delivery. **Settings -> Webhooks -> your webhook ->
|
|
156
|
+
Recent Deliveries** shows request and response for each one.
|
|
157
|
+
|
|
158
|
+
| Response | Meaning |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `202` `{"status":"queued"}` | Accepted and enqueued. This is success. |
|
|
161
|
+
| `401 invalid signature` | Secret mismatch between GitHub and the deployment. |
|
|
162
|
+
| `503 webhook secret not configured` | The receiver has no secret. Step 1. |
|
|
163
|
+
| `503 server busy, retry later` | Dispatch queue full; load shed on purpose. |
|
|
164
|
+
| `200 {"status":"duplicate"}` | Redelivery of a delivery ID already seen. |
|
|
165
|
+
| `200 skipped (action=...)` | Delivered fine, but not an action that fires. |
|
|
166
|
+
|
|
167
|
+
Then watch the worker:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
kubectl logs -l app.kubernetes.io/component=worker -f # Kubernetes
|
|
171
|
+
docker compose --profile service logs -f worker # Compose
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
You are looking for `[queue-consumer] starting build: spec=owner/repo#N`.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Path 2: manual enqueue
|
|
179
|
+
|
|
180
|
+
The queue is a plain Redis list. Pushing to it starts a build, with no GitHub
|
|
181
|
+
involved. This is the fastest way to prove a deployment works end to end, and
|
|
182
|
+
it is how you re-drive a build that failed.
|
|
183
|
+
|
|
184
|
+
Kubernetes:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
kubectl exec -it deploy/loki-redis-master -- \
|
|
188
|
+
redis-cli RPUSH loki-builds 'owner/repo#123'
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Compose:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
docker compose --profile service exec redis \
|
|
195
|
+
redis-cli RPUSH loki-builds 'owner/repo#123'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
A work item is anything `loki start` accepts: a GitHub issue ref
|
|
199
|
+
(`owner/repo#123`), a path to a PRD, a one-line brief, or a JSON object
|
|
200
|
+
`{"spec": "..."}`. An item beginning with `-` is rejected rather than being
|
|
201
|
+
parsed as a flag.
|
|
202
|
+
|
|
203
|
+
Check queue depth, which is also your "are the workers keeping up" metric:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
redis-cli LLEN loki-builds
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Path 3: the API endpoint
|
|
212
|
+
|
|
213
|
+
`POST /jobs` with a bearer token, for submitting a build from your own tooling
|
|
214
|
+
without involving GitHub at all.
|
|
215
|
+
|
|
216
|
+
Set `secrets.apiToken` (Helm) or `LOKI_API_TOKEN` (Compose). If it is unset the
|
|
217
|
+
receiver still starts, and rejects every `/jobs` request with 503 -- the same
|
|
218
|
+
fail-closed rule the webhook path follows.
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
curl -X POST https://loki.example.com/jobs \
|
|
222
|
+
-H "Authorization: Bearer $LOKI_API_TOKEN" \
|
|
223
|
+
-H "Content-Type: application/json" \
|
|
224
|
+
-d '{"spec": "owner/repo#123"}'
|
|
225
|
+
# -> 202 {"id": "...", "status": "queued"}
|
|
226
|
+
|
|
227
|
+
curl -H "Authorization: Bearer $LOKI_API_TOKEN" \
|
|
228
|
+
https://loki.example.com/jobs/<id>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
It is a **separate credential from the webhook HMAC** on purpose. The HMAC
|
|
232
|
+
authenticates GitHub; the bearer token authenticates a human operator. Holding
|
|
233
|
+
one must never grant the other, so they rotate on different schedules and a
|
|
234
|
+
leaked API token does not let the holder forge GitHub webhooks.
|
|
235
|
+
|
|
236
|
+
The token can also be read from a mounted file via `LOKI_API_TOKEN_FILE`, which
|
|
237
|
+
is the better option if you inject secrets as files rather than env vars.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Triggers that do not exist yet
|
|
242
|
+
|
|
243
|
+
Documented so you do not go looking for them:
|
|
244
|
+
|
|
245
|
+
- **Issue labelled** (add a `loki` label to start a build). Not implemented.
|
|
246
|
+
The `issues` handler acts on `opened` only.
|
|
247
|
+
- **Issue comment commands** (`/loki build`). Not implemented. The receiver has
|
|
248
|
+
no `issue_comment` handler; GitHub will deliver the event and get back
|
|
249
|
+
`unsupported event: issue_comment`.
|
|
250
|
+
|
|
251
|
+
Both are reasonable and neither exists in this release. Path 2 covers the same
|
|
252
|
+
ground manually today.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Kubernetes (Helm)
|
|
257
|
+
|
|
258
|
+
No `helm dependency build` step: the chart has no subchart dependencies. Redis
|
|
259
|
+
is a plain Deployment on `redis:7-alpine`, the same image `docker-compose.yml`
|
|
260
|
+
uses.
|
|
261
|
+
|
|
262
|
+
The default image tag is the chart's `appVersion`, which tracks `VERSION` and so
|
|
263
|
+
names a tag that exists only once the release pipeline has published it. If you
|
|
264
|
+
install from a source tree whose `VERSION` is ahead of Docker Hub, pods fail on
|
|
265
|
+
`ImagePullBackOff` -- add `--set image.tag=<a published version>`.
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
helm upgrade --install loki helm/loki-mode \
|
|
269
|
+
--namespace loki --create-namespace \
|
|
270
|
+
--set secrets.githubWebhookSecret='<hmac>' \
|
|
271
|
+
--set secrets.anthropicApiKey='<key>' \
|
|
272
|
+
--set worker.replicaCount=3
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Preview before applying:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
helm template loki helm/loki-mode | less
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### The values that matter
|
|
282
|
+
|
|
283
|
+
| Value | Default | Why you would change it |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `worker.replicaCount` | `2` | The scaling knob. One build per replica. |
|
|
286
|
+
| `worker.terminationGracePeriodSeconds` | `7200` | Must exceed your p99 build time. |
|
|
287
|
+
| `triggers.pullRequest` | `false` | Cost. See above -- and subscribe accordingly. |
|
|
288
|
+
| `queue.backend` | `redis` | `file` gives at-least-once, needs an RWX volume. |
|
|
289
|
+
| `networkPolicy.enabled` | `false` | Turn on if your CNI enforces policy. |
|
|
290
|
+
| `secrets.existingSecret` | `""` | Use a real secret manager. |
|
|
291
|
+
|
|
292
|
+
### Verify your install
|
|
293
|
+
|
|
294
|
+
Rendering valid YAML is not evidence that a pod starts. Run these after every
|
|
295
|
+
install.
|
|
296
|
+
|
|
297
|
+
Everything in this section was executed against a real cluster (kind v0.31.0,
|
|
298
|
+
Kubernetes v1.35.0) on the `asklokesh/loki-mode:9.17.2` image, except the two
|
|
299
|
+
items explicitly marked UNVERIFIED.
|
|
300
|
+
|
|
301
|
+
**1. Everything reached Ready.** `--wait` already fails the install if not, but
|
|
302
|
+
check what is running:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
kubectl get pods -n loki-verify
|
|
306
|
+
# loki-loki-mode-receiver-... 1/1 Running
|
|
307
|
+
# loki-loki-mode-receiver-... 1/1 Running
|
|
308
|
+
# loki-loki-mode-redis-... 1/1 Running
|
|
309
|
+
# loki-loki-mode-worker-... 1/1 Running
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**2. The receiver is actually serving.** This is what `helm test` is for:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
helm test loki -n loki-verify --logs
|
|
316
|
+
# Phase: Succeeded
|
|
317
|
+
# GET http://loki-loki-mode-receiver:80/health -> 200 {"status": "ok", "service": "loki-trigger-server"}
|
|
318
|
+
# PASS: receiver is serving
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The test asserts on the response body, not just the status code, so a proxy or
|
|
322
|
+
a wrong backend answering 200 still fails it.
|
|
323
|
+
|
|
324
|
+
**3. The receiver picked up its secret.** `secret_configured: true` is the
|
|
325
|
+
difference between a working webhook and one that 503s every delivery:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
kubectl port-forward -n loki-verify svc/loki-loki-mode-receiver 18080:80 &
|
|
329
|
+
curl -s http://127.0.0.1:18080/status
|
|
330
|
+
# {"status": "running", "dry_run": false, "port": 7373,
|
|
331
|
+
# "enabled_events": [...], "secret_configured": true}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**4. The worker can reach the queue.** A worker that cannot reach Redis sits in
|
|
335
|
+
its poll loop forever and looks perfectly healthy:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
W=$(kubectl get pod -n loki-verify -l app.kubernetes.io/component=worker \
|
|
339
|
+
--field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
|
|
340
|
+
kubectl exec -n loki-verify "$W" -- sh -c 'redis-cli -u "$LOKI_QUEUE_URL" ping'
|
|
341
|
+
# PONG
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**5. A queued item actually gets picked up.** The end-to-end check:
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
R=$(kubectl get pod -n loki-verify -l app.kubernetes.io/component=redis -o jsonpath='{.items[0].metadata.name}')
|
|
348
|
+
kubectl exec -n loki-verify "$R" -- redis-cli RPUSH loki-builds 'smoke-test-spec'
|
|
349
|
+
sleep 10
|
|
350
|
+
kubectl exec -n loki-verify "$R" -- redis-cli LLEN loki-builds # 0 == claimed
|
|
351
|
+
kubectl logs -n loki-verify "$W"
|
|
352
|
+
# [queue-consumer] starting build: spec=smoke-test-spec
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`starting build:` is the line that matters. With a placeholder API key the
|
|
356
|
+
build then fails, which is expected -- you are verifying that the item was
|
|
357
|
+
dequeued and `loki start` was invoked, not that the build succeeded.
|
|
358
|
+
|
|
359
|
+
Watch for one thing here: if the build exits 2, the consumer treats that as a
|
|
360
|
+
fatal configuration error and exits, and the pod restarts. `kubectl logs
|
|
361
|
+
--previous` shows what the previous container did.
|
|
362
|
+
|
|
363
|
+
**6. Confirm the hardening actually applied.** Cluster policy can override what
|
|
364
|
+
you set:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
kubectl exec -n loki-verify "$W" -- id
|
|
368
|
+
# uid=1000(loki) gid=1000(loki)
|
|
369
|
+
|
|
370
|
+
kubectl get deploy -n loki-verify loki-loki-mode-worker \
|
|
371
|
+
-o jsonpath='{.spec.template.spec.terminationGracePeriodSeconds}'
|
|
372
|
+
# 7200
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**7. NetworkPolicy enforcement -- test it, do not assume it.**
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
kubectl exec -n loki-verify "$W" -- curl -s --max-time 5 http://169.254.169.254/
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**A timeout here does not prove the policy works.** On a laptop cluster there
|
|
382
|
+
is no metadata service to reach, so the request times out either way. Run the
|
|
383
|
+
control before believing it:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
kubectl delete networkpolicy -n loki-verify loki-loki-mode-worker
|
|
387
|
+
kubectl exec -n loki-verify "$W" -- curl -s --max-time 5 http://169.254.169.254/
|
|
388
|
+
# Same timeout with the policy deleted => the policy was never what blocked it.
|
|
389
|
+
helm upgrade loki helm/loki-mode -n loki-verify --reuse-values # restore
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
That control was run on kind, whose default CNI (kindnet) does **not** enforce
|
|
393
|
+
NetworkPolicy: the result was identical with and without the policy. The object
|
|
394
|
+
applied cleanly and rendered the correct `except` for 169.254.169.254, and
|
|
395
|
+
nothing enforced it. This is the silent no-op described above, observed rather
|
|
396
|
+
than assumed.
|
|
397
|
+
|
|
398
|
+
**UNVERIFIED:** that the metadata endpoint is genuinely blocked on a
|
|
399
|
+
policy-enforcing CNI (Calico, Cilium). That needs a cluster with one, plus a
|
|
400
|
+
real metadata service. Run the delete-and-retry control there: on an enforcing
|
|
401
|
+
CNI the two results differ.
|
|
402
|
+
|
|
403
|
+
**UNVERIFIED:** the GitHub webhook path end to end. Delivery from GitHub to
|
|
404
|
+
`/webhook` with a real HMAC signature was not exercised, since it needs a
|
|
405
|
+
publicly reachable Ingress. The receiver's HMAC validation itself is covered by
|
|
406
|
+
the repository's test suite.
|
|
407
|
+
|
|
408
|
+
### Worker scaling is a tenancy decision
|
|
409
|
+
|
|
410
|
+
Each worker pod runs exactly one build at a time onto an `emptyDir` that is
|
|
411
|
+
created with the pod and destroyed with it. That is deliberate: one submitter's
|
|
412
|
+
checkout is never readable by the next build.
|
|
413
|
+
|
|
414
|
+
Adding replicas adds parallel builds without breaking that. But **any worker
|
|
415
|
+
can claim any queue item**, so all your submitters share one trust boundary. If
|
|
416
|
+
they are not mutually trusting, replicas are not enough -- run a separate
|
|
417
|
+
release per tenant, each with its own `queue.key` and namespace.
|
|
418
|
+
|
|
419
|
+
This is the same reasoning behind one-user-per-runner guidance for self-hosted
|
|
420
|
+
CI runners. A shared build service without it leaks source between tenants.
|
|
421
|
+
|
|
422
|
+
### Egress and the metadata endpoint
|
|
423
|
+
|
|
424
|
+
`networkPolicy.enabled=true` applies default-deny egress to the worker, with an
|
|
425
|
+
explicit exclusion for `169.254.169.254`.
|
|
426
|
+
|
|
427
|
+
That address needs naming specifically because it is **link-local**: it is not
|
|
428
|
+
in any pod CIDR, service CIDR or VPC subnet, so a policy written in terms of
|
|
429
|
+
subnets never touches it. Reaching it from inside a build returns the node's
|
|
430
|
+
instance credentials, which turns an ordinary build into access to your cloud
|
|
431
|
+
account.
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
helm upgrade --install loki helm/loki-mode \
|
|
435
|
+
--set networkPolicy.enabled=true \
|
|
436
|
+
--set 'networkPolicy.allowedEgressCIDRs={0.0.0.0/0}'
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Every allow rule carves out `169.254.0.0/16`, so even the wide-open CIDR above
|
|
440
|
+
cannot reopen it by accident.
|
|
441
|
+
|
|
442
|
+
**Verify rather than assume.** A NetworkPolicy is enforced by your CNI. On a
|
|
443
|
+
cluster whose CNI has no policy support, the API server accepts the object and
|
|
444
|
+
nothing enforces it -- no error, no warning. `kubectl get networkpolicy`
|
|
445
|
+
showing your policy is not evidence it works:
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
kubectl exec -it deploy/loki-worker -- \
|
|
449
|
+
curl -s --max-time 5 http://169.254.169.254/ && echo "NOT BLOCKED" || echo "blocked"
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
It also stops at the cluster edge. Filtering traffic once it has left the
|
|
453
|
+
cluster is a NAT gateway, firewall or proxy you run.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## Single node (Docker Compose)
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
cp .env.example .env
|
|
461
|
+
# set GITHUB_WEBHOOK_SECRET and ANTHROPIC_API_KEY
|
|
462
|
+
|
|
463
|
+
docker compose --profile service up -d
|
|
464
|
+
docker compose --profile service ps
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
The `service` profile is what separates the build service from the one-shot
|
|
468
|
+
path. Without it, `docker compose run loki start prd.md` still does exactly
|
|
469
|
+
what it always did -- one build, right now, in the current directory.
|
|
470
|
+
|
|
471
|
+
Scale workers:
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
docker compose --profile service up -d --scale worker=4
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Same tenancy caveat as Kubernetes: each worker gets its own named volume, and
|
|
478
|
+
any worker can claim any item. Scale within one trust boundary.
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Things that will bite you
|
|
483
|
+
|
|
484
|
+
**The grace period is a whole build, not a cleanup window.** On SIGTERM the
|
|
485
|
+
consumer lets the current build **run to completion** before exiting. At the
|
|
486
|
+
Kubernetes default of 30s (Docker's is 10s), every rolling update, node drain,
|
|
487
|
+
autoscaler scale-in and spot reclaim SIGKILLs a build mid-flight. The chart
|
|
488
|
+
sets `7200` and Compose sets `2h`. Set yours above your p99 build time.
|
|
489
|
+
|
|
490
|
+
**The Redis queue is at-most-once.** The shipped consumer pops an item and then
|
|
491
|
+
runs it. There is no visibility timeout and no dead-letter requeue, so if a
|
|
492
|
+
worker dies mid-build, that build is gone from the queue and **nothing retries
|
|
493
|
+
it** -- no error, no alert, just a build that never finishes. This is why the
|
|
494
|
+
grace period is measured in hours. If you need real at-least-once delivery, use
|
|
495
|
+
`queue.backend=file` (a crashed build leaves its item in `processing/` for a
|
|
496
|
+
human to re-drive) or bring a broker that has it and override `queue.command`
|
|
497
|
+
with your own consumer. SQS, Pub/Sub, RabbitMQ and Kafka are documented as
|
|
498
|
+
bring-your-own; they are not implemented here.
|
|
499
|
+
|
|
500
|
+
**No credentials in the image.** Every credential is injected at pod start from
|
|
501
|
+
a Secret or `.env`. A credential baked into a shared image is readable by every
|
|
502
|
+
job that image ever runs, including builds from submitters who should never
|
|
503
|
+
have had it, and rotating it means rebuilding and redeploying everything that
|
|
504
|
+
consumes it.
|
|
505
|
+
|
|
506
|
+
**A 200 response does not mean a build started.** `200 skipped (action=...)`
|
|
507
|
+
means the webhook was delivered and authenticated perfectly, and then did
|
|
508
|
+
nothing because it was not one of the three firing combinations. Only `202
|
|
509
|
+
queued` means a build is coming. Check worker logs, not just the delivery
|
|
510
|
+
response.
|
|
511
|
+
|
|
512
|
+
**Nothing here rate-limits spend.** The receiver's bounded queue caps
|
|
513
|
+
*in-flight dispatches*, not builds per day. Watch queue depth (`LLEN
|
|
514
|
+
loki-builds`) and worker logs, and turn `triggers.pullRequest` on only
|
|
515
|
+
deliberately.
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Reference
|
|
520
|
+
|
|
521
|
+
| Env var | Where | Meaning |
|
|
522
|
+
|---|---|---|
|
|
523
|
+
| `GITHUB_WEBHOOK_SECRET` | receiver | HMAC-SHA256 webhook secret. Required. |
|
|
524
|
+
| `LOKI_API_TOKEN` | receiver | Bearer token for `POST /jobs` (not yet live). |
|
|
525
|
+
| `ANTHROPIC_API_KEY` | worker | Provider credential. Worker only. |
|
|
526
|
+
| `GITHUB_TOKEN` | worker | Used by builds for push / PR creation. |
|
|
527
|
+
| `LOKI_QUEUE_BACKEND` | both | `redis` or `file`. |
|
|
528
|
+
| `LOKI_QUEUE_KEY` | both | Redis list key. Default `loki-builds`. |
|
|
529
|
+
| `LOKI_QUEUE_URL` | both | Redis connection URL. |
|
|
530
|
+
| `LOKI_QUEUE_DIR` | worker | File-backend root. `file` backend only. |
|
|
531
|
+
| `LOKI_QUEUE_ONESHOT` | worker | `1` processes one item and exits (KEDA). |
|
|
532
|
+
| `LOKI_QUEUE_POLL_SEC` | worker | File-backend empty-poll sleep. |
|
|
533
|
+
| `LOKI_QUEUE_BLOCK_SEC` | worker | Redis `BLPOP` block timeout. |
|
|
534
|
+
|
|
535
|
+
| Receiver route | Purpose |
|
|
536
|
+
|---|---|
|
|
537
|
+
| `GET /health` | Liveness and readiness. Does not touch the queue. |
|
|
538
|
+
| `GET /status` | Port, dry-run state, whether a secret is configured. |
|
|
539
|
+
| `POST /webhook` | The GitHub webhook endpoint. |
|
|
540
|
+
|
|
541
|
+
Source: `autonomy/trigger-server.py`, `autonomy/queue-consumer.sh`,
|
|
542
|
+
`helm/loki-mode/values.yaml`.
|