adispatchd 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/api/errors.d.ts +27 -0
- package/dist/api/errors.js +80 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +11 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/logs.d.ts +30 -0
- package/dist/api/logs.js +88 -0
- package/dist/api/logs.js.map +1 -0
- package/dist/api/serialize.d.ts +65 -0
- package/dist/api/serialize.js +135 -0
- package/dist/api/serialize.js.map +1 -0
- package/dist/api/server.d.ts +89 -0
- package/dist/api/server.js +751 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/sse.d.ts +90 -0
- package/dist/api/sse.js +225 -0
- package/dist/api/sse.js.map +1 -0
- package/dist/app.d.ts +173 -0
- package/dist/app.js +488 -0
- package/dist/app.js.map +1 -0
- package/dist/cli/config-validate.d.ts +6 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/config-validate.js.map +1 -0
- package/dist/cli/control.d.ts +31 -0
- package/dist/cli/control.js +186 -0
- package/dist/cli/control.js.map +1 -0
- package/dist/cli/http-client.d.ts +57 -0
- package/dist/cli/http-client.js +174 -0
- package/dist/cli/http-client.js.map +1 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-templates.d.ts +20 -0
- package/dist/cli/init-templates.js +275 -0
- package/dist/cli/init-templates.js.map +1 -0
- package/dist/cli/init.d.ts +16 -0
- package/dist/cli/init.js +107 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +25 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/program.d.ts +102 -0
- package/dist/cli/program.js +133 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/service.d.ts +27 -0
- package/dist/cli/service.js +252 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/cli/start.d.ts +22 -0
- package/dist/cli/start.js +133 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/workflow-dry-run.d.ts +13 -0
- package/dist/cli/workflow-dry-run.js +144 -0
- package/dist/cli/workflow-dry-run.js.map +1 -0
- package/dist/config/field-catalog.d.ts +33 -0
- package/dist/config/field-catalog.js +172 -0
- package/dist/config/field-catalog.js.map +1 -0
- package/dist/config/filter-schema.d.ts +15 -0
- package/dist/config/filter-schema.js +110 -0
- package/dist/config/filter-schema.js.map +1 -0
- package/dist/config/index.d.ts +15 -0
- package/dist/config/index.js +16 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/issues.d.ts +27 -0
- package/dist/config/issues.js +37 -0
- package/dist/config/issues.js.map +1 -0
- package/dist/config/loader.d.ts +32 -0
- package/dist/config/loader.js +250 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/manager.d.ts +153 -0
- package/dist/config/manager.js +276 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/model.d.ts +334 -0
- package/dist/config/model.js +2 -0
- package/dist/config/model.js.map +1 -0
- package/dist/config/normalize.d.ts +20 -0
- package/dist/config/normalize.js +200 -0
- package/dist/config/normalize.js.map +1 -0
- package/dist/config/parameters.d.ts +37 -0
- package/dist/config/parameters.js +71 -0
- package/dist/config/parameters.js.map +1 -0
- package/dist/config/public.d.ts +6 -0
- package/dist/config/public.js +18 -0
- package/dist/config/public.js.map +1 -0
- package/dist/config/schemas.d.ts +538 -0
- package/dist/config/schemas.js +321 -0
- package/dist/config/schemas.js.map +1 -0
- package/dist/config/snapshot.d.ts +38 -0
- package/dist/config/snapshot.js +57 -0
- package/dist/config/snapshot.js.map +1 -0
- package/dist/config/validate.d.ts +33 -0
- package/dist/config/validate.js +346 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/core/clock.d.ts +59 -0
- package/dist/core/clock.js +133 -0
- package/dist/core/clock.js.map +1 -0
- package/dist/core/duration.d.ts +19 -0
- package/dist/core/duration.js +93 -0
- package/dist/core/duration.js.map +1 -0
- package/dist/core/errors.d.ts +62 -0
- package/dist/core/errors.js +72 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/ids.d.ts +57 -0
- package/dist/core/ids.js +88 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +100 -0
- package/dist/core/logger.js +207 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/paths.d.ts +84 -0
- package/dist/core/paths.js +138 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/template.d.ts +136 -0
- package/dist/core/template.js +261 -0
- package/dist/core/template.js.map +1 -0
- package/dist/core/types.d.ts +544 -0
- package/dist/core/types.js +172 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/worker-protocol.d.ts +102 -0
- package/dist/core/worker-protocol.js +75 -0
- package/dist/core/worker-protocol.js.map +1 -0
- package/dist/executors/classify.d.ts +25 -0
- package/dist/executors/classify.js +54 -0
- package/dist/executors/classify.js.map +1 -0
- package/dist/executors/dispatcher.d.ts +35 -0
- package/dist/executors/dispatcher.js +68 -0
- package/dist/executors/dispatcher.js.map +1 -0
- package/dist/executors/env.d.ts +43 -0
- package/dist/executors/env.js +66 -0
- package/dist/executors/env.js.map +1 -0
- package/dist/executors/event-parser.d.ts +41 -0
- package/dist/executors/event-parser.js +106 -0
- package/dist/executors/event-parser.js.map +1 -0
- package/dist/executors/index.d.ts +12 -0
- package/dist/executors/index.js +13 -0
- package/dist/executors/index.js.map +1 -0
- package/dist/executors/log-capture.d.ts +57 -0
- package/dist/executors/log-capture.js +155 -0
- package/dist/executors/log-capture.js.map +1 -0
- package/dist/executors/process-identity.d.ts +124 -0
- package/dist/executors/process-identity.js +283 -0
- package/dist/executors/process-identity.js.map +1 -0
- package/dist/executors/shell-executor.d.ts +35 -0
- package/dist/executors/shell-executor.js +478 -0
- package/dist/executors/shell-executor.js.map +1 -0
- package/dist/executors/stream-redactor.d.ts +40 -0
- package/dist/executors/stream-redactor.js +89 -0
- package/dist/executors/stream-redactor.js.map +1 -0
- package/dist/executors/workspace.d.ts +10 -0
- package/dist/executors/workspace.js +47 -0
- package/dist/executors/workspace.js.map +1 -0
- package/dist/filters/evaluate.d.ts +77 -0
- package/dist/filters/evaluate.js +187 -0
- package/dist/filters/evaluate.js.map +1 -0
- package/dist/filters/fields.d.ts +35 -0
- package/dist/filters/fields.js +75 -0
- package/dist/filters/fields.js.map +1 -0
- package/dist/filters/index.d.ts +11 -0
- package/dist/filters/index.js +12 -0
- package/dist/filters/index.js.map +1 -0
- package/dist/filters/iso-date.d.ts +10 -0
- package/dist/filters/iso-date.js +42 -0
- package/dist/filters/iso-date.js.map +1 -0
- package/dist/filters/predicate.d.ts +24 -0
- package/dist/filters/predicate.js +178 -0
- package/dist/filters/predicate.js.map +1 -0
- package/dist/filters/priority.d.ts +51 -0
- package/dist/filters/priority.js +91 -0
- package/dist/filters/priority.js.map +1 -0
- package/dist/filters/pushdown.d.ts +12 -0
- package/dist/filters/pushdown.js +52 -0
- package/dist/filters/pushdown.js.map +1 -0
- package/dist/filters/shell-check.d.ts +110 -0
- package/dist/filters/shell-check.js +304 -0
- package/dist/filters/shell-check.js.map +1 -0
- package/dist/identities/github-app.d.ts +55 -0
- package/dist/identities/github-app.js +160 -0
- package/dist/identities/github-app.js.map +1 -0
- package/dist/identities/index.d.ts +6 -0
- package/dist/identities/index.js +7 -0
- package/dist/identities/index.js.map +1 -0
- package/dist/identities/jwt.d.ts +24 -0
- package/dist/identities/jwt.js +56 -0
- package/dist/identities/jwt.js.map +1 -0
- package/dist/lifecycle/context.d.ts +106 -0
- package/dist/lifecycle/context.js +170 -0
- package/dist/lifecycle/context.js.map +1 -0
- package/dist/lifecycle/engine.d.ts +185 -0
- package/dist/lifecycle/engine.js +551 -0
- package/dist/lifecycle/engine.js.map +1 -0
- package/dist/lifecycle/errors.d.ts +48 -0
- package/dist/lifecycle/errors.js +71 -0
- package/dist/lifecycle/errors.js.map +1 -0
- package/dist/lifecycle/index.d.ts +9 -0
- package/dist/lifecycle/index.js +9 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/lifecycle/performers.d.ts +66 -0
- package/dist/lifecycle/performers.js +179 -0
- package/dist/lifecycle/performers.js.map +1 -0
- package/dist/notifications/index.d.ts +6 -0
- package/dist/notifications/index.js +7 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/service.d.ts +115 -0
- package/dist/notifications/service.js +364 -0
- package/dist/notifications/service.js.map +1 -0
- package/dist/persistence/actions.d.ts +101 -0
- package/dist/persistence/actions.js +196 -0
- package/dist/persistence/actions.js.map +1 -0
- package/dist/persistence/blocks.d.ts +85 -0
- package/dist/persistence/blocks.js +140 -0
- package/dist/persistence/blocks.js.map +1 -0
- package/dist/persistence/cron-firings.d.ts +55 -0
- package/dist/persistence/cron-firings.js +70 -0
- package/dist/persistence/cron-firings.js.map +1 -0
- package/dist/persistence/database.d.ts +60 -0
- package/dist/persistence/database.js +124 -0
- package/dist/persistence/database.js.map +1 -0
- package/dist/persistence/errors.d.ts +39 -0
- package/dist/persistence/errors.js +67 -0
- package/dist/persistence/errors.js.map +1 -0
- package/dist/persistence/events.d.ts +72 -0
- package/dist/persistence/events.js +83 -0
- package/dist/persistence/events.js.map +1 -0
- package/dist/persistence/index.d.ts +18 -0
- package/dist/persistence/index.js +19 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/instance-lock.d.ts +26 -0
- package/dist/persistence/instance-lock.js +117 -0
- package/dist/persistence/instance-lock.js.map +1 -0
- package/dist/persistence/lock-keys.d.ts +16 -0
- package/dist/persistence/lock-keys.js +41 -0
- package/dist/persistence/lock-keys.js.map +1 -0
- package/dist/persistence/notifications.d.ts +113 -0
- package/dist/persistence/notifications.js +213 -0
- package/dist/persistence/notifications.js.map +1 -0
- package/dist/persistence/persistence.d.ts +46 -0
- package/dist/persistence/persistence.js +42 -0
- package/dist/persistence/persistence.js.map +1 -0
- package/dist/persistence/retention.d.ts +46 -0
- package/dist/persistence/retention.js +68 -0
- package/dist/persistence/retention.js.map +1 -0
- package/dist/persistence/runs.d.ts +215 -0
- package/dist/persistence/runs.js +417 -0
- package/dist/persistence/runs.js.map +1 -0
- package/dist/persistence/schema.d.ts +16 -0
- package/dist/persistence/schema.js +183 -0
- package/dist/persistence/schema.js.map +1 -0
- package/dist/persistence/sqlite.d.ts +49 -0
- package/dist/persistence/sqlite.js +131 -0
- package/dist/persistence/sqlite.js.map +1 -0
- package/dist/runs/controller.d.ts +307 -0
- package/dist/runs/controller.js +1078 -0
- package/dist/runs/controller.js.map +1 -0
- package/dist/runs/errors.d.ts +20 -0
- package/dist/runs/errors.js +26 -0
- package/dist/runs/errors.js.map +1 -0
- package/dist/runs/event-bus.d.ts +106 -0
- package/dist/runs/event-bus.js +105 -0
- package/dist/runs/event-bus.js.map +1 -0
- package/dist/runs/execution.d.ts +39 -0
- package/dist/runs/execution.js +146 -0
- package/dist/runs/execution.js.map +1 -0
- package/dist/runs/index.d.ts +12 -0
- package/dist/runs/index.js +13 -0
- package/dist/runs/index.js.map +1 -0
- package/dist/runs/retention.d.ts +64 -0
- package/dist/runs/retention.js +232 -0
- package/dist/runs/retention.js.map +1 -0
- package/dist/runs/workspace.d.ts +20 -0
- package/dist/runs/workspace.js +51 -0
- package/dist/runs/workspace.js.map +1 -0
- package/dist/scheduler/checks.d.ts +41 -0
- package/dist/scheduler/checks.js +111 -0
- package/dist/scheduler/checks.js.map +1 -0
- package/dist/scheduler/cron.d.ts +32 -0
- package/dist/scheduler/cron.js +83 -0
- package/dist/scheduler/cron.js.map +1 -0
- package/dist/scheduler/dry-run.d.ts +42 -0
- package/dist/scheduler/dry-run.js +167 -0
- package/dist/scheduler/dry-run.js.map +1 -0
- package/dist/scheduler/errors.d.ts +22 -0
- package/dist/scheduler/errors.js +22 -0
- package/dist/scheduler/errors.js.map +1 -0
- package/dist/scheduler/format.d.ts +3 -0
- package/dist/scheduler/format.js +73 -0
- package/dist/scheduler/format.js.map +1 -0
- package/dist/scheduler/index.d.ts +13 -0
- package/dist/scheduler/index.js +14 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/planner.d.ts +88 -0
- package/dist/scheduler/planner.js +255 -0
- package/dist/scheduler/planner.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +160 -0
- package/dist/scheduler/scheduler.js +680 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/state.d.ts +26 -0
- package/dist/scheduler/state.js +25 -0
- package/dist/scheduler/state.js.map +1 -0
- package/dist/scheduler/types.d.ts +197 -0
- package/dist/scheduler/types.js +3 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/service/index.d.ts +6 -0
- package/dist/service/index.js +7 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/install.d.ts +93 -0
- package/dist/service/install.js +250 -0
- package/dist/service/install.js.map +1 -0
- package/dist/service/unit.d.ts +50 -0
- package/dist/service/unit.js +171 -0
- package/dist/service/unit.js.map +1 -0
- package/dist/sources/github/adapter.d.ts +99 -0
- package/dist/sources/github/adapter.js +336 -0
- package/dist/sources/github/adapter.js.map +1 -0
- package/dist/sources/github/fields.d.ts +8 -0
- package/dist/sources/github/fields.js +72 -0
- package/dist/sources/github/fields.js.map +1 -0
- package/dist/sources/github/http.d.ts +100 -0
- package/dist/sources/github/http.js +288 -0
- package/dist/sources/github/http.js.map +1 -0
- package/dist/sources/github/index.d.ts +6 -0
- package/dist/sources/github/index.js +7 -0
- package/dist/sources/github/index.js.map +1 -0
- package/dist/sources/github/pushdown.d.ts +14 -0
- package/dist/sources/github/pushdown.js +47 -0
- package/dist/sources/github/pushdown.js.map +1 -0
- package/dist/sources/github/refs.d.ts +17 -0
- package/dist/sources/github/refs.js +61 -0
- package/dist/sources/github/refs.js.map +1 -0
- package/dist/sources/index.d.ts +6 -0
- package/dist/sources/index.js +7 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +28 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/public/app.css +239 -0
- package/dist/ui/public/favicon.svg +1 -0
- package/dist/ui/public/index.html +27 -0
- package/dist/ui/public/js/client/api.js +68 -0
- package/dist/ui/public/js/client/app.js +146 -0
- package/dist/ui/public/js/client/dom.js +34 -0
- package/dist/ui/public/js/client/page.js +9 -0
- package/dist/ui/public/js/client/pages/actions.js +35 -0
- package/dist/ui/public/js/client/pages/blocked.js +58 -0
- package/dist/ui/public/js/client/pages/config.js +126 -0
- package/dist/ui/public/js/client/pages/dashboard.js +175 -0
- package/dist/ui/public/js/client/pages/run.js +367 -0
- package/dist/ui/public/js/client/pages/runs.js +72 -0
- package/dist/ui/public/js/client/pages/views.js +80 -0
- package/dist/ui/public/js/client/pages/workflows.js +291 -0
- package/dist/ui/public/js/client/types.js +1 -0
- package/dist/ui/public/js/client/ui.js +152 -0
- package/dist/ui/public/js/shared/dom.js +77 -0
- package/dist/ui/public/js/shared/events.js +88 -0
- package/dist/ui/public/js/shared/format.js +213 -0
- package/dist/ui/public/js/shared/logs.js +136 -0
- package/dist/ui/public/js/shared/params.js +90 -0
- package/dist/ui/public/js/shared/route.js +36 -0
- package/dist/ui/public/js/shared/sort.js +70 -0
- package/dist/ui/public/js/shared/sse.js +193 -0
- package/dist/ui/public/js/shared/throttle.js +65 -0
- package/dist/views/index.d.ts +6 -0
- package/dist/views/index.js +7 -0
- package/dist/views/index.js.map +1 -0
- package/dist/views/service.d.ts +131 -0
- package/dist/views/service.js +264 -0
- package/dist/views/service.js.map +1 -0
- package/docs/api.md +239 -0
- package/docs/configuration-reference.md +1785 -0
- package/docs/cron-semantics.md +96 -0
- package/docs/manual-tests/github-fixture/config.yaml +30 -0
- package/docs/manual-tests/github-fixture/helpers.sh +93 -0
- package/docs/manual-tests/github-fixture/identities.yaml +18 -0
- package/docs/manual-tests/github-fixture/scripts/test-worker.sh +100 -0
- package/docs/manual-tests/github-fixture/sources.yaml +13 -0
- package/docs/manual-tests/github-fixture/views/dependencies.yaml +17 -0
- package/docs/manual-tests/github-fixture/views/needs-maintainer.yaml +17 -0
- package/docs/manual-tests/github-fixture/workflows/coder-test.yaml +76 -0
- package/docs/manual-tests/github-fixture/workflows/commenter-test.yaml +63 -0
- package/docs/manual-tests/github-fixture/workflows/reviewer-test.yaml +60 -0
- package/docs/manual-tests/github.md +1308 -0
- package/docs/manual-tests/process-supervision.md +64 -0
- package/docs/manual-tests/systemd.md +68 -0
- package/docs/operations.md +271 -0
- package/docs/worker-protocol.md +145 -0
- package/examples/config/config.yaml +35 -0
- package/examples/config/identities.yaml +18 -0
- package/examples/config/sources.yaml +13 -0
- package/examples/config/views/needs-maintainer.yaml +15 -0
- package/examples/config/workflows/coder-project-a.yaml +86 -0
- package/examples/config/workflows/deploy.yaml +15 -0
- package/examples/config/workflows/nightly-maintenance.yaml +13 -0
- package/package.json +75 -4
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Manueller Spike: Prozess-Supervision unter Linux (Spec §13, §24.3)
|
|
2
|
+
|
|
3
|
+
Ziel: vor der Produktionsfreigabe klären, ob Prozessgruppen + Startidentität (`linux:<boot-id>:<starttime>` aus
|
|
4
|
+
`/proc/<pid>/stat`) für Cancel und Crash-Recovery genügen oder ob systemd-Scopes/cgroups bzw. ein Wrapper nötig sind.
|
|
5
|
+
Automatisierte Tests laufen nur auf macOS bzw. mit simuliertem procfs (WI-05/WI-08). Durchführung auf einer Linux-VM,
|
|
6
|
+
einmal **ohne** systemd (Vordergrund-`start`) und einmal **unter** systemd (`service install`, siehe [systemd.md](systemd.md)).
|
|
7
|
+
|
|
8
|
+
## Vorbereitung
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
adispatch init --config-dir ~/spike/config --data-dir ~/spike/data
|
|
12
|
+
cd ~/spike/config
|
|
13
|
+
cat > scripts/tree.sh <<'EOF'
|
|
14
|
+
#!/bin/sh
|
|
15
|
+
# Prozessbaum: Vordergrundkind, Hintergrundkind, Enkel mit eigener Session, Zombie-Kandidat
|
|
16
|
+
sleep 600 &
|
|
17
|
+
sh -c 'sleep 600' &
|
|
18
|
+
setsid sh -c 'echo $$ > /tmp/spike-setsid.pid; sleep 600' &
|
|
19
|
+
echo "@@ADISPATCH_EVENT {\"type\":\"status\",\"message\":\"tree started\"}"
|
|
20
|
+
sleep 600
|
|
21
|
+
EOF
|
|
22
|
+
chmod 0750 scripts/tree.sh
|
|
23
|
+
cat > workflows/tree.yaml <<'EOF'
|
|
24
|
+
id: tree
|
|
25
|
+
trigger: { type: manual }
|
|
26
|
+
execute: { type: shell, command: scripts/tree.sh, cancel_grace: 5s }
|
|
27
|
+
EOF
|
|
28
|
+
adispatch start --config-dir ~/spike/config --data-dir ~/spike/data & # ohne systemd
|
|
29
|
+
start_tree() { curl -s -X POST -H 'X-Adispatch-Request: 1' http://127.0.0.1:8787/api/v1/workflows/tree/start | jq -r .run.id; }
|
|
30
|
+
show() { ps -eo pid,pgid,sid,stat,lstart,cmd --forest | grep -E 'tree.sh|sleep 600' | grep -v grep; }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Checkliste
|
|
34
|
+
|
|
35
|
+
| # | Szenario | Erwartung | Ergebnis |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| P1 | `id=$(start_tree); show` | Worker ist Leader einer eigenen Prozessgruppe/Session (PGID = PID = SID); `sleep`-Kinder in derselben Gruppe; setsid-Enkel in eigener Session | |
|
|
38
|
+
| P2 | `curl -s http://127.0.0.1:8787/api/v1/runs/$id \| jq .pid`; `cat /proc/<pid>/stat \| awk '{print $22}'`; `cat /proc/sys/kernel/random/boot_id` | in der DB (`runs.process_start_identity`) steht `linux:<boot-id>:<starttime>` passend zu `/proc` | |
|
|
39
|
+
| P3 | **Cancel:** `curl -X POST -H 'X-Adispatch-Request: 1' …/runs/$id/cancel`; nach 6 s `show` | ganze Gruppe weg (TERM, nach 5 s KILL); Run `cancelled`; **setsid-Enkel lebt weiter** (bekannte Grenze, Skript-Verantwortung) → `kill $(cat /tmp/spike-setsid.pid)` | |
|
|
40
|
+
| P4 | **Timeout:** `timeout: { max_duration: 10s }` ergänzen, Reload, Start | nach 10 s TERM→KILL an die Gruppe, Run `timed_out` | |
|
|
41
|
+
| P5 | **Orchestrator-Absturz ohne systemd:** Run starten, `kill -9 <orchestrator-pid>`, `show` | Worker-Gruppe lebt weiter (verwaist, PPID 1/Subreaper) | |
|
|
42
|
+
| P6 | Orchestrator neu starten | Recovery verifiziert die Identität, beendet die Gruppe (TERM→KILL), Run `crashed` + `on_crash`; `/ready` erst danach `200` | |
|
|
43
|
+
| P7 | **PID-Wiederverwendung simulieren:** P5 wiederholen, Worker-Gruppe manuell beenden, in der DB `process_start_identity` auf einen falschen `starttime` setzen (`sqlite3 data/adispatchd.sqlite "UPDATE runs SET process_start_identity=json_set(process_start_identity,'$.token','linux:<boot-id>:1') WHERE id='$id'"`), dabei einen fremden Prozess mit derselben PID nicht nötig – Neustart | kein Signal an fremde Prozesse; Run `crashed` (Gruppe weg) bzw. `recovery_pending` (Gruppe mit alter PGID existiert) + Notification `recovery_failed` | |
|
|
44
|
+
| P8 | **`recovery_pending` auflösen:** UI „Retry Recovery“ bzw. `POST /api/v1/runs/$id/recovery/retry` (`{"assumeGone":true}`) | Run finalisiert (`crashed`), Lock frei | |
|
|
45
|
+
| P9 | **Zombies:** Worker, dessen Kind nicht gereapt wird (z. B. Container ohne Init, `docker run` ohne `--init`) | Cancel meldet kein falsches `kill_failed` (Zombies zählen nicht); Empfehlung `--init`/tini bestätigt | |
|
|
46
|
+
| P10 | **Container:** Worker startet `docker run --rm alpine sleep 600` (ohne `-d`) | Cancel beendet den `docker`-CLI-Prozess; Container läuft u. U. weiter → Skript braucht `trap 'docker stop …' TERM` bzw. `--init`; dokumentieren | |
|
|
47
|
+
| P11 | **Unter systemd** (`KillMode=mixed`): P5 mit `systemctl kill --kill-whom=main --signal=SIGKILL` | systemd SIGKILLt die ganze cgroup inkl. Worker **und** setsid-Enkel; Recovery findet die Gruppe weg → `crashed` | |
|
|
48
|
+
| P12 | Unter systemd: `systemd-cgls -u adispatchd.service` während eines Runs | alle Worker (auch setsid/nohup) in der Dienst-cgroup; Docker-Container **nicht** (gehören zu dockerd) | |
|
|
49
|
+
| P13 | Unter systemd: `systemctl stop` während P1 | nur der Hauptprozess bekommt SIGTERM (Worker laufen bis Drain-Timeout weiter); danach Cancel der Gruppe | |
|
|
50
|
+
|
|
51
|
+
## Auswertung / Entscheidung
|
|
52
|
+
|
|
53
|
+
Festhalten, ob für V1 ausreichend:
|
|
54
|
+
|
|
55
|
+
- Prozessgruppe + Startidentität für Cancel/Timeout/Recovery (P1–P8),
|
|
56
|
+
- systemd-cgroup als Sicherheitsnetz für Abstürze (P11–P13),
|
|
57
|
+
- oder ob Worker zusätzlich in eigene transiente Scopes (`systemd-run --scope --unit=orch-run-<id>`) bzw. einen Wrapper
|
|
58
|
+
gehören, um auch setsid-/nohup-Prozesse bei Cancel (nicht nur beim Dienst-Stop) zuverlässig zu erfassen.
|
|
59
|
+
|
|
60
|
+
## Protokoll
|
|
61
|
+
|
|
62
|
+
| Datum | Host / Kernel / systemd / Node | Version | Schritte | Beobachtung / Entscheidung |
|
|
63
|
+
|---|---|---|---|---|
|
|
64
|
+
| | | | | |
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Manuelle Tests: systemd-Dienst (Spec §22.1 „systemd“, §14, §24.4, Szenario 12)
|
|
2
|
+
|
|
3
|
+
Automatisiert (auf macOS) sind nur die Unit-Generierung (Snapshot), `service install|uninstall --dry-run`, Drain/Cancel-All
|
|
4
|
+
über die API, der Exit-Code-Vertrag des CLI-Starts (SIGTERM → Exit 0), Migration mit Backup und das `npm pack`-Update
|
|
5
|
+
(`npm run test:pack`). Diese Checkliste prüft den echten Service-Vertrag auf einem **Linux-Host mit systemd** (Wegwerf-VM,
|
|
6
|
+
nicht Produktion). Ergebnisse unten protokollieren.
|
|
7
|
+
|
|
8
|
+
## Vorbereitung
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# VM: Debian/Ubuntu mit systemd, Node 24 systemweit (/usr/bin/node)
|
|
12
|
+
npm pack # im Repo → adispatchd-0.1.0.tgz
|
|
13
|
+
scp adispatchd-0.1.0.tgz vm:
|
|
14
|
+
ssh vm
|
|
15
|
+
sudo npm install -g ./adispatchd-0.1.0.tgz
|
|
16
|
+
sudo adispatch init --config-dir /etc/adispatchd --data-dir /var/lib/adispatchd
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Test-Workflow mit langem, gut erkennbarem Worker (manuell gestartet, ohne GitHub):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
sudo tee /etc/adispatchd/scripts/long.sh >/dev/null <<'EOF'
|
|
23
|
+
#!/bin/sh
|
|
24
|
+
trap 'echo "got TERM"; exit 143' TERM
|
|
25
|
+
echo "@@ADISPATCH_EVENT {\"type\":\"status\",\"message\":\"sleeping $1s\"}"
|
|
26
|
+
sleep "$1" & wait $!
|
|
27
|
+
EOF
|
|
28
|
+
sudo chmod 0750 /etc/adispatchd/scripts/long.sh
|
|
29
|
+
sudo tee /etc/adispatchd/workflows/long.yaml >/dev/null <<'EOF'
|
|
30
|
+
id: long
|
|
31
|
+
trigger: { type: manual }
|
|
32
|
+
parameters: { seconds: { type: number, default: 600 } }
|
|
33
|
+
execute: { type: shell, command: scripts/long.sh, args: ['{parameters.seconds}'], cancel_grace: 10s }
|
|
34
|
+
on_cancel: [ { type: shell, command: /bin/echo, args: ['on_cancel {run.id}'] } ]
|
|
35
|
+
on_crash: [ { type: shell, command: /bin/echo, args: ['on_crash {run.id}'] } ]
|
|
36
|
+
EOF
|
|
37
|
+
start_long() { curl -s -X POST -H 'X-Adispatch-Request: 1' -H 'Content-Type: application/json' \
|
|
38
|
+
-d "{\"parameters\":{\"seconds\":$1}}" http://127.0.0.1:8787/api/v1/workflows/long/start | jq -r .run.id; }
|
|
39
|
+
run_status() { curl -s http://127.0.0.1:8787/api/v1/runs/$1 | jq '{status, finalReason, lifecycleStatus}'; }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Checkliste
|
|
43
|
+
|
|
44
|
+
| # | Schritt | Erwartung | Ergebnis |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| S1 | `sudo adispatch service install --dry-run --config-dir /etc/adispatchd --data-dir /var/lib/adispatchd --create-user` | Unit mit `/usr/bin/node …/dist/cli/main.js start …`, kein `npx`, `User=adispatchd`, `KillMode=mixed`, `Restart=on-failure`, `TimeoutStopSec=810`; nichts geändert (`id adispatch` → unbekannt, keine Unit-Datei) | |
|
|
47
|
+
| S2 | wie S1 ohne `--dry-run` | Benutzer angelegt (`/usr/sbin/nologin`), `/var/lib/adispatchd` 0750 `adispatchd`, `/etc/adispatchd` `root:adispatchd` 0750/0640, `/etc/default/adispatchd` 0640 (Vorlage), Unit enabled; `systemd-analyze verify /etc/systemd/system/adispatchd.service` ohne Fehler | |
|
|
48
|
+
| S3 | `sudo systemctl start adispatchd; adispatch status --config-dir /etc/adispatchd` | `ready: yes`; `ps -o user= -C node` → `adispatchd`; Dienst kann `/etc/adispatchd` nicht beschreiben (`sudo -u adispatchd touch /etc/adispatchd/x` → Permission denied) | |
|
|
49
|
+
| S4 | **Drain per CLI:** `id=$(start_long 20)`, `adispatch drain --config-dir /etc/adispatchd --wait` | Run läuft zu Ende (`completed`), danach Exit 0; `systemctl status` → `inactive (dead)`, **kein** Neustart (`journalctl` zeigt keinen Restart, `NRestarts=0` in `systemctl show -p NRestarts`) | |
|
|
50
|
+
| S5 | **systemctl stop während eines Runs:** Dienst starten, `id=$(start_long 30)`, `sudo systemctl stop adispatchd` | `stop` blockiert bis der Run fertig ist (Drain; Worker bekam **kein** SIGTERM: kein „got TERM“ im Run-Log), Run `completed`, Exit 0 | |
|
|
51
|
+
| S6 | **Drain-Timeout → Cancel-All:** Unit mit `--drain-timeout 20s --cancel-timeout 30s` neu installieren, `start_long 600`, `systemctl stop` | nach ~20 s Cancel: Worker-Log „got TERM“, Run `cancelled`, `on_cancel` gelaufen, Dienst gestoppt deutlich vor `TimeoutStopSec` | |
|
|
52
|
+
| S7 | **Zweites SIGTERM:** `start_long 600`, `sudo systemctl stop adispatchd &`, dann `sudo systemctl kill --kill-whom=main --signal=SIGTERM adispatchd` | sofortiges Cancel-All, Run `cancelled`, Exit 0 | |
|
|
53
|
+
| S8 | **Cancel-All per CLI:** `start_long 600`, `adispatch cancel-all --config-dir /etc/adispatchd --wait` | Run `cancelled`, `on_cancel`, Exit 0, kein Neustart | |
|
|
54
|
+
| S9 | **App-Absturz:** `start_long 600`, `sudo systemctl kill --kill-whom=main --signal=SIGKILL adispatchd` | systemd tötet die cgroup inkl. Worker (`pgrep -f long.sh` leer), Neustart nach ~5 s (`NRestarts=1`), Recovery: Run `crashed` + `on_crash`, technischer Zähler +1; `/ready` erst nach Recovery | |
|
|
55
|
+
| S10 | **Absturz mit Exit ≠ 0** (z. B. `kill -SEGV <pid>` oder DB-Datei vorübergehend unlesbar machen) | Neustart (`Restart=on-failure`) | |
|
|
56
|
+
| S11 | **Grace länger als Cancel-Timeout:** Unit mit `--drain-timeout 5s --cancel-timeout 5s`, Worker ignoriert TERM (`trap '' TERM`), `cancel_grace: 300s` **ohne** Neuinstallation, `systemctl stop` | `start` warnt beim Start („not smaller than the cancel timeout“); Hauptprozess endet nach ~5 s Drain + 5 s Cancel + bis 10 s Hook-Abbruch (≈ 20–25 s) mit **Exit 0**, ohne dass der Worker SIGKILL bekam; systemd (`KillMode=mixed`) SIGKILLt danach sofort die restliche cgroup inkl. Worker (nicht erst nach `TimeoutStopSec`); nächster Start: Run `crashed` + `on_crash` (kein hängender Lock) | |
|
|
57
|
+
| S11b | **Konfigurationsfehler:** YAML-Syntaxfehler in `config.yaml`, `systemctl restart` | Exit 78, `systemctl status` zeigt `status=78`, **kein** Neustart-Loop (`NRestarts` bleibt) | |
|
|
58
|
+
| S12 | **Boot-Recovery:** `start_long 600`, `sudo reboot` | Dienst startet nach Boot automatisch; Run `crashed` + `on_crash`; Boot-ID-Wechsel → keine fremden Prozesse signalisiert | |
|
|
59
|
+
| S13 | **Update-Migration (Szenario 12):** mit Version N Runs erzeugen, `drain --wait`, Version N+1 (mit neuer Migration) als Tarball `npm install -g ./adispatchd-<N+1>.tgz`, `systemctl start` | `journalctl`: „database backup written before migration“ + Pfad, „database migrated“; `backups/orchestrator-<N>-*.sqlite` vorhanden; alte Runs, Logs, Blocks, config-dir und Env-Datei unverändert; neuer Run funktioniert | |
|
|
60
|
+
| S14 | **Rollback:** Dienst stoppen, Version N installieren, Backup als `adispatchd.sqlite` zurück (inkl. `-wal`/`-shm` entfernen), starten | startet mit Version N; DB mit neuerer Version ohne Rückspielen → Start abgelehnt („newer than this build supports“) | |
|
|
61
|
+
| S15 | **Zweite Instanz:** bei laufendem Dienst `sudo -u adispatchd adispatch start --config-dir /etc/adispatchd --data-dir /var/lib/adispatchd --port 8788` | sofortiger Abbruch „Another orchestrator instance is running on this data directory“ | |
|
|
62
|
+
| S16 | `sudo adispatch service uninstall` | Dienst gedraint und deaktiviert, Unit-Datei weg; config-dir, data-dir, Env-Datei, Benutzer bleiben | |
|
|
63
|
+
|
|
64
|
+
## Protokoll
|
|
65
|
+
|
|
66
|
+
| Datum | Host / Distribution / systemd / Node | Version | Schritte | Beobachtung |
|
|
67
|
+
|---|---|---|---|---|
|
|
68
|
+
| | | | | |
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Betrieb
|
|
2
|
+
|
|
3
|
+
Installation als Dienst, Shutdown/Drain, Updates, Backups, Recovery und Fernzugriff. Spec §4, §13, §14, §15, §20, §21.
|
|
4
|
+
API-Referenz: [api.md](api.md) · Worker-Vertrag: [worker-protocol.md](worker-protocol.md) · Cron: [cron-semantics.md](cron-semantics.md)
|
|
5
|
+
|
|
6
|
+
## Verzeichnisse und Rechte
|
|
7
|
+
|
|
8
|
+
| Pfad (Vorschlag) | Inhalt | Eigentümer / Modus |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `/etc/adispatchd/` (config-dir) | `config.yaml`, `sources.yaml`, `identities.yaml`, `workflows/`, `views/`, `scripts/` | `root:adispatchd`, Verzeichnisse 0750, Dateien 0640, Skripte 0750 |
|
|
11
|
+
| `/etc/default/adispatchd` | Secrets als Umgebungsvariablen (GitHub-App-Keys, Webhook-URLs) | `root:adispatchd`, 0640 |
|
|
12
|
+
| `/var/lib/adispatchd/` (data-dir) | `adispatchd.sqlite` (+ `-wal`/`-shm`), `adispatchd.lock`, `logs/`, `runs/`, `workspaces/`, `backups/` | `adispatchd`, 0750 |
|
|
13
|
+
| Paket, z. B. `/usr/lib/node_modules/adispatchd/` | Code (`dist/`), wird bei Updates ersetzt | `root`, nur lesbar für den Dienst |
|
|
14
|
+
|
|
15
|
+
- **Schreibrecht auf config-dir = Codeausführung** mit den Rechten des Dienstes (Skripte, Kommandos in YAML, Spec §20.2).
|
|
16
|
+
Deshalb gehört config-dir `root`, der Dienst kann seine eigene Konfiguration nicht ändern.
|
|
17
|
+
- **data-dir muss auf einem lokalen Dateisystem liegen (kein NFS/SMB).** Der Instanz-Lock (`adispatchd.lock`) beruht auf
|
|
18
|
+
`fcntl`-Sperren, die auf Netzwerk-Dateisystemen unzuverlässig sind; zwei Instanzen auf demselben data-dir wären möglich.
|
|
19
|
+
- Es läuft genau **eine** Instanz pro data-dir (OS-Sperre, wird bei Prozessende immer freigegeben, auch nach SIGKILL).
|
|
20
|
+
Mehrere Instanzen mit getrennten data-dirs koordinieren sich nicht.
|
|
21
|
+
- `workspace.base_dir` (Default `<data-dir>/workspaces`) muss für den Dienst schreibbar sein; liegt es außerhalb von
|
|
22
|
+
data-dir, Rechte selbst setzen.
|
|
23
|
+
|
|
24
|
+
## Installation als systemd-Dienst (Linux)
|
|
25
|
+
|
|
26
|
+
Voraussetzungen: Linux mit systemd, Node.js ≥ 22.13 (empfohlen 24 LTS) **systemweit** installiert (z. B. `/usr/bin/node`
|
|
27
|
+
aus Distribution/NodeSource – nicht aus `nvm` im Home-Verzeichnis, das der Dienstbenutzer nicht lesen kann).
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. Feste Version global installieren (kein npx im Dienst!).
|
|
31
|
+
sudo npm install -g adispatchd@0.1.0
|
|
32
|
+
# alternativ aus dem Repository-Checkout: npm install && npm pack && sudo npm install -g ./adispatchd-0.1.0.tgz
|
|
33
|
+
|
|
34
|
+
# 2. Konfiguration anlegen (überschreibt nie bestehende Dateien)
|
|
35
|
+
sudo adispatch init --config-dir /etc/adispatchd --data-dir /var/lib/adispatchd
|
|
36
|
+
sudoedit /etc/adispatchd/sources.yaml /etc/adispatchd/identities.yaml # anpassen
|
|
37
|
+
sudo adispatch config validate --config-dir /etc/adispatchd --data-dir /var/lib/adispatchd
|
|
38
|
+
|
|
39
|
+
# 3. Vorschau: Unit-Datei und geplante Schritte (ändert nichts)
|
|
40
|
+
sudo adispatch service install --dry-run \
|
|
41
|
+
--config-dir /etc/adispatchd --data-dir /var/lib/adispatchd --create-user
|
|
42
|
+
|
|
43
|
+
# 4. Installieren: Benutzer anlegen, Rechte setzen, Env-Datei-Vorlage, Unit schreiben, daemon-reload, enable
|
|
44
|
+
sudo adispatch service install \
|
|
45
|
+
--config-dir /etc/adispatchd --data-dir /var/lib/adispatchd --create-user
|
|
46
|
+
|
|
47
|
+
# 5. Secrets eintragen und starten
|
|
48
|
+
sudoedit /etc/default/adispatchd
|
|
49
|
+
sudo systemctl start adispatchd
|
|
50
|
+
adispatch status --config-dir /etc/adispatchd
|
|
51
|
+
journalctl -u adispatchd -f
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Optionen von `service install`: `--user`/`--group` (Default `adispatchd`), `--create-user`, `--name` (Unit-Name),
|
|
55
|
+
`--node`/`--entry` (absolute Pfade, Default: das laufende Node und diese Installation), `--env-file <pfad|none>`,
|
|
56
|
+
`--drain-timeout` (Default 10m), `--cancel-timeout` (Default 2m), `--unit-dir`, `--now` (sofort starten).
|
|
57
|
+
`service install` verweigert `npx`-Cache-Pfade, relative Pfade, TypeScript-Quellen und `root` als Dienstbenutzer.
|
|
58
|
+
Außerhalb von `--dry-run` bricht `service install` ab, wenn `config.yaml` fehlt oder die Konfiguration ungültig ist
|
|
59
|
+
(Schutz vor Tippfehlern, da `chown -R`/`chmod -R` auf config-dir und data-dir laufen). Systemverzeichnisse (`/`, `/etc`,
|
|
60
|
+
`/var`, `/var/lib`, `/usr`, `/home`, `/opt`, `/root`, `/tmp`, …), Pfade direkt unter `/`, Home-Verzeichnisse sowie gleiche
|
|
61
|
+
oder ineinander verschachtelte config-/data-dirs werden abgelehnt (auch über Symlinks). Ohne `--create-user` muss der Benutzer existieren (Hinweis mit passendem `useradd`-Kommando). Eine bestehende Env-Datei wird
|
|
62
|
+
nie überschrieben. `service status` zeigt `systemctl status`; `service uninstall` stoppt (Drain), deaktiviert und entfernt nur
|
|
63
|
+
die Unit-Datei – config-dir, data-dir und Benutzer bleiben. Auf macOS lehnen die Kommandos mit Erklärung ab
|
|
64
|
+
(`--dry-run` funktioniert überall).
|
|
65
|
+
|
|
66
|
+
### Generierte Unit (Beispiel)
|
|
67
|
+
|
|
68
|
+
```ini
|
|
69
|
+
[Service]
|
|
70
|
+
Type=simple
|
|
71
|
+
User=adispatchd
|
|
72
|
+
Group=adispatchd
|
|
73
|
+
ExecStart=/usr/bin/node /usr/lib/node_modules/adispatchd/dist/cli/main.js start --config-dir /etc/adispatchd --data-dir /var/lib/adispatchd --drain-timeout 10m --cancel-timeout 2m
|
|
74
|
+
WorkingDirectory=/var/lib/adispatchd
|
|
75
|
+
EnvironmentFile=-/etc/default/adispatchd
|
|
76
|
+
Environment=NODE_ENV=production
|
|
77
|
+
Restart=on-failure
|
|
78
|
+
RestartSec=5s
|
|
79
|
+
RestartPreventExitStatus=78
|
|
80
|
+
KillMode=mixed
|
|
81
|
+
KillSignal=SIGTERM
|
|
82
|
+
TimeoutStopSec=810
|
|
83
|
+
UMask=0027
|
|
84
|
+
NoNewPrivileges=true
|
|
85
|
+
PrivateTmp=true
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`NoNewPrivileges` verhindert `sudo`/setuid in Workern, `PrivateTmp` gibt dem Dienst ein eigenes `/tmp`. Werden weitere
|
|
89
|
+
Härtungen (`ProtectSystem=`, `ReadWritePaths=`) gewünscht, per Drop-in (`systemctl edit adispatchd`) ergänzen –
|
|
90
|
+
Agent-Skripte brauchen oft Schreibzugriff auf Caches im Home-Verzeichnis. Änderungen direkt in der Unit-Datei überschreibt
|
|
91
|
+
das nächste `service install`.
|
|
92
|
+
|
|
93
|
+
### Stop-/Restart-Vertrag (Spec §14, §24.4)
|
|
94
|
+
|
|
95
|
+
| Ereignis | Verhalten | Exit | systemd |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| `systemctl stop` / `restart` | SIGTERM an den **Hauptprozess** → Drain bis `--drain-timeout`, dann Cancel-All (`on_cancel`) bis `--cancel-timeout`, danach werden Hooks unterbrochen (Recovery setzt sie beim nächsten Start fort) | 0 | gestoppt |
|
|
98
|
+
| zweites SIGTERM (`systemctl kill --kill-whom=main --signal=SIGTERM adispatchd`) | sofort Cancel-All | 0 | gestoppt |
|
|
99
|
+
| drittes Signal | sofortiger Exit (Recovery räumt beim nächsten Start auf) | 1 | – |
|
|
100
|
+
| `adispatch drain` / UI „Drain“ | keine neuen Runs, aktive laufen zu Ende, dann Ende | 0 | inactive, **kein** Neustart |
|
|
101
|
+
| `adispatch cancel-all` / UI „Cancel All“ | alle Runs abbrechen, `on_cancel`, dann Ende | 0 | inactive, **kein** Neustart |
|
|
102
|
+
| Konfigurations-/Startfehler (ungültige YAML, Instanz-Lock belegt, DB-Schema neuer als der Code) | Start abgebrochen | 78 (EX_CONFIG) | **kein** Neustart (`RestartPreventExitStatus=78`) – Fehler beheben, dann `systemctl start` |
|
|
103
|
+
| Absturz / Exit ≠ 0 | – | ≠ 0 | Neustart nach 5 s → Recovery |
|
|
104
|
+
| `TimeoutStopSec` abgelaufen | systemd SIGKILLt die ganze cgroup inkl. Worker | – | gestoppt; nächster Start: Runs `crashed` + `on_crash` |
|
|
105
|
+
|
|
106
|
+
- **`KillMode=mixed`**: SIGTERM geht nur an den Orchestrator, Worker laufen während des Drains weiter. Mit dem Default
|
|
107
|
+
`control-group` bekämen alle Worker sofort SIGTERM (gleiche cgroup trotz eigener Prozessgruppe) → kein Drain möglich.
|
|
108
|
+
- **`TimeoutStopSec`** = Drain-Timeout + Cancel-Timeout + größtes `execute.cancel_grace` der Konfiguration + 60 s Puffer
|
|
109
|
+
(wird bei `service install` aus der Konfiguration berechnet). Die Grace-Zeit (SIGTERM → SIGKILL der Worker-Gruppe) läuft
|
|
110
|
+
**innerhalb** des Cancel-Timeouts – danach werden Hooks unterbrochen und der Orchestrator beendet sich; der Grace-Term ist
|
|
111
|
+
nur Sicherheitsmarge. `cancel_grace` muss daher **kleiner als `--cancel-timeout`** sein, sonst beendet sich der Orchestrator
|
|
112
|
+
ggf. vor dem SIGKILL (dann räumt erst systemd per cgroup ab) – `service install` und `start` warnen.
|
|
113
|
+
- **`Restart=on-failure`**: Ein absichtlicher Shutdown über UI/CLI endet mit Exit 0 und wird **nicht** als Crash neu gestartet.
|
|
114
|
+
- **Lange Runs:** Ein Agent-Run dauert oft länger als der Drain-Timeout. `systemctl stop` kann einen vollständigen Drain daher
|
|
115
|
+
nicht garantieren – für geplante Stopps vorher `adispatch drain --wait` ausführen (beliebig lange, ohne Timeout)
|
|
116
|
+
und erst danach `systemctl stop`/Update/Reboot.
|
|
117
|
+
- **Absturz unter systemd:** Stirbt der Hauptprozess, beendet systemd (bei `KillMode=mixed`) auch alle Worker der cgroup per
|
|
118
|
+
SIGKILL. Die Recovery findet beim Neustart die Prozessgruppen daher i. d. R. bereits beendet und finalisiert die Runs als
|
|
119
|
+
`crashed` mit `on_crash` (zählt als technischer Fehler, ggf. Retry-Block). Verwaiste, noch lebende Worker (Recovery tötet
|
|
120
|
+
sie nur bei sicher verifizierter Identität) entstehen v. a. außerhalb von systemd (lokaler Start, `KillMode=process`).
|
|
121
|
+
- **Reboot:** wie Absturz – alle Worker sind weg → `crashed` + `on_crash`. Geplante Reboots daher vorher drainen.
|
|
122
|
+
|
|
123
|
+
## Pause, Drain, Cancel-All
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
adispatch status --config-dir /etc/adispatchd # Zustand, Limits, Auslastung, Blocks
|
|
127
|
+
adispatch pause --config-dir /etc/adispatchd # keine automatischen Starts (manuelle weiter möglich)
|
|
128
|
+
adispatch resume --config-dir /etc/adispatchd
|
|
129
|
+
adispatch drain --config-dir /etc/adispatchd --wait [--timeout 2h]
|
|
130
|
+
adispatch cancel-all --config-dir /etc/adispatchd --reason "maintenance" --wait
|
|
131
|
+
adispatch config reload --config-dir /etc/adispatchd # atomar; ungültige YAML behält aktive Konfiguration
|
|
132
|
+
adispatch workflow dry-run coder --config-dir /etc/adispatchd # gegen die laufende Instanz, sonst lokal
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Die Kommandos sprechen per HTTP mit der laufenden Instanz: `--url http://127.0.0.1:8787` oder – mit `--config-dir` – Host/Port
|
|
136
|
+
aus `config.yaml` (Wildcard-Binds `0.0.0.0`/`::` werden über Loopback angesprochen). **Als normaler Benutzer (nicht root)
|
|
137
|
+
`--url` verwenden:** das config-dir ist nach `service install` nur für root und die Dienstgruppe lesbar; mit `--config-dir`
|
|
138
|
+
meldet das Kommando dann „not readable (permission denied)“ und fällt auf `http://127.0.0.1:8787` zurück. URLs mit
|
|
139
|
+
Zugangsdaten (`user:pass@`) werden abgelehnt (Exit 2) – Proxy-Authentifizierung unterstützt die CLI nicht; auf dem Host
|
|
140
|
+
selbst die lokale URL nutzen. Sie senden den CSRF-Header
|
|
141
|
+
`X-Adispatch-Request: 1`. Exit-Codes: 0 ok, 1 API-Fehler (z. B. Reload abgelehnt), 2 Aufruffehler, 3 Instanz nicht
|
|
142
|
+
erreichbar, 4 `--wait`-Timeout (der Shutdown läuft im Hintergrund weiter). `workflow dry-run` fällt ohne erreichbare Instanz
|
|
143
|
+
auf die lokale Auswertung zurück (`--local` erzwingt sie; mit explizitem `--url` kein Fallback).
|
|
144
|
+
|
|
145
|
+
## Update (Spec §4, Szenario 12)
|
|
146
|
+
|
|
147
|
+
Updates ersetzen nur den Code; config-dir, data-dir und Env-Datei bleiben unverändert.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
adispatch drain --config-dir /etc/adispatchd --wait # aktive Runs sauber beenden (ohne Zeitlimit)
|
|
151
|
+
sudo systemctl stop adispatchd # falls noch aktiv (nach Drain bereits beendet)
|
|
152
|
+
sudo cp -a /etc/adispatchd /root/adispatchd-config-$(date +%F) # optional, config-dir sichern
|
|
153
|
+
sudo npm install -g adispatchd@<neue-version> # oder Tarball aus dem Checkout (npm pack)
|
|
154
|
+
sudo adispatch config validate --config-dir /etc/adispatchd --data-dir /var/lib/adispatchd
|
|
155
|
+
sudo adispatch service install --config-dir /etc/adispatchd --data-dir /var/lib/adispatchd # nur wenn sich Pfade/Optionen ändern
|
|
156
|
+
sudo systemctl start adispatchd
|
|
157
|
+
adispatch status --config-dir /etc/adispatchd
|
|
158
|
+
journalctl -u adispatchd -n 50 # "database migrated" + Backup-Pfad, "recovery finished"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- **DB-Migrationen** sind versioniert und laufen beim Start automatisch. Existiert die DB und stehen Migrationen aus, wird
|
|
162
|
+
**vorher** eine konsistente Kopie geschrieben: `data-dir/backups/orchestrator-<alte-version>-<zeitstempel>.sqlite`
|
|
163
|
+
(`VACUUM INTO`, auch im WAL-Modus konsistent). Schlägt eine Migration fehl, wird sie zurückgerollt und der Start abgebrochen.
|
|
164
|
+
- Eine DB mit **neuerer** Schemaversion als der Code kennt wird abgelehnt (kein stilles Downgrade).
|
|
165
|
+
- **Rollback:** Dienst stoppen, alte Paketversion installieren, `adispatchd.sqlite`, `adispatchd.sqlite-wal` und
|
|
166
|
+
`adispatchd.sqlite-shm` beiseitelegen, das passende Backup als `adispatchd.sqlite` zurückkopieren (Eigentümer
|
|
167
|
+
`adispatchd`), starten. Runs nach dem Backup-Zeitpunkt fehlen dann in der Historie (ihre Logdateien bleiben).
|
|
168
|
+
- Alte Dateien unter `backups/` werden nicht automatisch gelöscht.
|
|
169
|
+
|
|
170
|
+
## Backups
|
|
171
|
+
|
|
172
|
+
| Was | Wie |
|
|
173
|
+
|---|---|
|
|
174
|
+
| config-dir | Versionsverwaltung (git) oder Dateikopie; enthält keine Secrets |
|
|
175
|
+
| Env-Datei | sicher (verschlüsselt) aufbewahren – enthält Private Keys |
|
|
176
|
+
| `adispatchd.sqlite` | im laufenden Betrieb konsistent: `sqlite3 /var/lib/adispatchd/adispatchd.sqlite ".backup '/backup/orch.sqlite'"` (bzw. `VACUUM INTO`); bei gestopptem Dienst auch Dateikopie inkl. `-wal`/`-shm` |
|
|
177
|
+
| `logs/`, `runs/` | normale Dateikopie (DB und Logdateien bilden gemeinsam das Run-Journal) |
|
|
178
|
+
| `workspaces/` | meist nicht nötig (Arbeitskopien der Worker) |
|
|
179
|
+
|
|
180
|
+
**`adispatchd.lock` und `adispatchd.lock.json` nie öffnen, kopieren, archivieren oder wiederherstellen.** Die Instanzsperre
|
|
181
|
+
ist eine POSIX-`fcntl`-Sperre auf `adispatchd.lock`; sie gehört dem Orchestrator-Prozess und fällt, sobald *dieser* Prozess
|
|
182
|
+
irgendeinen Deskriptor auf die Datei schließt – deshalb öffnet der Orchestrator sie selbst (auch bei Retention/Backups) nie
|
|
183
|
+
ein zweites Mal. Externe Werkzeuge, die die Datei als SQLite-DB öffnen (`sqlite3`, Backup-Agenten mit SQLite-Support),
|
|
184
|
+
konkurrieren um die Sperre und scheitern bzw. blockieren; eine kopierte Lock-Datei ist wertlos. Beim Sichern ausschließen, z. B.:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
rsync -a --exclude 'adispatchd.lock*' --exclude 'adispatchd.sqlite*' /var/lib/adispatchd/ /backup/orch-data/
|
|
188
|
+
sqlite3 /var/lib/adispatchd/adispatchd.sqlite ".backup '/backup/orch-data/adispatchd.sqlite'"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Wiederherstellen nur bei gestopptem Dienst; die Lock-Dateien legt der Orchestrator selbst neu an.
|
|
192
|
+
|
|
193
|
+
## Recovery nach Absturz
|
|
194
|
+
|
|
195
|
+
Beim Start erfasst die Recovery alle zuvor aktiven Runs, **bevor** der Scheduler startet (`/ready` erst danach `200`):
|
|
196
|
+
|
|
197
|
+
- noch lebende, sicher identifizierte Worker-Prozessgruppen (PID + Startzeit/Boot-ID) werden beendet; verschwundene Runs
|
|
198
|
+
werden `crashed` mit `on_crash` (zählt als technischer Fehler); halb abgearbeitete Lifecycle-Ketten werden fortgesetzt
|
|
199
|
+
(bereits erfolgreiche Actions laufen nicht erneut; Kommentare werden über Marker abgeglichen).
|
|
200
|
+
- **`recovery_pending`**: Prozess nicht sicher identifizierbar oder nicht beendbar → der Run bleibt sichtbar aktiv, hält
|
|
201
|
+
seinen Lock (kein Doppelstart) und erzeugt die Notification `recovery_failed`. Lösung: Prozess prüfen/beenden, dann in der
|
|
202
|
+
UI „Retry Recovery“ bzw. `POST /api/v1/runs/<id>/recovery/retry` (mit `{"assumeGone": true}`, wenn sicher ist, dass der
|
|
203
|
+
Prozess weg ist).
|
|
204
|
+
- Retry-Blocks (UI „Blocked“), fehlgeschlagene Lifecycle-Actions (UI „Failed Actions“, Retry/Skip) und
|
|
205
|
+
Benachrichtigungs-Fehlzustellungen bleiben über Neustarts erhalten.
|
|
206
|
+
|
|
207
|
+
## Fernzugriff, Reverse Proxy (Spec §15, §20.1)
|
|
208
|
+
|
|
209
|
+
V1 hat **keine Anmeldung**. Standard ist `server.host: 127.0.0.1`. Bei einem Nicht-Loopback-Bind warnt `start` deutlich.
|
|
210
|
+
|
|
211
|
+
- Zugriff von außen nur über einen **authentifizierenden** Reverse Proxy (z. B. nginx/Caddy mit OIDC über `oauth2-proxy`,
|
|
212
|
+
Basic-Auth über TLS), VPN oder Tailscale. Der Proxy muss **jede** Anfrage authentifizieren, auch `/api/v1/events` (SSE).
|
|
213
|
+
- Den öffentlichen Namen des Proxys in `server.allowed_hosts` eintragen (DNS-Rebinding-Schutz: jede Anfrage braucht einen
|
|
214
|
+
erlaubten `Host`, ein gesendeter `Origin` muss exakt passen); der Proxy muss den ursprünglichen `Host` weiterreichen.
|
|
215
|
+
- Keine CORS-Header am Proxy hinzufügen; mutierende Requests brauchen `X-Adispatch-Request: 1` (die UI setzt ihn).
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
# config.yaml
|
|
219
|
+
server:
|
|
220
|
+
host: 127.0.0.1
|
|
221
|
+
port: 8787
|
|
222
|
+
allowed_hosts: [orchestrator.example.com]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```nginx
|
|
226
|
+
server {
|
|
227
|
+
listen 443 ssl;
|
|
228
|
+
server_name orchestrator.example.com;
|
|
229
|
+
# ssl_certificate …;
|
|
230
|
+
auth_basic "Agent Dispatch Daemon"; # oder auth_request an oauth2-proxy
|
|
231
|
+
auth_basic_user_file /etc/nginx/orchestrator.htpasswd;
|
|
232
|
+
location / {
|
|
233
|
+
proxy_pass http://127.0.0.1:8787;
|
|
234
|
+
proxy_set_header Host $host; # muss in server.allowed_hosts stehen
|
|
235
|
+
proxy_http_version 1.1;
|
|
236
|
+
proxy_buffering off; # SSE (/api/v1/events)
|
|
237
|
+
proxy_read_timeout 1h;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Tailscale: `tailscale serve --bg http://127.0.0.1:8787` und den MagicDNS-Namen in `allowed_hosts` eintragen.
|
|
243
|
+
|
|
244
|
+
## Observability
|
|
245
|
+
|
|
246
|
+
- `GET /health` (Prozess lebt), `GET /ready` (DB ok, Recovery fertig, Scheduler läuft, kein Shutdown), `adispatch status`.
|
|
247
|
+
- Dienstlog: JSON-Zeilen auf stderr → `journalctl -u adispatchd` (Secrets werden redigiert).
|
|
248
|
+
- Run-Logs: `data-dir/logs/<run-id>/{stdout,stderr}.log`, Events/Status in SQLite, live per SSE.
|
|
249
|
+
- Aufbewahrung über `retention` in `config.yaml` (Default: erfolgreiche Runs 30 d, fehlgeschlagene 90 d, Block-Runs nie,
|
|
250
|
+
100 MB Log pro Run, Prüfung alle 24 h). Aktive Runs, offene Blocks und unvollständige Aktionsketten werden nie gelöscht.
|
|
251
|
+
|
|
252
|
+
## Fehlerbilder
|
|
253
|
+
|
|
254
|
+
| Meldung | Ursache / Lösung |
|
|
255
|
+
|---|---|
|
|
256
|
+
| „Another orchestrator instance is running on this data directory (pid …)“ | Eine andere Instanz nutzt das data-dir. |
|
|
257
|
+
| `cannot listen: EADDRINUSE` | Port belegt → `server.port` ändern oder `start --port`. |
|
|
258
|
+
| `Configuration … is invalid` | `config validate` zeigt Datei:Zeile: Pfad: Meldung. |
|
|
259
|
+
| „Database schema version … is newer than this build supports“ | data-dir stammt von einer neueren Version → passende Version installieren oder Backup einspielen. |
|
|
260
|
+
| `ExperimentalWarning: SQLite` | Node 22: `node:sqlite` ist experimentell; Node 24 verwenden. |
|
|
261
|
+
| `GITHUB_…_PRIVATE_KEY is not set` | Env-Datei fehlt/unvollständig (`systemctl show adispatchd -p EnvironmentFiles`). |
|
|
262
|
+
|
|
263
|
+
## Bekannte Grenzen
|
|
264
|
+
|
|
265
|
+
- Die Source-ID ist Teil des Item-Schlüssels (`<source>:<nummer>`): Umbenennen einer Source trennt die Run-Historie und
|
|
266
|
+
Retry-Blocks von den Items.
|
|
267
|
+
- Prozesse außerhalb der Worker-Prozessgruppe (eigene Session, Container) beendet der Orchestrator nicht (siehe
|
|
268
|
+
[worker-protocol.md](worker-protocol.md#abbruch-timeout-prozesse)).
|
|
269
|
+
- Linux-spezifisches Verhalten (systemd, cgroups, Prozessidentität über `/proc`) ist manuell zu prüfen:
|
|
270
|
+
[manual-tests/systemd.md](manual-tests/systemd.md), [manual-tests/process-supervision.md](manual-tests/process-supervision.md);
|
|
271
|
+
GitHub-Annahmen: [manual-tests/github.md](manual-tests/github.md).
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Worker-Protokoll
|
|
2
|
+
|
|
3
|
+
Vertrag zwischen Orchestrator und Haupt-Worker (`execute`) bzw. Shell-Lifecycle-Actions. Spec §8.2, §9, §13, §20.
|
|
4
|
+
Implementierung: `src/core/worker-protocol.ts`, `src/executors/`.
|
|
5
|
+
|
|
6
|
+
## Start
|
|
7
|
+
|
|
8
|
+
- Jeder Run bekommt einen **eigenen Workspace** (Default `{workspace.base_dir}/{workflow.id}/{run.id}`, `create_directory: true`);
|
|
9
|
+
der Orchestrator legt ihn an. Klonen, Git-Worktrees, `npm install`, Docker … erledigt das Skript selbst.
|
|
10
|
+
- Das Kommando wird **ohne Shell** mit Argumentliste gestartet (`command` + `args`); Platzhalter in `args`/`env` werden vorher
|
|
11
|
+
ersetzt (feste Platzhalterliste, unbekannte sind Konfigurationsfehler). Wer Shell-Syntax braucht, schreibt ein Skript.
|
|
12
|
+
- Eigene Prozessgruppe/Session (`setsid`), stdin = `/dev/null`, cwd = Workspace.
|
|
13
|
+
|
|
14
|
+
## Input
|
|
15
|
+
|
|
16
|
+
### Umgebung (Minimalumgebung, Spec §9.1/§20.3)
|
|
17
|
+
|
|
18
|
+
Der Worker erbt **nicht** die Umgebung des Orchestrators. Er bekommt nur:
|
|
19
|
+
|
|
20
|
+
| Variable | Inhalt |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `PATH`, `HOME`, `LANG`, `TZ`, `USER`, `LOGNAME`, `TMPDIR` | aus der Controller-Umgebung (Fallback-`PATH` `/usr/local/bin:/usr/bin:/bin`) |
|
|
23
|
+
| `execute.env` | explizite Werte aus der Workflow-YAML (keine Secrets!) |
|
|
24
|
+
| `execute.secrets` | z. B. `GH_TOKEN: { identity: coder }` → kurzlebiges GitHub-App-Installation-Token, nur im Speicher |
|
|
25
|
+
| `ADISPATCH_RUN_ID` | stabile, eindeutige Run-ID (`run-YYYYMMDDTHHMMSSmmmZ-xxxxxxxx`) |
|
|
26
|
+
| `ADISPATCH_WORKFLOW_ID` | Workflow-ID |
|
|
27
|
+
| `ADISPATCH_SOURCE_TYPE`, `ADISPATCH_SOURCE_ID`, `ADISPATCH_SOURCE_ITEM_ID`, `ADISPATCH_SOURCE_URL` | nur bei Runs mit Source-Item (z. B. `github`, `project_a`, `123`, Issue-URL) |
|
|
28
|
+
| `ADISPATCH_CONTEXT_FILE` | Pfad der Kontextdatei (JSON, s. u.) |
|
|
29
|
+
| `ADISPATCH_RESULT_FILE` | Pfad, an den der Worker optional sein Ergebnis schreibt |
|
|
30
|
+
| `ADISPATCH_WORKSPACE` | absoluter Workspace-Pfad |
|
|
31
|
+
|
|
32
|
+
`ADISPATCH_*` kann durch `env`/`secrets` nicht überschrieben werden (reservierter Präfix).
|
|
33
|
+
|
|
34
|
+
### Kontextdatei `ADISPATCH_CONTEXT_FILE` (`data-dir/runs/<run-id>/context.json`, Modus 0600)
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"schema_version": 1,
|
|
39
|
+
"run": { "id": "run-…", "trigger": "source", "started_at": "2026-10-16T19:00:00Z" },
|
|
40
|
+
"workflow": { "id": "coder-project-a", "name": "Coder – Project A" },
|
|
41
|
+
"source": {
|
|
42
|
+
"id": "project_a", "type": "github", "item_id": "123", "item_key": "project_a:123",
|
|
43
|
+
"url": "https://github.com/example/project-a/issues/123",
|
|
44
|
+
"attributes": { "title": "Implement OAuth", "labels": ["coding"] }
|
|
45
|
+
},
|
|
46
|
+
"parameters": {},
|
|
47
|
+
"workspace": { "path": "/var/lib/adispatchd/workspaces/coder-project-a/run-…" }
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`source` fehlt bei Cron-/Manual-Runs ohne Item; `run.scheduled_for` gibt es bei Cron-Runs; `parameters` enthält die validierten
|
|
52
|
+
Parameter manueller Starts (inkl. Defaults). Die Datei enthält **nie** Secrets.
|
|
53
|
+
|
|
54
|
+
## Output
|
|
55
|
+
|
|
56
|
+
### Exit-Code und Ergebnisdatei (verbindlich, Spec §8.2)
|
|
57
|
+
|
|
58
|
+
| Exit-Code | Ergebnisdatei | Ergebnis |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `0` | `"result": "success"` | `completed` / `success` → `on_success` |
|
|
61
|
+
| `0` | `"result": "failure"` | `completed` / `failure` → `on_failure` (fachlich, **kein** technischer Fehler) |
|
|
62
|
+
| `0` | keine | `completed` / `success` (einfache Shell-Skripte) |
|
|
63
|
+
| `0` | ungültig / zu groß / kein reguläres File | `crashed` (Protokollfehler) → `on_crash` |
|
|
64
|
+
| `≠ 0` | beliebig (wird ignoriert) | `crashed` → `on_crash` |
|
|
65
|
+
| Signal | beliebig | `crashed` (außer durch Cancel/Timeout) |
|
|
66
|
+
| Start unmöglich (ENOENT, kein Exec-Recht, `on_start` gescheitert) | – | `start_error` → `on_start_error` |
|
|
67
|
+
| Timeout (`max_duration`, `idle_timeout`) | beliebig | `timed_out` → `on_timeout` |
|
|
68
|
+
| kontrollierter Abbruch (UI/API/Cancel-All) | beliebig | `cancelled` → `on_cancel` |
|
|
69
|
+
|
|
70
|
+
- **Ein fachliches `failure` gibt es nur mit gültiger Ergebnisdatei.** Ein Exit-Code allein ist nie „Needs Maintainer“ – ein
|
|
71
|
+
versehentliches `exit 1` (z. B. `git clone` scheitert) ist ein technischer Fehler.
|
|
72
|
+
- Technische Fehler (`crashed`, `start_error`, `timed_out`) zählen pro Workflow+Item; nach `retry.max_technical_failures`
|
|
73
|
+
(Default 3) aufeinanderfolgenden Fehlern entsteht ein **Retry-Block** (UI „Blocked“ → „Remove Block“). `failure` und
|
|
74
|
+
`cancelled` zählen nicht; ein Erfolg setzt den Zähler zurück.
|
|
75
|
+
|
|
76
|
+
Ergebnisdatei (max. 1 MiB, **atomar schreiben**: temporäre Datei + `mv`):
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"schema_version": 1,
|
|
81
|
+
"result": "failure",
|
|
82
|
+
"message": "OAuth-Anbieter nicht spezifiziert",
|
|
83
|
+
"details": { "missing_information": ["OAuth provider", "Scopes"] },
|
|
84
|
+
"artifacts": [ { "type": "url", "name": "Draft PR", "url": "https://github.com/example/project-a/pull/142" } ]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| Feld | Pflicht | Regel |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `schema_version` | ja | `1` |
|
|
91
|
+
| `result` | ja | `success` \| `failure` |
|
|
92
|
+
| `message` | nein | ≤ 10 000 Zeichen; als `{result.message}` in Hooks verfügbar |
|
|
93
|
+
| `details` | nein | beliebiges Objekt |
|
|
94
|
+
| `artifacts` | nein | ≤ 100 Einträge `{type, name?, url?}` |
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
tmp="${ADISPATCH_RESULT_FILE}.tmp"
|
|
98
|
+
printf '%s\n' '{"schema_version":1,"result":"success","message":"PR opened"}' > "$tmp"
|
|
99
|
+
mv "$tmp" "$ADISPATCH_RESULT_FILE"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Logs und Events (Spec §9.3)
|
|
103
|
+
|
|
104
|
+
- stdout und stderr werden getrennt und unverändert nach `data-dir/logs/<run-id>/{stdout,stderr}.log` geschrieben
|
|
105
|
+
(best-effort Redaction bekannter Secrets) und live per SSE an die UI gesendet. Limit `retention.max_log_size_per_run`
|
|
106
|
+
(stdout+stderr zusammen, Default 100 MB): danach wird abgeschnitten und das als Diagnose-Event markiert.
|
|
107
|
+
- Strukturierte Events: eine Zeile `@@ADISPATCH_EVENT {json}` auf stdout oder stderr, JSON-Objekt mit `type`, max. 64 KiB:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
@@ADISPATCH_EVENT {"type":"status","message":"Tests laufen"}
|
|
111
|
+
@@ADISPATCH_EVENT {"type":"progress","value":60}
|
|
112
|
+
@@ADISPATCH_EVENT {"type":"question","message":"Welcher OAuth-Anbieter?"}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Events sind **nur informativ** (keine Workflow-Transitionen); `status`-Events mit `message` erscheinen als letzte
|
|
116
|
+
Statusmeldung auf Run-Karten. Ungültige Zeilen bleiben im Log und erzeugen eine Parser-Warnung.
|
|
117
|
+
- `idle_timeout` misst die Zeit seit der letzten Ausgabe (auch Events). Lange stille Phasen → regelmäßig ein
|
|
118
|
+
`progress`-/`status`-Event ausgeben.
|
|
119
|
+
|
|
120
|
+
## Abbruch, Timeout, Prozesse
|
|
121
|
+
|
|
122
|
+
- Cancel/Timeout: `SIGTERM` an die **ganze Prozessgruppe**, nach `execute.cancel_grace` (Default 30 s) `SIGKILL`; der Run
|
|
123
|
+
gilt erst als beendet, wenn die Gruppe nachweislich weg ist.
|
|
124
|
+
- Bleiben nach normalem Exit noch Prozesse der Gruppe übrig (`cmd &` ohne `wait`), werden sie beendet.
|
|
125
|
+
- Prozesse **außerhalb** der Gruppe (eigene Session via `setsid`, `nohup … &` mit neuer Session, Docker-Container) erfasst
|
|
126
|
+
der Orchestrator nicht – das Skript ist für deren Cleanup verantwortlich (`trap '…' TERM EXIT`, `docker run --rm --init`).
|
|
127
|
+
- Nach einem Absturz des Orchestrators beendet die Recovery verwaiste Worker nur bei sicher verifizierter Identität
|
|
128
|
+
(PID + Startzeit/Boot-ID); sonst `recovery_pending` (sichtbar, kein Doppelstart). Unter systemd beendet systemd die
|
|
129
|
+
Worker beim Absturz ohnehin mit der cgroup (siehe [operations.md](operations.md#systemd)).
|
|
130
|
+
|
|
131
|
+
## Sicherheit
|
|
132
|
+
|
|
133
|
+
Worker und Shell-Actions sind **vertrauenswürdiger Konfigurationscode** mit den OS-Rechten des Dienstes – es gibt keine
|
|
134
|
+
Sandbox. Ein Worker kann ihm übergebene Secrets (z. B. `GH_TOKEN`) ausgeben; die Redaction in Logs ist best-effort
|
|
135
|
+
(transformierte Secrets, z. B. base64, werden nicht erkannt). Nur benötigte Secrets deklarieren, App-Rechte minimal halten.
|
|
136
|
+
Event-/Result-/Log-Inhalte werden in API/UI als untrusted Text behandelt.
|
|
137
|
+
|
|
138
|
+
## Shell-Lifecycle-Actions und Prüfskripte
|
|
139
|
+
|
|
140
|
+
| Art | Erfolg | Kontext |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `shell`-Action in Hooks | Exit 0 (Ergebnisdatei wird ignoriert); sonst Retry gemäß `lifecycle.action_retry` | `ADISPATCH_CONTEXT_FILE` mit `kind: lifecycle_action`, Run, Hook, Action, `result` (außer `on_start`), Minimal-Env + `env` + `ADISPATCH_*`, **keine Secrets** |
|
|
143
|
+
| `conditions.*`, `shell_filter` | Exit 0 = passt, 1 = passt nicht, ≥ 2/Timeout/Signal = Prüffehler (`unknown`, kein Start) | `ADISPATCH_CHECK_CONTEXT_FILE` (Workflow bzw. Item) |
|
|
144
|
+
|
|
145
|
+
Die Exit-Code-Semantik 0/1/≥2 gilt **nur** für Prüfskripte, nicht für den Haupt-Worker. Dry-Runs führen Prüfskripte nie aus.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Global settings (Spec §5.1)
|
|
2
|
+
server:
|
|
3
|
+
host: 127.0.0.1
|
|
4
|
+
port: 8787
|
|
5
|
+
# Extra host names accepted in Host/Origin headers (e.g. the public name of an
|
|
6
|
+
# authenticating reverse proxy); loopback names are always accepted.
|
|
7
|
+
allowed_hosts: []
|
|
8
|
+
|
|
9
|
+
scheduler:
|
|
10
|
+
polling_interval: 30s
|
|
11
|
+
max_concurrent: 5
|
|
12
|
+
|
|
13
|
+
workspace:
|
|
14
|
+
base_dir: /var/lib/adispatchd/workspaces
|
|
15
|
+
cleanup: on_success # always | on_success | never
|
|
16
|
+
|
|
17
|
+
lifecycle:
|
|
18
|
+
action_retry:
|
|
19
|
+
max_attempts: 3
|
|
20
|
+
delay: 30s
|
|
21
|
+
|
|
22
|
+
retention:
|
|
23
|
+
completed_runs: 30d
|
|
24
|
+
failed_runs: 90d
|
|
25
|
+
blocked_runs: never
|
|
26
|
+
max_log_size_per_run: 100MB
|
|
27
|
+
cleanup_interval: 24h
|
|
28
|
+
|
|
29
|
+
notifications:
|
|
30
|
+
# Admin/orchestrator notifications (Spec §18). Channels are optional, e.g.:
|
|
31
|
+
# channels:
|
|
32
|
+
# - type: webhook
|
|
33
|
+
# url_env: SLACK_WEBHOOK_URL
|
|
34
|
+
# headers_env: { Authorization: WEBHOOK_AUTH_HEADER }
|
|
35
|
+
on: [retry_block_created, lifecycle_action_failed, recovery_failed]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# identities.yaml (Spec §5.3) – private keys are read from the named
|
|
2
|
+
# environment variables at runtime, never stored here.
|
|
3
|
+
identities:
|
|
4
|
+
maintainer:
|
|
5
|
+
type: github_app
|
|
6
|
+
app_id: 100001
|
|
7
|
+
installation_id: 200001
|
|
8
|
+
private_key_env: GITHUB_MAINTAINER_PRIVATE_KEY
|
|
9
|
+
coder:
|
|
10
|
+
type: github_app
|
|
11
|
+
app_id: 100002
|
|
12
|
+
installation_id: 200002
|
|
13
|
+
private_key_env: GITHUB_CODER_PRIVATE_KEY
|
|
14
|
+
reviewer:
|
|
15
|
+
type: github_app
|
|
16
|
+
app_id: 100003
|
|
17
|
+
installation_id: 200003
|
|
18
|
+
private_key_env: GITHUB_REVIEWER_PRIVATE_KEY
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# views/needs-maintainer.yaml (Spec §17)
|
|
2
|
+
id: needs-maintainer
|
|
3
|
+
title: Needs Maintainer
|
|
4
|
+
sources: [project_a, project_b]
|
|
5
|
+
filter:
|
|
6
|
+
all:
|
|
7
|
+
- { field: labels, operator: contains, value: needs-maintainer }
|
|
8
|
+
fields:
|
|
9
|
+
- { field: repository, label: Repository }
|
|
10
|
+
- { field: number, label: Issue }
|
|
11
|
+
- { field: title, label: Titel, link: url }
|
|
12
|
+
- { field: run.last_workflow, label: Letzter Agent }
|
|
13
|
+
- { field: run.last_result.message, label: Ergebnis }
|
|
14
|
+
- { field: updated_at, label: Aktualisiert }
|
|
15
|
+
sort: { field: updated_at, direction: desc }
|