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.
Files changed (396) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/api/errors.d.ts +27 -0
  4. package/dist/api/errors.js +80 -0
  5. package/dist/api/errors.js.map +1 -0
  6. package/dist/api/index.d.ts +10 -0
  7. package/dist/api/index.js +11 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/api/logs.d.ts +30 -0
  10. package/dist/api/logs.js +88 -0
  11. package/dist/api/logs.js.map +1 -0
  12. package/dist/api/serialize.d.ts +65 -0
  13. package/dist/api/serialize.js +135 -0
  14. package/dist/api/serialize.js.map +1 -0
  15. package/dist/api/server.d.ts +89 -0
  16. package/dist/api/server.js +751 -0
  17. package/dist/api/server.js.map +1 -0
  18. package/dist/api/sse.d.ts +90 -0
  19. package/dist/api/sse.js +225 -0
  20. package/dist/api/sse.js.map +1 -0
  21. package/dist/app.d.ts +173 -0
  22. package/dist/app.js +488 -0
  23. package/dist/app.js.map +1 -0
  24. package/dist/cli/config-validate.d.ts +6 -0
  25. package/dist/cli/config-validate.js +39 -0
  26. package/dist/cli/config-validate.js.map +1 -0
  27. package/dist/cli/control.d.ts +31 -0
  28. package/dist/cli/control.js +186 -0
  29. package/dist/cli/control.js.map +1 -0
  30. package/dist/cli/http-client.d.ts +57 -0
  31. package/dist/cli/http-client.js +174 -0
  32. package/dist/cli/http-client.js.map +1 -0
  33. package/dist/cli/index.d.ts +14 -0
  34. package/dist/cli/index.js +14 -0
  35. package/dist/cli/index.js.map +1 -0
  36. package/dist/cli/init-templates.d.ts +20 -0
  37. package/dist/cli/init-templates.js +275 -0
  38. package/dist/cli/init-templates.js.map +1 -0
  39. package/dist/cli/init.d.ts +16 -0
  40. package/dist/cli/init.js +107 -0
  41. package/dist/cli/init.js.map +1 -0
  42. package/dist/cli/main.d.ts +2 -0
  43. package/dist/cli/main.js +25 -0
  44. package/dist/cli/main.js.map +1 -0
  45. package/dist/cli/program.d.ts +102 -0
  46. package/dist/cli/program.js +133 -0
  47. package/dist/cli/program.js.map +1 -0
  48. package/dist/cli/service.d.ts +27 -0
  49. package/dist/cli/service.js +252 -0
  50. package/dist/cli/service.js.map +1 -0
  51. package/dist/cli/start.d.ts +22 -0
  52. package/dist/cli/start.js +133 -0
  53. package/dist/cli/start.js.map +1 -0
  54. package/dist/cli/workflow-dry-run.d.ts +13 -0
  55. package/dist/cli/workflow-dry-run.js +144 -0
  56. package/dist/cli/workflow-dry-run.js.map +1 -0
  57. package/dist/config/field-catalog.d.ts +33 -0
  58. package/dist/config/field-catalog.js +172 -0
  59. package/dist/config/field-catalog.js.map +1 -0
  60. package/dist/config/filter-schema.d.ts +15 -0
  61. package/dist/config/filter-schema.js +110 -0
  62. package/dist/config/filter-schema.js.map +1 -0
  63. package/dist/config/index.d.ts +15 -0
  64. package/dist/config/index.js +16 -0
  65. package/dist/config/index.js.map +1 -0
  66. package/dist/config/issues.d.ts +27 -0
  67. package/dist/config/issues.js +37 -0
  68. package/dist/config/issues.js.map +1 -0
  69. package/dist/config/loader.d.ts +32 -0
  70. package/dist/config/loader.js +250 -0
  71. package/dist/config/loader.js.map +1 -0
  72. package/dist/config/manager.d.ts +153 -0
  73. package/dist/config/manager.js +276 -0
  74. package/dist/config/manager.js.map +1 -0
  75. package/dist/config/model.d.ts +334 -0
  76. package/dist/config/model.js +2 -0
  77. package/dist/config/model.js.map +1 -0
  78. package/dist/config/normalize.d.ts +20 -0
  79. package/dist/config/normalize.js +200 -0
  80. package/dist/config/normalize.js.map +1 -0
  81. package/dist/config/parameters.d.ts +37 -0
  82. package/dist/config/parameters.js +71 -0
  83. package/dist/config/parameters.js.map +1 -0
  84. package/dist/config/public.d.ts +6 -0
  85. package/dist/config/public.js +18 -0
  86. package/dist/config/public.js.map +1 -0
  87. package/dist/config/schemas.d.ts +538 -0
  88. package/dist/config/schemas.js +321 -0
  89. package/dist/config/schemas.js.map +1 -0
  90. package/dist/config/snapshot.d.ts +38 -0
  91. package/dist/config/snapshot.js +57 -0
  92. package/dist/config/snapshot.js.map +1 -0
  93. package/dist/config/validate.d.ts +33 -0
  94. package/dist/config/validate.js +346 -0
  95. package/dist/config/validate.js.map +1 -0
  96. package/dist/core/clock.d.ts +59 -0
  97. package/dist/core/clock.js +133 -0
  98. package/dist/core/clock.js.map +1 -0
  99. package/dist/core/duration.d.ts +19 -0
  100. package/dist/core/duration.js +93 -0
  101. package/dist/core/duration.js.map +1 -0
  102. package/dist/core/errors.d.ts +62 -0
  103. package/dist/core/errors.js +72 -0
  104. package/dist/core/errors.js.map +1 -0
  105. package/dist/core/ids.d.ts +57 -0
  106. package/dist/core/ids.js +88 -0
  107. package/dist/core/ids.js.map +1 -0
  108. package/dist/core/index.d.ts +13 -0
  109. package/dist/core/index.js +14 -0
  110. package/dist/core/index.js.map +1 -0
  111. package/dist/core/logger.d.ts +100 -0
  112. package/dist/core/logger.js +207 -0
  113. package/dist/core/logger.js.map +1 -0
  114. package/dist/core/paths.d.ts +84 -0
  115. package/dist/core/paths.js +138 -0
  116. package/dist/core/paths.js.map +1 -0
  117. package/dist/core/template.d.ts +136 -0
  118. package/dist/core/template.js +261 -0
  119. package/dist/core/template.js.map +1 -0
  120. package/dist/core/types.d.ts +544 -0
  121. package/dist/core/types.js +172 -0
  122. package/dist/core/types.js.map +1 -0
  123. package/dist/core/worker-protocol.d.ts +102 -0
  124. package/dist/core/worker-protocol.js +75 -0
  125. package/dist/core/worker-protocol.js.map +1 -0
  126. package/dist/executors/classify.d.ts +25 -0
  127. package/dist/executors/classify.js +54 -0
  128. package/dist/executors/classify.js.map +1 -0
  129. package/dist/executors/dispatcher.d.ts +35 -0
  130. package/dist/executors/dispatcher.js +68 -0
  131. package/dist/executors/dispatcher.js.map +1 -0
  132. package/dist/executors/env.d.ts +43 -0
  133. package/dist/executors/env.js +66 -0
  134. package/dist/executors/env.js.map +1 -0
  135. package/dist/executors/event-parser.d.ts +41 -0
  136. package/dist/executors/event-parser.js +106 -0
  137. package/dist/executors/event-parser.js.map +1 -0
  138. package/dist/executors/index.d.ts +12 -0
  139. package/dist/executors/index.js +13 -0
  140. package/dist/executors/index.js.map +1 -0
  141. package/dist/executors/log-capture.d.ts +57 -0
  142. package/dist/executors/log-capture.js +155 -0
  143. package/dist/executors/log-capture.js.map +1 -0
  144. package/dist/executors/process-identity.d.ts +124 -0
  145. package/dist/executors/process-identity.js +283 -0
  146. package/dist/executors/process-identity.js.map +1 -0
  147. package/dist/executors/shell-executor.d.ts +35 -0
  148. package/dist/executors/shell-executor.js +478 -0
  149. package/dist/executors/shell-executor.js.map +1 -0
  150. package/dist/executors/stream-redactor.d.ts +40 -0
  151. package/dist/executors/stream-redactor.js +89 -0
  152. package/dist/executors/stream-redactor.js.map +1 -0
  153. package/dist/executors/workspace.d.ts +10 -0
  154. package/dist/executors/workspace.js +47 -0
  155. package/dist/executors/workspace.js.map +1 -0
  156. package/dist/filters/evaluate.d.ts +77 -0
  157. package/dist/filters/evaluate.js +187 -0
  158. package/dist/filters/evaluate.js.map +1 -0
  159. package/dist/filters/fields.d.ts +35 -0
  160. package/dist/filters/fields.js +75 -0
  161. package/dist/filters/fields.js.map +1 -0
  162. package/dist/filters/index.d.ts +11 -0
  163. package/dist/filters/index.js +12 -0
  164. package/dist/filters/index.js.map +1 -0
  165. package/dist/filters/iso-date.d.ts +10 -0
  166. package/dist/filters/iso-date.js +42 -0
  167. package/dist/filters/iso-date.js.map +1 -0
  168. package/dist/filters/predicate.d.ts +24 -0
  169. package/dist/filters/predicate.js +178 -0
  170. package/dist/filters/predicate.js.map +1 -0
  171. package/dist/filters/priority.d.ts +51 -0
  172. package/dist/filters/priority.js +91 -0
  173. package/dist/filters/priority.js.map +1 -0
  174. package/dist/filters/pushdown.d.ts +12 -0
  175. package/dist/filters/pushdown.js +52 -0
  176. package/dist/filters/pushdown.js.map +1 -0
  177. package/dist/filters/shell-check.d.ts +110 -0
  178. package/dist/filters/shell-check.js +304 -0
  179. package/dist/filters/shell-check.js.map +1 -0
  180. package/dist/identities/github-app.d.ts +55 -0
  181. package/dist/identities/github-app.js +160 -0
  182. package/dist/identities/github-app.js.map +1 -0
  183. package/dist/identities/index.d.ts +6 -0
  184. package/dist/identities/index.js +7 -0
  185. package/dist/identities/index.js.map +1 -0
  186. package/dist/identities/jwt.d.ts +24 -0
  187. package/dist/identities/jwt.js +56 -0
  188. package/dist/identities/jwt.js.map +1 -0
  189. package/dist/lifecycle/context.d.ts +106 -0
  190. package/dist/lifecycle/context.js +170 -0
  191. package/dist/lifecycle/context.js.map +1 -0
  192. package/dist/lifecycle/engine.d.ts +185 -0
  193. package/dist/lifecycle/engine.js +551 -0
  194. package/dist/lifecycle/engine.js.map +1 -0
  195. package/dist/lifecycle/errors.d.ts +48 -0
  196. package/dist/lifecycle/errors.js +71 -0
  197. package/dist/lifecycle/errors.js.map +1 -0
  198. package/dist/lifecycle/index.d.ts +9 -0
  199. package/dist/lifecycle/index.js +9 -0
  200. package/dist/lifecycle/index.js.map +1 -0
  201. package/dist/lifecycle/performers.d.ts +66 -0
  202. package/dist/lifecycle/performers.js +179 -0
  203. package/dist/lifecycle/performers.js.map +1 -0
  204. package/dist/notifications/index.d.ts +6 -0
  205. package/dist/notifications/index.js +7 -0
  206. package/dist/notifications/index.js.map +1 -0
  207. package/dist/notifications/service.d.ts +115 -0
  208. package/dist/notifications/service.js +364 -0
  209. package/dist/notifications/service.js.map +1 -0
  210. package/dist/persistence/actions.d.ts +101 -0
  211. package/dist/persistence/actions.js +196 -0
  212. package/dist/persistence/actions.js.map +1 -0
  213. package/dist/persistence/blocks.d.ts +85 -0
  214. package/dist/persistence/blocks.js +140 -0
  215. package/dist/persistence/blocks.js.map +1 -0
  216. package/dist/persistence/cron-firings.d.ts +55 -0
  217. package/dist/persistence/cron-firings.js +70 -0
  218. package/dist/persistence/cron-firings.js.map +1 -0
  219. package/dist/persistence/database.d.ts +60 -0
  220. package/dist/persistence/database.js +124 -0
  221. package/dist/persistence/database.js.map +1 -0
  222. package/dist/persistence/errors.d.ts +39 -0
  223. package/dist/persistence/errors.js +67 -0
  224. package/dist/persistence/errors.js.map +1 -0
  225. package/dist/persistence/events.d.ts +72 -0
  226. package/dist/persistence/events.js +83 -0
  227. package/dist/persistence/events.js.map +1 -0
  228. package/dist/persistence/index.d.ts +18 -0
  229. package/dist/persistence/index.js +19 -0
  230. package/dist/persistence/index.js.map +1 -0
  231. package/dist/persistence/instance-lock.d.ts +26 -0
  232. package/dist/persistence/instance-lock.js +117 -0
  233. package/dist/persistence/instance-lock.js.map +1 -0
  234. package/dist/persistence/lock-keys.d.ts +16 -0
  235. package/dist/persistence/lock-keys.js +41 -0
  236. package/dist/persistence/lock-keys.js.map +1 -0
  237. package/dist/persistence/notifications.d.ts +113 -0
  238. package/dist/persistence/notifications.js +213 -0
  239. package/dist/persistence/notifications.js.map +1 -0
  240. package/dist/persistence/persistence.d.ts +46 -0
  241. package/dist/persistence/persistence.js +42 -0
  242. package/dist/persistence/persistence.js.map +1 -0
  243. package/dist/persistence/retention.d.ts +46 -0
  244. package/dist/persistence/retention.js +68 -0
  245. package/dist/persistence/retention.js.map +1 -0
  246. package/dist/persistence/runs.d.ts +215 -0
  247. package/dist/persistence/runs.js +417 -0
  248. package/dist/persistence/runs.js.map +1 -0
  249. package/dist/persistence/schema.d.ts +16 -0
  250. package/dist/persistence/schema.js +183 -0
  251. package/dist/persistence/schema.js.map +1 -0
  252. package/dist/persistence/sqlite.d.ts +49 -0
  253. package/dist/persistence/sqlite.js +131 -0
  254. package/dist/persistence/sqlite.js.map +1 -0
  255. package/dist/runs/controller.d.ts +307 -0
  256. package/dist/runs/controller.js +1078 -0
  257. package/dist/runs/controller.js.map +1 -0
  258. package/dist/runs/errors.d.ts +20 -0
  259. package/dist/runs/errors.js +26 -0
  260. package/dist/runs/errors.js.map +1 -0
  261. package/dist/runs/event-bus.d.ts +106 -0
  262. package/dist/runs/event-bus.js +105 -0
  263. package/dist/runs/event-bus.js.map +1 -0
  264. package/dist/runs/execution.d.ts +39 -0
  265. package/dist/runs/execution.js +146 -0
  266. package/dist/runs/execution.js.map +1 -0
  267. package/dist/runs/index.d.ts +12 -0
  268. package/dist/runs/index.js +13 -0
  269. package/dist/runs/index.js.map +1 -0
  270. package/dist/runs/retention.d.ts +64 -0
  271. package/dist/runs/retention.js +232 -0
  272. package/dist/runs/retention.js.map +1 -0
  273. package/dist/runs/workspace.d.ts +20 -0
  274. package/dist/runs/workspace.js +51 -0
  275. package/dist/runs/workspace.js.map +1 -0
  276. package/dist/scheduler/checks.d.ts +41 -0
  277. package/dist/scheduler/checks.js +111 -0
  278. package/dist/scheduler/checks.js.map +1 -0
  279. package/dist/scheduler/cron.d.ts +32 -0
  280. package/dist/scheduler/cron.js +83 -0
  281. package/dist/scheduler/cron.js.map +1 -0
  282. package/dist/scheduler/dry-run.d.ts +42 -0
  283. package/dist/scheduler/dry-run.js +167 -0
  284. package/dist/scheduler/dry-run.js.map +1 -0
  285. package/dist/scheduler/errors.d.ts +22 -0
  286. package/dist/scheduler/errors.js +22 -0
  287. package/dist/scheduler/errors.js.map +1 -0
  288. package/dist/scheduler/format.d.ts +3 -0
  289. package/dist/scheduler/format.js +73 -0
  290. package/dist/scheduler/format.js.map +1 -0
  291. package/dist/scheduler/index.d.ts +13 -0
  292. package/dist/scheduler/index.js +14 -0
  293. package/dist/scheduler/index.js.map +1 -0
  294. package/dist/scheduler/planner.d.ts +88 -0
  295. package/dist/scheduler/planner.js +255 -0
  296. package/dist/scheduler/planner.js.map +1 -0
  297. package/dist/scheduler/scheduler.d.ts +160 -0
  298. package/dist/scheduler/scheduler.js +680 -0
  299. package/dist/scheduler/scheduler.js.map +1 -0
  300. package/dist/scheduler/state.d.ts +26 -0
  301. package/dist/scheduler/state.js +25 -0
  302. package/dist/scheduler/state.js.map +1 -0
  303. package/dist/scheduler/types.d.ts +197 -0
  304. package/dist/scheduler/types.js +3 -0
  305. package/dist/scheduler/types.js.map +1 -0
  306. package/dist/service/index.d.ts +6 -0
  307. package/dist/service/index.js +7 -0
  308. package/dist/service/index.js.map +1 -0
  309. package/dist/service/install.d.ts +93 -0
  310. package/dist/service/install.js +250 -0
  311. package/dist/service/install.js.map +1 -0
  312. package/dist/service/unit.d.ts +50 -0
  313. package/dist/service/unit.js +171 -0
  314. package/dist/service/unit.js.map +1 -0
  315. package/dist/sources/github/adapter.d.ts +99 -0
  316. package/dist/sources/github/adapter.js +336 -0
  317. package/dist/sources/github/adapter.js.map +1 -0
  318. package/dist/sources/github/fields.d.ts +8 -0
  319. package/dist/sources/github/fields.js +72 -0
  320. package/dist/sources/github/fields.js.map +1 -0
  321. package/dist/sources/github/http.d.ts +100 -0
  322. package/dist/sources/github/http.js +288 -0
  323. package/dist/sources/github/http.js.map +1 -0
  324. package/dist/sources/github/index.d.ts +6 -0
  325. package/dist/sources/github/index.js +7 -0
  326. package/dist/sources/github/index.js.map +1 -0
  327. package/dist/sources/github/pushdown.d.ts +14 -0
  328. package/dist/sources/github/pushdown.js +47 -0
  329. package/dist/sources/github/pushdown.js.map +1 -0
  330. package/dist/sources/github/refs.d.ts +17 -0
  331. package/dist/sources/github/refs.js +61 -0
  332. package/dist/sources/github/refs.js.map +1 -0
  333. package/dist/sources/index.d.ts +6 -0
  334. package/dist/sources/index.js +7 -0
  335. package/dist/sources/index.js.map +1 -0
  336. package/dist/ui/index.d.ts +12 -0
  337. package/dist/ui/index.js +28 -0
  338. package/dist/ui/index.js.map +1 -0
  339. package/dist/ui/public/app.css +239 -0
  340. package/dist/ui/public/favicon.svg +1 -0
  341. package/dist/ui/public/index.html +27 -0
  342. package/dist/ui/public/js/client/api.js +68 -0
  343. package/dist/ui/public/js/client/app.js +146 -0
  344. package/dist/ui/public/js/client/dom.js +34 -0
  345. package/dist/ui/public/js/client/page.js +9 -0
  346. package/dist/ui/public/js/client/pages/actions.js +35 -0
  347. package/dist/ui/public/js/client/pages/blocked.js +58 -0
  348. package/dist/ui/public/js/client/pages/config.js +126 -0
  349. package/dist/ui/public/js/client/pages/dashboard.js +175 -0
  350. package/dist/ui/public/js/client/pages/run.js +367 -0
  351. package/dist/ui/public/js/client/pages/runs.js +72 -0
  352. package/dist/ui/public/js/client/pages/views.js +80 -0
  353. package/dist/ui/public/js/client/pages/workflows.js +291 -0
  354. package/dist/ui/public/js/client/types.js +1 -0
  355. package/dist/ui/public/js/client/ui.js +152 -0
  356. package/dist/ui/public/js/shared/dom.js +77 -0
  357. package/dist/ui/public/js/shared/events.js +88 -0
  358. package/dist/ui/public/js/shared/format.js +213 -0
  359. package/dist/ui/public/js/shared/logs.js +136 -0
  360. package/dist/ui/public/js/shared/params.js +90 -0
  361. package/dist/ui/public/js/shared/route.js +36 -0
  362. package/dist/ui/public/js/shared/sort.js +70 -0
  363. package/dist/ui/public/js/shared/sse.js +193 -0
  364. package/dist/ui/public/js/shared/throttle.js +65 -0
  365. package/dist/views/index.d.ts +6 -0
  366. package/dist/views/index.js +7 -0
  367. package/dist/views/index.js.map +1 -0
  368. package/dist/views/service.d.ts +131 -0
  369. package/dist/views/service.js +264 -0
  370. package/dist/views/service.js.map +1 -0
  371. package/docs/api.md +239 -0
  372. package/docs/configuration-reference.md +1785 -0
  373. package/docs/cron-semantics.md +96 -0
  374. package/docs/manual-tests/github-fixture/config.yaml +30 -0
  375. package/docs/manual-tests/github-fixture/helpers.sh +93 -0
  376. package/docs/manual-tests/github-fixture/identities.yaml +18 -0
  377. package/docs/manual-tests/github-fixture/scripts/test-worker.sh +100 -0
  378. package/docs/manual-tests/github-fixture/sources.yaml +13 -0
  379. package/docs/manual-tests/github-fixture/views/dependencies.yaml +17 -0
  380. package/docs/manual-tests/github-fixture/views/needs-maintainer.yaml +17 -0
  381. package/docs/manual-tests/github-fixture/workflows/coder-test.yaml +76 -0
  382. package/docs/manual-tests/github-fixture/workflows/commenter-test.yaml +63 -0
  383. package/docs/manual-tests/github-fixture/workflows/reviewer-test.yaml +60 -0
  384. package/docs/manual-tests/github.md +1308 -0
  385. package/docs/manual-tests/process-supervision.md +64 -0
  386. package/docs/manual-tests/systemd.md +68 -0
  387. package/docs/operations.md +271 -0
  388. package/docs/worker-protocol.md +145 -0
  389. package/examples/config/config.yaml +35 -0
  390. package/examples/config/identities.yaml +18 -0
  391. package/examples/config/sources.yaml +13 -0
  392. package/examples/config/views/needs-maintainer.yaml +15 -0
  393. package/examples/config/workflows/coder-project-a.yaml +86 -0
  394. package/examples/config/workflows/deploy.yaml +15 -0
  395. package/examples/config/workflows/nightly-maintenance.yaml +13 -0
  396. 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,13 @@
1
+ # sources.yaml (Spec §5.2)
2
+ sources:
3
+ project_a:
4
+ type: github
5
+ owner: example
6
+ repository: project-a
7
+ identity: maintainer
8
+
9
+ project_b:
10
+ type: github
11
+ owner: example
12
+ repository: project-b
13
+ identity: maintainer
@@ -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 }