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,1785 @@
|
|
|
1
|
+
# Konfigurationsreferenz
|
|
2
|
+
|
|
3
|
+
> **Verbindlichkeit.** Dieses Dokument ist die maßgebliche Referenz für alle Dateien im `config-dir`. Es **ersetzt
|
|
4
|
+
> die vorgeschlagenen Dateiformate** der Spezifikation ([spec-v0.1.md](spec-v0.1.md) §5, §6, §17, §18 und die
|
|
5
|
+
> YAML-Beispiele dort). Alles hier ist aus dem Code abgeleitet (`src/config/`, `src/core/`, `src/filters/`,
|
|
6
|
+
> `src/sources/github/`, `src/scheduler/`, `src/runs/`, `src/lifecycle/`, `src/executors/`,
|
|
7
|
+
> `src/notifications/`, `src/views/`). Weicht etwas vom Code ab, ist das ein Fehler dieses Dokuments.
|
|
8
|
+
>
|
|
9
|
+
> **Abgesichert durch Tests:** Alle YAML-Blöcke mit Kopfzeile `# file: <pfad>` werden von
|
|
10
|
+
> `src/config/reference-doc.test.ts` je Beispiel-Set (`# example: <name>`) in ein temporäres config-dir geschrieben
|
|
11
|
+
> und mit dem echten Loader validiert. Blöcke mit `# invalid` müssen die unter `# expect:` dokumentierte
|
|
12
|
+
> Fehlermeldung liefern. Kopfzeilen: `# example:` (Set-Name), `# extends:` (übernimmt zuerst alle Dateien eines
|
|
13
|
+
> anderen Sets), `# file:` (Zielpfad relativ zum config-dir), `# invalid`, `# expect:` / `# expect-warning:`
|
|
14
|
+
> (Teilstring von `<datei>: <pfad>: <meldung>`). Blöcke ohne `# file:` sind Ausschnitte und werden nicht geprüft.
|
|
15
|
+
|
|
16
|
+
## Inhalt
|
|
17
|
+
|
|
18
|
+
1. [Überblick](#1-überblick)
|
|
19
|
+
2. [Gemeinsame Datentypen](#2-gemeinsame-datentypen)
|
|
20
|
+
3. [Die Dateien](#3-die-dateien) – [config.yaml](#31-configyaml), [sources.yaml](#32-sourcesyaml),
|
|
21
|
+
[identities.yaml](#33-identitiesyaml), [workflows/*.yaml](#34-workflowsyaml), [views/*.yaml](#35-viewsyaml),
|
|
22
|
+
[Grenzen](#36-bekannte-grenzen-v1)
|
|
23
|
+
4. [Filter-Sprache](#4-filter-sprache)
|
|
24
|
+
5. [Platzhalter-Referenz](#5-platzhalter-referenz)
|
|
25
|
+
6. [Auswertungsablauf](#6-auswertungsablauf)
|
|
26
|
+
7. [Vollständige Beispiele](#7-vollständige-beispiele)
|
|
27
|
+
8. [Häufige Validierungsfehler](#8-häufige-validierungsfehler)
|
|
28
|
+
9. [Checkliste „neuen Workflow anlegen“](#9-checkliste-neuen-workflow-anlegen)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Überblick
|
|
33
|
+
|
|
34
|
+
### 1.1 Verzeichnisstruktur
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
config-dir/
|
|
38
|
+
config.yaml globale Einstellungen optional (fehlt → Warnung, Defaults)
|
|
39
|
+
sources.yaml externe Quellen (V1: GitHub-Repositories) optional (fehlt → keine Sources)
|
|
40
|
+
identities.yaml GitHub-App-Identitäten optional (fehlt → keine Identities)
|
|
41
|
+
workflows/*.yaml ein Workflow pro Datei optional (fehlt → keine Workflows)
|
|
42
|
+
views/*.yaml eine View pro Datei optional
|
|
43
|
+
scripts/… frei; Skripte werden über Pfade referenziert (siehe §2.4)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`adispatch init --config-dir D --data-dir D` legt eine kommentierte, gültige Beispielkonfiguration an
|
|
47
|
+
(`src/cli/init-templates.ts`): `config.yaml`, `sources.yaml`, `identities.yaml`, `workflows/hello.yaml`
|
|
48
|
+
(manuell, aktiv), `workflows/coder.yaml` und `workflows/nightly-maintenance.yaml` (beide `enabled: false`),
|
|
49
|
+
`views/needs-maintainer.yaml` und Beispielskripte unter `scripts/`. `init` überschreibt nie bestehende Dateien.
|
|
50
|
+
|
|
51
|
+
Das `data-dir` (SQLite, Logs, `runs/<id>/context.json|result.json`, Default-Workspaces) ist **keine**
|
|
52
|
+
Konfiguration und wird hier nur erwähnt, wo es Defaults bestimmt.
|
|
53
|
+
|
|
54
|
+
### 1.2 Dateinamensregeln
|
|
55
|
+
|
|
56
|
+
| Regel | Verhalten |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Top-Level-Dateien | exakt `config.yaml`, `sources.yaml`, `identities.yaml`. `.yml`-Varianten werden **nicht** gelesen. |
|
|
59
|
+
| `workflows/`, `views/` | alle Einträge, deren Name auf `.yaml` **oder** `.yml` endet und nicht mit `.` beginnt, alphabetisch sortiert. |
|
|
60
|
+
| Unterordner | werden **nicht** rekursiv gelesen (`workflows/alt/x.yaml` wird ignoriert). Ein *Verzeichnis* namens `x.yaml` ist ein Fehler („Not a regular file“). |
|
|
61
|
+
| Versteckte Dateien | `.foo.yaml` wird ignoriert (z. B. Editor-Backups). Andere Endungen (`x.yaml.bak`, `x.json`) ebenfalls. |
|
|
62
|
+
| Dateiname ↔ `id` | Maßgeblich ist das Feld `id`. Weicht der Dateiname (ohne Endung) davon ab, gibt es nur eine **Warnung** (nur für Workflows). Doppelte `id` über zwei Dateien ist ein **Fehler** in der alphabetisch späteren Datei. |
|
|
63
|
+
| Größe | max. 1 MiB pro Datei, sonst Fehler. |
|
|
64
|
+
|
|
65
|
+
### 1.3 Kodierung und YAML-Dialekt
|
|
66
|
+
|
|
67
|
+
- Dateien werden als **UTF-8** gelesen (Parser `yaml`, YAML **1.2** Core-Schema).
|
|
68
|
+
- Folge von YAML 1.2: `yes`/`no`/`on`/`off` sind **Strings**, keine Booleans. Schreibe `true`/`false`.
|
|
69
|
+
(`enabled: yes` ist daher ein Typfehler.)
|
|
70
|
+
- Doppelte Schlüssel in einem Mapping sind ein Fehler („Map keys must be unique“).
|
|
71
|
+
- Anker/Aliase sind erlaubt (max. 100 Alias-Auflösungen pro Datei).
|
|
72
|
+
- Die oberste Ebene jeder Datei muss ein Mapping sein. Eine leere Datei gilt als leeres Mapping
|
|
73
|
+
(für `config.yaml` also „alle Defaults“; für einen Workflow fehlen dann Pflichtfelder).
|
|
74
|
+
- **Alle Objekte sind strikt:** unbekannte Schlüssel (Tippfehler) sind Fehler: `Unknown key(s): "…"`.
|
|
75
|
+
- Zahlen sind YAML-Zahlen. Wo ein String erwartet wird (Dauer, Größe, `args`), ist eine unquotierte Zahl ein
|
|
76
|
+
Typfehler. Ausnahme: Werte in `env`-Maps dürfen Zahl/Boolean sein und werden zu Strings (`PORT: 8080` → `"8080"`).
|
|
77
|
+
|
|
78
|
+
### 1.4 Wie geladen und validiert wird
|
|
79
|
+
|
|
80
|
+
`config validate`, `start` und `config reload` verwenden denselben Loader (`src/config/loader.ts`):
|
|
81
|
+
|
|
82
|
+
1. config-dir muss existieren und ein Verzeichnis sein.
|
|
83
|
+
2. Alle Dateien werden gelesen und geparst (YAML-Syntaxfehler mit Zeilennummer).
|
|
84
|
+
3. Jede Datei wird gegen ihr Schema geprüft (`src/config/schemas.ts`: Struktur, Typen, Dauern, Größen, Defaults).
|
|
85
|
+
Ist `config.yaml` ungültig, werden Workflows trotzdem gegen die **Default**-Globalwerte weitergeprüft.
|
|
86
|
+
4. Semantische Prüfung über alle Dateien (`src/config/validate.ts`): Referenzen, eindeutige IDs, Platzhalter je
|
|
87
|
+
Kontext, Widersprüche, Cron/Zeitzone, Parameter-Defaults, Feldnamen/Operatoren/Werttypen in Filtern und Views
|
|
88
|
+
gegen den statischen Feldkatalog des Adaptertyps (`src/config/field-catalog.ts`, §4.3).
|
|
89
|
+
5. **Alle** Probleme aller Dateien werden gesammelt (nicht beim ersten abgebrochen), sortiert nach Datei, Zeile,
|
|
90
|
+
Pfad, und als `datei:zeile: pfad: meldung` ausgegeben. Warnungen (`warning:`) verhindern das Laden nicht.
|
|
91
|
+
|
|
92
|
+
Folgefehler sind möglich: Ist z. B. `identities.yaml` ungültig, meldet `sources.yaml` zusätzlich
|
|
93
|
+
`Unknown identity "…"`. Den ersten Fehler zuerst beheben.
|
|
94
|
+
|
|
95
|
+
Der Loader liest **keine** Umgebungsvariablen. `config validate` prüft also nicht, ob `private_key_env`,
|
|
96
|
+
`url_env` oder `headers_env` gesetzt sind, und es prüft weder Skriptpfade auf Existenz noch App-Rechte oder die
|
|
97
|
+
Erreichbarkeit von GitHub. Feldnamen werden offline gegen den statischen Feldkatalog geprüft (§4.3).
|
|
98
|
+
Exit-Code: 0 = gültig (ggf. mit Warnungen), 1 = ungültig.
|
|
99
|
+
|
|
100
|
+
### 1.5 Wann Änderungen wirken
|
|
101
|
+
|
|
102
|
+
Konfigurationsänderungen wirken **nur** durch einen manuellen Reload (`adispatch config reload`,
|
|
103
|
+
`POST /api/v1/config/reload`, UI „Reload“) oder einen Neustart. Es gibt keine Dateiüberwachung.
|
|
104
|
+
|
|
105
|
+
- **Atomar:** Der Reload lädt alles neu. Nur wenn **kein Fehler** auftritt, wird die neue Konfiguration aktiv;
|
|
106
|
+
sonst bleibt die bisherige vollständig aktiv (HTTP 422 `config_invalid` mit allen Fehlern).
|
|
107
|
+
- **Laufende Runs** verwenden den beim Reservieren gespeicherten Snapshot ihres Workflows (inkl. `execute`, Hooks,
|
|
108
|
+
`workspace`, effektiver Action-Retry-Werte und der Source-Konfiguration). Ein Reload ändert laufende Runs und
|
|
109
|
+
deren spätere Hooks nicht; auch manuelle Retries von Actions nutzen den Snapshot.
|
|
110
|
+
- Nach einem erfolgreichen Reload läuft sofort ein Planungsschritt.
|
|
111
|
+
|
|
112
|
+
| Bereich | Wirkung nach erfolgreichem Reload |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `server.host`, `server.port` | erst nach Neustart |
|
|
115
|
+
| `server.allowed_hosts` | sofort (nächster Request) |
|
|
116
|
+
| `scheduler.polling_interval` | für den nächsten Timer (nach dem laufenden Planungsschritt) |
|
|
117
|
+
| `scheduler.max_concurrent` | sofort |
|
|
118
|
+
| `workspace.*`, `lifecycle.action_retry` | für neu reservierte Runs (Snapshot) |
|
|
119
|
+
| `retention.*` | ab dem nächsten Retention-Durchlauf; `cleanup_interval` bestimmt den Timer nach jedem Durchlauf |
|
|
120
|
+
| `notifications.*` | sofort für neue Benachrichtigungen; offene Zustellungen an entfernte Kanäle scheitern endgültig |
|
|
121
|
+
| `sources.yaml`, `identities.yaml` | Adapter/Credentials werden neu aufgebaut, wenn sich etwas geändert hat; entfernte bleiben für laufende Runs verfügbar |
|
|
122
|
+
| Workflows | sofort für Planung/Cron; geänderte Cron-Ausdrücke planen ab „jetzt“; Cache der Workflow-Conditions wird geleert |
|
|
123
|
+
| Views | sofort; View-Cache wird verworfen |
|
|
124
|
+
| Umgebungsvariablen (Keys, Webhook-URLs, Header) | gehören zum Prozess, nicht zur Konfiguration → Änderung erfordert **Neustart** |
|
|
125
|
+
|
|
126
|
+
**Runtime-Overrides** (UI bzw. `PATCH /api/v1/workflows/:id/runtime`, `PATCH /api/v1/scheduler/runtime`):
|
|
127
|
+
pro Workflow `enabled`, `priority`, `maxConcurrent`, global `maxConcurrent`. Sie werden **nicht persistiert**
|
|
128
|
+
(Neustart = weg), gehören nicht zum Run-Snapshot und wirken nur auf das Scheduling. Beim Reload:
|
|
129
|
+
|
|
130
|
+
- ein Workflow-Override wird **pro Feld** zurückgesetzt, wenn sich genau dieser konfigurierte Wert geändert hat;
|
|
131
|
+
andere Änderungen am Workflow lassen ihn bestehen;
|
|
132
|
+
- ein entfernter Workflow verliert alle Overrides;
|
|
133
|
+
- das globale Override wird zurückgesetzt, wenn sich `scheduler.max_concurrent` geändert hat;
|
|
134
|
+
- die Antwort nennt betroffene Workflows in `resetOverrides` (`"*"` = global).
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 2. Gemeinsame Datentypen
|
|
139
|
+
|
|
140
|
+
### 2.1 Dauer
|
|
141
|
+
|
|
142
|
+
Syntax: `<zahl><einheit>` – genau **eine** Zahl und **eine** Einheit, optional umgebende Leerzeichen.
|
|
143
|
+
|
|
144
|
+
| Einheit | Bedeutung |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `ms` | Millisekunden |
|
|
147
|
+
| `s` | Sekunden |
|
|
148
|
+
| `m` | Minuten |
|
|
149
|
+
| `h` | Stunden |
|
|
150
|
+
| `d` | Tage (24 h) |
|
|
151
|
+
|
|
152
|
+
- Dezimalzahlen sind erlaubt, wenn das Ergebnis ganze Millisekunden ergibt: `1.5h`, `0.5s` ✓, `0.0001s` ✗.
|
|
153
|
+
- Nicht erlaubt: zusammengesetzte Formen (`1h30m` → `90m`), Leerzeichen zwischen Zahl und Einheit (`6 h`),
|
|
154
|
+
negative Werte, fehlende Einheit, Großbuchstaben (`30S`), unquotierte Zahlen (`timeout: 30`).
|
|
155
|
+
- `0s` ist syntaktisch gültig, aber nur dort erlaubt, wo 0 einen Sinn hat (Retry-`delay`, `cancel_grace`,
|
|
156
|
+
`cache`, `retention.*_runs`). **> 0** verlangen: `execute.timeout.max_duration`, `execute.timeout.idle_timeout`,
|
|
157
|
+
`timeout` von Conditions, `shell_filter` und `shell`-Actions. **≥ 1 s** verlangen: `scheduler.polling_interval`
|
|
158
|
+
und `retention.cleanup_interval` (ein Null-Intervall würde die Schleife ohne Pause laufen lassen).
|
|
159
|
+
- Wo `never` erlaubt ist (`retention.*_runs`), bedeutet es „nie löschen“.
|
|
160
|
+
|
|
161
|
+
### 2.2 Größe
|
|
162
|
+
|
|
163
|
+
Syntax: `<zahl>[ ]<einheit>`, Einheit ohne Beachtung der Groß-/Kleinschreibung: `B`, `KB`, `MB`, `GB`, `KiB`,
|
|
164
|
+
`MiB`, `GiB`. **Alle** Einheiten sind binär (`1 KB = 1024 B`, `100MB = 104 857 600 B`). Dezimalzahlen erlaubt
|
|
165
|
+
(abgerundet auf ganze Bytes). Eine Zahl ohne Einheit ist ungültig.
|
|
166
|
+
|
|
167
|
+
### 2.3 IDs und Namen
|
|
168
|
+
|
|
169
|
+
| Art | Regel | Verwendung |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| Konfigurations-ID | `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$` – Buchstaben, Ziffern, `_`, `-`; beginnt mit Buchstabe/Ziffer; 1–64 Zeichen; Groß-/Kleinschreibung zählt | Workflow-`id`, View-`id`, Source- und Identity-Schlüssel, Action-`id`, Kanal-`id`, alle Referenzen darauf |
|
|
172
|
+
| Parametername | `^[A-Za-z_][A-Za-z0-9_-]*$` | Schlüssel unter `parameters`, `{parameters.<name>}` |
|
|
173
|
+
| Env-Variablenname | `^[A-Za-z_][A-Za-z0-9_]*$` | Schlüssel in `env`/`secrets`, `private_key_env`, `url_env`, Werte in `headers_env` |
|
|
174
|
+
| Präfix `ADISPATCH_` | in `env`/`secrets` verboten (Groß-/Kleinschreibung egal) | reserviert für das Worker-Protokoll |
|
|
175
|
+
| HTTP-Header-Name | `^[A-Za-z0-9-]+$` | Schlüssel in `headers_env` |
|
|
176
|
+
|
|
177
|
+
Source-IDs sind Teil des globalen Item-Schlüssels `<source-id>:<item-id>` (z. B. `project_a:123`), der für Locks,
|
|
178
|
+
Retry-Blocks, Run-Historie und Views benutzt wird. **Umbenennen einer Source trennt die Historie.**
|
|
179
|
+
|
|
180
|
+
### 2.4 Pfade und Kommandos
|
|
181
|
+
|
|
182
|
+
| Feld | Auflösung |
|
|
183
|
+
|---|---|
|
|
184
|
+
| `workspace.base_dir` | absolut bleibt; relativ → relativ zum **config-dir** (nicht zum Arbeitsverzeichnis). Default `<data-dir>/workspaces`. |
|
|
185
|
+
| `command` in `execute`, `conditions.*`, `shell_filter`, `shell`-Actions | absolut → normalisiert; enthält `/` (`scripts/x.sh`, `./x.sh`, `../x`) → relativ zum **config-dir**; ohne `/` (`bash`, `gh`) → bleibt Name für die **PATH-Suche** zur Laufzeit (PATH der Minimalumgebung, §6.11). |
|
|
186
|
+
| `execute.working_directory` | Platzhalter-Template im Modus `path` (§2.5); muss mit `/` oder `{workspace.base_dir}` beginnen. |
|
|
187
|
+
|
|
188
|
+
Es gibt **keine Shell**: `command` ist genau ein ausführbares Programm, Argumente kommen ausschließlich aus `args`
|
|
189
|
+
(Liste, ein Element = ein Argument, keine Wortzerlegung, keine Globs, keine `$VAR`-Expansion).
|
|
190
|
+
`command: 'test -f /x'` ist falsch (würde als Pfad `config-dir/test -f /x` aufgelöst). Für Pipes etc.:
|
|
191
|
+
`command: /bin/sh`, `args: ['-c', '…']` – dann liegt die Quotierung beim Autor. `command` ist **kein**
|
|
192
|
+
Template; Platzhalter in `command` werden nicht ersetzt. Existenz/Ausführbarkeit wird erst zur Laufzeit geprüft
|
|
193
|
+
(→ `start_error` bzw. Prüffehler).
|
|
194
|
+
|
|
195
|
+
### 2.5 Platzhalter-Syntax
|
|
196
|
+
|
|
197
|
+
- Form `{name}`, z. B. `{run.id}`, `{parameters.version}`; Leerzeichen innerhalb der Klammern werden ignoriert.
|
|
198
|
+
- Nur die feste Liste aus §5 ist erlaubt; jeder andere Name ist ein Konfigurationsfehler
|
|
199
|
+
(`Unknown placeholder "{…}"`), ebenso ein Platzhalter außerhalb seines Kontexts
|
|
200
|
+
(`… is not available in this context`) und ein nicht deklarierter Parameter (`Parameter "x" is not declared`).
|
|
201
|
+
- **Escaping:** `{{` ergibt `{`, `}}` ergibt `}`. Ein einzelnes `}` oder ein nicht geschlossenes `{` ist ein
|
|
202
|
+
Syntaxfehler. Beispiel: `'{{"run":"{run.id}"}}'` → `{"run":"run-2026…"}`.
|
|
203
|
+
- Templates sind nur folgende Felder: `execute.working_directory` (Modus `path`), `execute.args[]`,
|
|
204
|
+
`execute.env.*`, `conditions.workflow[].args[]`, `conditions.item[].args[]`, `source.comment.message`,
|
|
205
|
+
`shell`-Action `args[]` und `env.*` (alle Modus `text`). **Keine** Templates: `command`, Labels in
|
|
206
|
+
`source.update`, Filterwerte, `secrets`, alle übrigen Felder.
|
|
207
|
+
|
|
208
|
+
**Modus `text`:** Werte werden unverändert eingesetzt (Zahlen/Booleans als `5`, `true`). Ein erlaubter Platzhalter
|
|
209
|
+
ohne Wert wird zu einem **leeren String**.
|
|
210
|
+
|
|
211
|
+
**Modus `path`** (nur `working_directory`):
|
|
212
|
+
|
|
213
|
+
- `{workspace.base_dir}` ist eine vertrauenswürdige Wurzel: nur ganz am Anfang erlaubt und muss von `/` oder dem
|
|
214
|
+
Ende gefolgt werden (`{workspace.base_dir}{run.id}` ist ein Fehler).
|
|
215
|
+
- Jeder andere Wert wird zu **genau einem** sicheren Pfadsegment: jedes Zeichen außer `[A-Za-z0-9._@+=,-]` wird
|
|
216
|
+
`_` (auch `/`, `\`, Leerzeichen, NUL); ein Wert nur aus Punkten wird zu Unterstrichen; leer → `_`; max. 128 Zeichen.
|
|
217
|
+
- Literale Teile des Templates werden **nicht** bereinigt; das Ergebnis wird normalisiert (`..` im Literal wird
|
|
218
|
+
aufgelöst – das ist Sache des Autors).
|
|
219
|
+
- Ein Platzhalter ohne Wert ist ein **Fehler** (Run endet als `start_error`). Deshalb dürfen im
|
|
220
|
+
`working_directory` nur Parameter stehen, die `required` sind oder einen `default` haben.
|
|
221
|
+
- Das Ergebnis muss absolut sein und darf nicht `workspace.base_dir` selbst sein.
|
|
222
|
+
|
|
223
|
+
**Sicherheit:** Ersetzte Werte (z. B. `{item.title}`, `{result.message}`) stammen aus externen Quellen und sind
|
|
224
|
+
untrusted. In `args` sind sie wegen der fehlenden Shell unkritisch; wer sie über `/bin/sh -c` interpoliert,
|
|
225
|
+
erzeugt eine Injection. In `source.comment.message` wird `<!--` zu `<!--` neutralisiert (§6.9).
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 3. Die Dateien
|
|
230
|
+
|
|
231
|
+
### 3.1 config.yaml
|
|
232
|
+
|
|
233
|
+
Alle Schlüssel sind optional. Fehlt die Datei, gelten alle Defaults (mit Warnung).
|
|
234
|
+
|
|
235
|
+
| Schlüssel | Typ | Default | Erlaubte Werte / Regeln | Bedeutung |
|
|
236
|
+
|---|---|---|---|---|
|
|
237
|
+
| `server.host` | String | `127.0.0.1` | nicht leer | Bind-Adresse von API/UI. Keine Authentifizierung – nur Loopback oder hinter authentifizierendem Proxy/VPN. |
|
|
238
|
+
| `server.port` | Integer | `8787` | 1–65535 | TCP-Port. |
|
|
239
|
+
| `server.allowed_hosts` | Liste String | `[]` | je `name` oder `name:port`; IPv6 in Klammern (`[::1]:8787`) | zusätzliche Namen, die im `Host`-Header akzeptiert werden (DNS-Rebinding-Schutz), z. B. der öffentliche Name des Reverse-Proxys. Vergleich ohne Groß-/Kleinschreibung; `name` = jeder Port, `name:port` = nur dieser Port. Ein `Host` **ohne** Port (Standardport des Schemas, z. B. hinter einem HTTPS-Proxy) passt auf `name`, `name:80` und `name:443`. Immer erlaubt: `localhost`, `127.0.0.1`, `[::1]` und – wenn nicht Loopback/Wildcard – `server.host` selbst. |
|
|
240
|
+
| `scheduler.polling_interval` | Dauer | `30s` | ≥ 1 s | Pause zwischen zwei Planungsschritten (gemessen ab Ende des vorherigen). |
|
|
241
|
+
| `scheduler.max_concurrent` | Integer | `5` | ≥ 1 | Globales Limit für **automatische** Starts. Gezählt werden alle aktiven Runs (inkl. manueller und `recovery_pending`). |
|
|
242
|
+
| `workspace.base_dir` | String | `<data-dir>/workspaces` | relativ → zum config-dir | Wurzel der Run-Workspaces; nur Verzeichnisse **strikt darunter** werden symlink-sicher angelegt und automatisch gelöscht. |
|
|
243
|
+
| `workspace.cleanup` | Enum | `on_success` | `always`, `on_success`, `never` | Wann der Workspace nach Run-Ende gelöscht wird (§6.12). |
|
|
244
|
+
| `lifecycle.action_retry.max_attempts` | Integer | `3` | ≥ 1 | Standard-Anzahl **Versuche insgesamt** pro Lifecycle-Action. |
|
|
245
|
+
| `lifecycle.action_retry.delay` | Dauer | `30s` | ≥ 0 | Pause zwischen Versuchen (mind. ein `Retry-After` der API). |
|
|
246
|
+
| `retention.completed_runs` | Dauer \| `never` | `30d` | | Aufbewahrung erfolgreicher Runs (`completed` + `success`). |
|
|
247
|
+
| `retention.failed_runs` | Dauer \| `never` | `90d` | | alle anderen beendeten Runs (fachlich `failure`, technisch gescheitert, `cancelled`), soweit nicht „blocked“. |
|
|
248
|
+
| `retention.blocked_runs` | Dauer \| `never` | `never` | | Runs, deren technischer Fehler in einen Retry-Block gezählt wurde. |
|
|
249
|
+
| `retention.max_log_size_per_run` | Größe | `100MB` | | Log-Limit des Haupt-Workers pro Run (stdout+stderr); Überlauf wird markiert. |
|
|
250
|
+
| `retention.cleanup_interval` | Dauer | `24h` | ≥ 1 s | Abstand der Retention-Durchläufe (erster Lauf 60 s nach Bereitschaft). |
|
|
251
|
+
| `notifications.on` | Liste Enum | alle drei | `retry_block_created`, `lifecycle_action_failed`, `recovery_failed` | global aktivierte Ereignisse. `[]` = keine Benachrichtigungen. |
|
|
252
|
+
| `notifications.channels` | Liste | `[]` | | Zustellkanäle, siehe unten. |
|
|
253
|
+
|
|
254
|
+
Retention löscht nie aktive Runs, Runs mit unfertiger oder gescheiterter Lifecycle-Kette und Runs eines **aktiven**
|
|
255
|
+
Retry-Blocks. Gelöscht werden DB-Zeilen, `runs/<id>/`, `logs/<id>/` und der Workspace (nur unter `base_dir`).
|
|
256
|
+
|
|
257
|
+
**Kanal `type: webhook`:**
|
|
258
|
+
|
|
259
|
+
| Schlüssel | Typ | Pflicht | Default | Bedeutung |
|
|
260
|
+
|---|---|---|---|---|
|
|
261
|
+
| `type` | `webhook` | ja | | einziger Kanaltyp in V1 |
|
|
262
|
+
| `id` | Konfigurations-ID | nein | `channel-<n>` (1-basiert nach Position) | eindeutig innerhalb der Kanäle |
|
|
263
|
+
| `url` | String | genau eines von `url`/`url_env` | | absolute `http(s)`-URL. Wird intern redigiert, kann aber Tokens enthalten → besser `url_env`. |
|
|
264
|
+
| `url_env` | Env-Name | genau eines von `url`/`url_env` | | Umgebungsvariable mit der URL (gelesen bei jeder Zustellung; fehlt sie → Zustellung scheitert endgültig). |
|
|
265
|
+
| `headers_env` | Map Header-Name → Env-Name | nein | `{}` | zusätzliche HTTP-Header, Werte aus Umgebungsvariablen (fehlt eine → Zustellung scheitert endgültig). |
|
|
266
|
+
| `on` | Liste Enum | nein | `notifications.on` | Ereignisse für diesen Kanal. Wirksam ist nur die Schnittmenge mit `notifications.on`. |
|
|
267
|
+
|
|
268
|
+
Zustellung: `POST` mit `content-type: application/json`, Body
|
|
269
|
+
`{"source":"adispatchd","id":…,"event":"…","createdAt":"…","text":"[adispatchd] …","payload":{…}}`
|
|
270
|
+
(Slack-kompatibles `text`), Timeout 10 s, keine Redirects. 2xx = Erfolg; 408/429/5xx/Netzfehler werden bis zu
|
|
271
|
+
6-mal versucht (Pausen 5 s, 30 s, 2 min, 10 min, 30 min); andere Status sind endgültig. Ereignisse:
|
|
272
|
+
`retry_block_created` (Retry-Block entstanden), `lifecycle_action_failed` (Action endgültig gescheitert und
|
|
273
|
+
**ohne** `continue_on_error`), `recovery_failed` (Run nach Neustart nicht sicher zuordenbar → `recovery_pending`).
|
|
274
|
+
|
|
275
|
+
Vollständige `config.yaml` mit allen Schlüsseln (Defaults, außer wo kommentiert):
|
|
276
|
+
|
|
277
|
+
```yaml
|
|
278
|
+
# example: reference
|
|
279
|
+
# file: config.yaml
|
|
280
|
+
server:
|
|
281
|
+
host: 127.0.0.1
|
|
282
|
+
port: 8787
|
|
283
|
+
allowed_hosts: [orchestrator.example.com] # Name des authentifizierenden Reverse-Proxys
|
|
284
|
+
scheduler:
|
|
285
|
+
polling_interval: 30s
|
|
286
|
+
max_concurrent: 5
|
|
287
|
+
workspace:
|
|
288
|
+
base_dir: /var/lib/adispatchd/workspaces # Default: <data-dir>/workspaces
|
|
289
|
+
cleanup: on_success # always | on_success | never
|
|
290
|
+
lifecycle:
|
|
291
|
+
action_retry:
|
|
292
|
+
max_attempts: 3
|
|
293
|
+
delay: 30s
|
|
294
|
+
retention:
|
|
295
|
+
completed_runs: 30d
|
|
296
|
+
failed_runs: 90d
|
|
297
|
+
blocked_runs: never
|
|
298
|
+
max_log_size_per_run: 100MB
|
|
299
|
+
cleanup_interval: 24h
|
|
300
|
+
notifications:
|
|
301
|
+
on: [retry_block_created, lifecycle_action_failed, recovery_failed]
|
|
302
|
+
channels:
|
|
303
|
+
- id: ops
|
|
304
|
+
type: webhook
|
|
305
|
+
url_env: OPS_WEBHOOK_URL
|
|
306
|
+
headers_env: { Authorization: OPS_WEBHOOK_AUTH }
|
|
307
|
+
on: [retry_block_created, recovery_failed]
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 3.2 sources.yaml
|
|
311
|
+
|
|
312
|
+
```yaml
|
|
313
|
+
# example: reference
|
|
314
|
+
# file: sources.yaml
|
|
315
|
+
sources:
|
|
316
|
+
project_a: # Source-ID (Konfigurations-ID), Teil jedes Item-Schlüssels project_a:<nr>
|
|
317
|
+
type: github
|
|
318
|
+
owner: example # GitHub-Owner/Organisation
|
|
319
|
+
repository: project-a # Repository-Name
|
|
320
|
+
identity: maintainer # Identity für lesende Aufrufe (und Fallback für Schreiben)
|
|
321
|
+
project_b:
|
|
322
|
+
type: github
|
|
323
|
+
owner: example
|
|
324
|
+
repository: project-b
|
|
325
|
+
identity: maintainer
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
| Schlüssel | Typ | Pflicht | Regeln | Bedeutung |
|
|
329
|
+
|---|---|---|---|---|
|
|
330
|
+
| `sources` | Map Source-ID → Source | nein (Default `{}`) | Schlüssel = Konfigurations-ID | alle Sources |
|
|
331
|
+
| `<id>.type` | `github` | ja | einziger Adapter in V1 | |
|
|
332
|
+
| `<id>.owner` | String | ja | nicht leer (Format nicht geprüft) | Owner/Organisation |
|
|
333
|
+
| `<id>.repository` | String | ja | nicht leer | Repository-Name; `{source.repository}` und Feld `repository` = `owner/repository` in der hier geschriebenen Schreibweise |
|
|
334
|
+
| `<id>.identity` | Identity-ID | ja | muss in `identities.yaml` existieren | liest Issues/PRs/Dependencies; Fallback für Schreibaktionen (§6.10) |
|
|
335
|
+
|
|
336
|
+
Nur `github.com` wird unterstützt (keine Einstellung für GitHub Enterprise). Gelesen werden Issues **und** Pull
|
|
337
|
+
Requests des Repositories über `GET /repos/{owner}/{repo}/issues`, standardmäßig **nur offene** (siehe §4.5).
|
|
338
|
+
|
|
339
|
+
### 3.3 identities.yaml
|
|
340
|
+
|
|
341
|
+
```yaml
|
|
342
|
+
# example: reference
|
|
343
|
+
# file: identities.yaml
|
|
344
|
+
identities:
|
|
345
|
+
maintainer:
|
|
346
|
+
type: github_app
|
|
347
|
+
app_id: 100001 # Zahl, keine Zeichenkette
|
|
348
|
+
installation_id: 200001
|
|
349
|
+
private_key_env: GITHUB_MAINTAINER_PRIVATE_KEY
|
|
350
|
+
coder:
|
|
351
|
+
type: github_app
|
|
352
|
+
app_id: 100002
|
|
353
|
+
installation_id: 200002
|
|
354
|
+
private_key_env: GITHUB_CODER_PRIVATE_KEY
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
| Schlüssel | Typ | Pflicht | Regeln | Bedeutung |
|
|
358
|
+
|---|---|---|---|---|
|
|
359
|
+
| `identities` | Map Identity-ID → Identity | nein (Default `{}`) | Schlüssel = Konfigurations-ID | |
|
|
360
|
+
| `<id>.type` | `github_app` | ja | einziger Typ in V1 | |
|
|
361
|
+
| `<id>.app_id` | Integer | ja | > 0 | GitHub-App-ID (auch zur Erkennung eigener Kommentare) |
|
|
362
|
+
| `<id>.installation_id` | Integer | ja | > 0 | Installation der App im Ziel-Owner |
|
|
363
|
+
| `<id>.private_key_env` | Env-Name | ja | `^[A-Za-z_][A-Za-z0-9_]*$` | Name der Umgebungsvariable mit dem privaten Schlüssel |
|
|
364
|
+
|
|
365
|
+
**Secrets über die Umgebung:** Der Schlüssel steht nie in YAML. Die Variable muss in der Umgebung des
|
|
366
|
+
Orchestrator-Prozesses gesetzt sein (unter systemd: `EnvironmentFile`, Default `/etc/default/adispatchd`,
|
|
367
|
+
siehe [operations.md](operations.md)). Format des Werts: **RSA**-Private-Key im PEM-Format
|
|
368
|
+
(`-----BEGIN RSA PRIVATE KEY-----` bzw. PKCS#8 `-----BEGIN PRIVATE KEY-----`); akzeptiert werden echte
|
|
369
|
+
Zeilenumbrüche, wörtliche `\n`-Escapes (typisch in Env-Dateien), CRLF sowie umschließende `"…"`/`'…'`.
|
|
370
|
+
Gelesen wird der Schlüssel erst beim **ersten Token-Bedarf** (nicht beim Laden/Validieren); fehlt er oder ist er
|
|
371
|
+
ungültig, scheitert der jeweilige Zugriff mit einem nicht wiederholbaren Authentifizierungsfehler (Source-Fehler
|
|
372
|
+
in der Planungsdiagnose, `start_error` bei `execute.secrets`, endgültig gescheiterte Action). Aus dem Schlüssel
|
|
373
|
+
werden kurzlebige Installation-Tokens erzeugt und im Speicher gecacht; Schlüssel, JWT und Tokens werden in Logs
|
|
374
|
+
redigiert. Minimale App-Rechte: Issues lesen/schreiben, Pull Requests schreiben (Labels/Kommentare auf PRs),
|
|
375
|
+
Metadata lesen.
|
|
376
|
+
|
|
377
|
+
### 3.4 workflows/*.yaml
|
|
378
|
+
|
|
379
|
+
Ein Workflow pro Datei. Vollständige Referenz mit (fast) allen Schlüsseln – ein Source-Workflow:
|
|
380
|
+
|
|
381
|
+
```yaml
|
|
382
|
+
# example: reference
|
|
383
|
+
# file: workflows/full-reference.yaml
|
|
384
|
+
id: full-reference # Pflicht; Dateiname sollte <id>.yaml sein
|
|
385
|
+
name: Full Reference # Anzeigename, Default = id
|
|
386
|
+
enabled: true # Default true; false = keine automatischen Starts
|
|
387
|
+
priority: 10 # Default 0; höher = früher (auch negativ/dezimal erlaubt)
|
|
388
|
+
max_concurrent: 2 # Default 1; Limit gleichzeitiger automatischer Runs dieses Workflows
|
|
389
|
+
identity: coder # Default: Identity der Source; Schreib-Identity für Actions
|
|
390
|
+
|
|
391
|
+
trigger:
|
|
392
|
+
type: source # source | schedule | manual
|
|
393
|
+
source: project_a
|
|
394
|
+
|
|
395
|
+
filter: # deklarativ, nur bei trigger.type: source
|
|
396
|
+
all:
|
|
397
|
+
- { field: labels, operator: contains, value: ready }
|
|
398
|
+
- { field: is_pull_request, operator: equals, value: false }
|
|
399
|
+
- not: { field: labels, operator: contains, value: on-hold }
|
|
400
|
+
- any:
|
|
401
|
+
- { field: dependencies.blocked_by_open, operator: equals, value: 0 }
|
|
402
|
+
- { field: labels, operator: contains, value: ignore-dependencies }
|
|
403
|
+
|
|
404
|
+
shell_filter: # nur Source-Workflows; ein Programm ohne Argumente
|
|
405
|
+
command: scripts/filter.sh # bekommt ADISPATCH_CHECK_CONTEXT_FILE; Exit 0/1/≥2
|
|
406
|
+
timeout: 30s
|
|
407
|
+
|
|
408
|
+
item_priority:
|
|
409
|
+
rules: # erste zutreffende Regel (true) gewinnt
|
|
410
|
+
- when: { field: labels, operator: contains, value: 'priority:critical' }
|
|
411
|
+
value: 100
|
|
412
|
+
- when: { field: labels, operator: contains, value: 'priority:high' }
|
|
413
|
+
value: 50
|
|
414
|
+
default: 0
|
|
415
|
+
|
|
416
|
+
conditions:
|
|
417
|
+
workflow: # einmal pro Planungsschritt bzw. Cron-Slot
|
|
418
|
+
- type: shell
|
|
419
|
+
command: scripts/check-credits.sh
|
|
420
|
+
args: ['{workflow.id}']
|
|
421
|
+
timeout: 15s
|
|
422
|
+
cache: 60s # nur bei workflow-Conditions erlaubt
|
|
423
|
+
item: # pro Kandidat, nur Source-Workflows
|
|
424
|
+
- type: shell
|
|
425
|
+
command: scripts/check-item.sh
|
|
426
|
+
args: ['{item.key}', '{item.url}']
|
|
427
|
+
timeout: 30s
|
|
428
|
+
|
|
429
|
+
execute:
|
|
430
|
+
type: shell
|
|
431
|
+
command: scripts/worker.sh
|
|
432
|
+
args: ['--issue', '{item.number}', '--run', '{run.id}']
|
|
433
|
+
working_directory: '{workspace.base_dir}/{workflow.id}/{run.id}' # Default
|
|
434
|
+
create_directory: true
|
|
435
|
+
env:
|
|
436
|
+
NODE_ENV: production
|
|
437
|
+
ISSUE_URL: '{item.url}'
|
|
438
|
+
secrets:
|
|
439
|
+
GH_TOKEN: { identity: coder } # kurzlebiges Installation-Token der Identity
|
|
440
|
+
timeout:
|
|
441
|
+
max_duration: 6h
|
|
442
|
+
idle_timeout: 45m
|
|
443
|
+
cancel_grace: 30s
|
|
444
|
+
|
|
445
|
+
retry:
|
|
446
|
+
max_technical_failures: 3
|
|
447
|
+
|
|
448
|
+
on_start:
|
|
449
|
+
- type: source.update
|
|
450
|
+
labels: { add: [in-progress], remove: [ready] }
|
|
451
|
+
on_success:
|
|
452
|
+
- id: done-labels
|
|
453
|
+
type: source.update
|
|
454
|
+
labels: { add: [done], remove: [in-progress] }
|
|
455
|
+
- type: source.comment
|
|
456
|
+
message: 'Fertig ({result.result}): {result.message}'
|
|
457
|
+
identity: maintainer
|
|
458
|
+
continue_on_error: true
|
|
459
|
+
retry: { max_attempts: 5, delay: 1m }
|
|
460
|
+
- type: shell
|
|
461
|
+
command: scripts/notify.sh
|
|
462
|
+
args: ['{workflow.id}', '{run.id}', '{result.status}']
|
|
463
|
+
env: { CHANNEL: '#agents' }
|
|
464
|
+
timeout: 1m
|
|
465
|
+
on_failure:
|
|
466
|
+
- type: source.update
|
|
467
|
+
labels: { add: [needs-maintainer], remove: [in-progress] }
|
|
468
|
+
on_crash:
|
|
469
|
+
- type: source.update
|
|
470
|
+
labels: { add: [ready], remove: [in-progress] }
|
|
471
|
+
on_start_error:
|
|
472
|
+
- type: source.update
|
|
473
|
+
labels: { add: [ready], remove: [in-progress] }
|
|
474
|
+
on_cancel:
|
|
475
|
+
- type: source.update
|
|
476
|
+
labels: { add: [ready], remove: [in-progress] }
|
|
477
|
+
on_timeout:
|
|
478
|
+
- type: source.update
|
|
479
|
+
labels: { add: [ready], remove: [in-progress] }
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
#### 3.4.1 Oberste Ebene
|
|
483
|
+
|
|
484
|
+
| Schlüssel | Typ | Pflicht | Default | Regeln | Bedeutung |
|
|
485
|
+
|---|---|---|---|---|---|
|
|
486
|
+
| `id` | Konfigurations-ID | ja | | eindeutig über alle Workflows | stabiler Schlüssel (Runs, Locks, Blocks, API) |
|
|
487
|
+
| `name` | String | nein | `id` | nicht leer | Anzeigename, `{workflow.name}` |
|
|
488
|
+
| `enabled` | Boolean | nein | `true` | | `false`: keine automatischen Starts (Source/Cron); **manuelle Starts bleiben möglich** |
|
|
489
|
+
| `priority` | Zahl | nein | `0` | | Workflow-Priorität, wird vor der Item-Priorität sortiert (§6.2) |
|
|
490
|
+
| `max_concurrent` | Integer | nein | `1` | ≥ 1 | Limit gleichzeitig aktiver Runs dieses Workflows für automatische Starts |
|
|
491
|
+
| `identity` | Identity-ID | nein | Source-Identity | muss existieren | Schreib-Identity für `source.update`/`source.comment` (§6.10) |
|
|
492
|
+
| `trigger` | Objekt | ja | | §3.4.2 | |
|
|
493
|
+
| `filter` | Filter-AST | nein | (alles passt) | nur `trigger.type: source` | §4 |
|
|
494
|
+
| `shell_filter` | Objekt | nein | | nur `source` | §3.4.4 |
|
|
495
|
+
| `item_priority` | Objekt | nein | Priorität 0 | nur `source` | §3.4.5 |
|
|
496
|
+
| `conditions` | Objekt | nein | keine | `item` nur bei `source` | §3.4.6 |
|
|
497
|
+
| `parameters` | Map | nein | `{}` | §3.4.7 | Parameter für manuelle Starts |
|
|
498
|
+
| `execute` | Objekt | ja | | §3.4.8 | Haupt-Worker |
|
|
499
|
+
| `retry.max_technical_failures` | Integer | nein | `3` | ≥ 1 | aufeinanderfolgende technische Fehler bis zum Retry-Block (§6.4) |
|
|
500
|
+
| `on_start`, `on_success`, `on_failure`, `on_crash`, `on_start_error`, `on_cancel`, `on_timeout` | Liste Actions | nein | `[]` | §3.4.10 | Lifecycle-Hooks |
|
|
501
|
+
|
|
502
|
+
#### 3.4.2 `trigger`
|
|
503
|
+
|
|
504
|
+
| `type` | weitere Schlüssel | Bedeutung |
|
|
505
|
+
|---|---|---|
|
|
506
|
+
| `source` | `source` (Pflicht, Source-ID, muss existieren) | Items der Source werden jeden Planungsschritt gepollt; ein Run ist an genau ein Item gebunden. |
|
|
507
|
+
| `schedule` | `cron` (Pflicht, nicht leer), `timezone` (Default `UTC`, IANA-Name wie `Europe/Berlin`) | höchstens ein Run pro Cron-Slot, ohne Item (§6.7). |
|
|
508
|
+
| `manual` | – | nur manuelle Starts (UI/API), ohne Item. |
|
|
509
|
+
|
|
510
|
+
Cron-Syntax: [croner](https://github.com/hexagon/croner) – 5 Felder (`min std tag monat wochentag`) oder 6 mit
|
|
511
|
+
führenden Sekunden; Listen, Bereiche, Schritte (`*/15`), Namen (`MON`, `JAN`) und croner-Erweiterungen (`L`, `#`)
|
|
512
|
+
werden akzeptiert. Ein Ausdruck, der nie feuert (`0 3 31 2 *`), ist ein Fehler. Jeder Workflow kann – unabhängig
|
|
513
|
+
vom Trigger – manuell gestartet werden; Source-Workflows dann mit Item-Referenz.
|
|
514
|
+
|
|
515
|
+
#### 3.4.3 `filter`
|
|
516
|
+
|
|
517
|
+
Deklarativer Filter-AST, siehe §4. Nur für Source-Workflows (sonst Fehler). Fehlt er, passt jedes Item der
|
|
518
|
+
Source (beachte §4.5: standardmäßig nur offene Issues/PRs).
|
|
519
|
+
|
|
520
|
+
#### 3.4.4 `shell_filter`
|
|
521
|
+
|
|
522
|
+
| Schlüssel | Typ | Pflicht | Default | Bedeutung |
|
|
523
|
+
|---|---|---|---|---|
|
|
524
|
+
| `command` | String | ja | | Programm (§2.4). **Keine `args`** – das Item kommt über `ADISPATCH_CHECK_CONTEXT_FILE`. |
|
|
525
|
+
| `timeout` | Dauer | nein | `30s` | > 0; danach `unknown` |
|
|
526
|
+
|
|
527
|
+
Exit `0` = passt, `1` = passt nicht, `≥ 2`/Signal/Timeout/Startfehler = `unknown` (kein Start). Nie gecacht, nie im
|
|
528
|
+
Dry-Run ausgeführt, nicht in Views. Umgebung und Kontextdatei: §6.11.
|
|
529
|
+
|
|
530
|
+
#### 3.4.5 `item_priority`
|
|
531
|
+
|
|
532
|
+
| Schlüssel | Typ | Default | Bedeutung |
|
|
533
|
+
|---|---|---|---|
|
|
534
|
+
| `rules` | Liste `{ when: <Filter-AST>, value: <Zahl> }` | `[]` | in Reihenfolge ausgewertet; die erste Regel mit Ergebnis `true` liefert die Priorität; `false`/`unknown` zählen nicht |
|
|
535
|
+
| `default` | Zahl | `0` | wenn keine Regel zutrifft |
|
|
536
|
+
|
|
537
|
+
Ohne `item_priority` ist die Item-Priorität 0. Regeln dürfen teure Felder verwenden (werden dann aufgelöst).
|
|
538
|
+
|
|
539
|
+
#### 3.4.6 `conditions`
|
|
540
|
+
|
|
541
|
+
`conditions.workflow[]` und `conditions.item[]` haben dieselbe Form:
|
|
542
|
+
|
|
543
|
+
| Schlüssel | Typ | Pflicht | Default | Regeln | Bedeutung |
|
|
544
|
+
|---|---|---|---|---|---|
|
|
545
|
+
| `type` | `shell` | ja | | | |
|
|
546
|
+
| `command` | String | ja | | §2.4 | Prüfprogramm |
|
|
547
|
+
| `args` | Liste String | nein | `[]` | Templates (§5) | |
|
|
548
|
+
| `timeout` | Dauer | nein | `30s` | > 0 | danach `unknown` |
|
|
549
|
+
| `cache` | Dauer | nein | kein Cache | **nur** `conditions.workflow` | Ergebnis `true`/`false` so lange wiederverwenden; `unknown` wird nie gecacht |
|
|
550
|
+
|
|
551
|
+
- `workflow`: werden – falls Kandidaten vorhanden – einmal pro Planungsschritt bzw. einmal pro Cron-Slot geprüft,
|
|
552
|
+
in Listenreihenfolge, Abbruch beim ersten Nicht-`true`. Erlaubt bei allen Triggern; bei `manual` wirkungslos
|
|
553
|
+
(manuelle Starts umgehen Conditions).
|
|
554
|
+
- `item`: pro Kandidat, nur Source-Workflows, nie gecacht.
|
|
555
|
+
- Exit-Semantik wie `shell_filter`. Dry-Runs führen Conditions nie aus.
|
|
556
|
+
|
|
557
|
+
#### 3.4.7 `parameters`
|
|
558
|
+
|
|
559
|
+
Map Parametername → Definition. Werte **übergeben** werden nur bei manuellen Starts (UI-Formular,
|
|
560
|
+
`POST /api/v1/workflows/:id/start` mit `{"parameters": {…}}`) und vor dem Start validiert; automatisch gestartete
|
|
561
|
+
Runs (Source, Cron) erhalten die `default`-Werte (siehe unten).
|
|
562
|
+
|
|
563
|
+
| Schlüssel | Typ | Pflicht | Default | Regeln |
|
|
564
|
+
|---|---|---|---|---|
|
|
565
|
+
| `type` | Enum | ja | | `string`, `number`, `boolean`, `enum` |
|
|
566
|
+
| `values` | Liste | bei `enum` ja | | nicht leer, eindeutig; Zahlen/Booleans werden zu Strings |
|
|
567
|
+
| `required` | Boolean | nein | `false` | |
|
|
568
|
+
| `default` | passend zum Typ | nein | keiner | `string` → String, `number` → endliche Zahl, `boolean` → Boolean, `enum` → einer der `values` (Skalare werden zu Strings, `values: [1, 2]` + `default: 1` ist gültig) |
|
|
569
|
+
| `description` | String | nein | | Anzeige im Formular |
|
|
570
|
+
|
|
571
|
+
Validierung beim Start: unbekannte Parameter → Fehler; fehlender/leerer Wert (`null`, `""`) → `default`, sonst
|
|
572
|
+
Fehler, wenn `required`; `number` akzeptiert auch numerische Strings (`"7"`), `boolean` auch `"true"`/`"false"`.
|
|
573
|
+
|
|
574
|
+
Regeln der Konfigurationsprüfung:
|
|
575
|
+
|
|
576
|
+
- `required: true` **ohne** `default` in einem `source`- oder `schedule`-Workflow ist ein Fehler (automatische
|
|
577
|
+
Runs könnten ihn nie liefern).
|
|
578
|
+
- Im `working_directory` nur Parameter mit `required: true` oder `default`.
|
|
579
|
+
- Automatisch gestartete Runs (Source, Cron) erhalten die **`default`-Werte** aller Parameter, die einen haben
|
|
580
|
+
(gespeichert am Run, sichtbar in API/UI und Kontextdatei). Parameter ohne Default bleiben dort leer
|
|
581
|
+
(`{parameters.x}` = leerer String).
|
|
582
|
+
|
|
583
|
+
#### 3.4.8 `execute`
|
|
584
|
+
|
|
585
|
+
| Schlüssel | Typ | Pflicht | Default | Regeln | Bedeutung |
|
|
586
|
+
|---|---|---|---|---|---|
|
|
587
|
+
| `type` | `shell` | ja | | einziger Executor in V1 | |
|
|
588
|
+
| `command` | String | ja | | §2.4 | Worker-Programm |
|
|
589
|
+
| `args` | Liste String | nein | `[]` | Templates, Modus `text` | Argumente |
|
|
590
|
+
| `working_directory` | String | nein | `{workspace.base_dir}/{workflow.id}/{run.id}` | Modus `path`; beginnt mit `/` oder `{workspace.base_dir}` | Arbeitsverzeichnis = Workspace (`ADISPATCH_WORKSPACE`) |
|
|
591
|
+
| `create_directory` | Boolean | nein | `true` | | Verzeichnis anlegen; bei `false` muss es existieren (sonst `start_error`) |
|
|
592
|
+
| `env` | Map Name → String/Zahl/Boolean | nein | `{}` | Templates; kein `ADISPATCH_`-Präfix | zusätzliche Umgebungsvariablen |
|
|
593
|
+
| `secrets` | Map Name → `{ identity: <id> }` | nein | `{}` | Identity muss existieren; Name nicht zugleich in `env`; kein `ADISPATCH_` | Installation-Token der Identity als Env-Variable (nur im Speicher) |
|
|
594
|
+
| `timeout.max_duration` | Dauer | nein | unbegrenzt | > 0 | Gesamtlaufzeit → `timed_out` |
|
|
595
|
+
| `timeout.idle_timeout` | Dauer | nein | unbegrenzt | > 0 | Zeit ohne Ausgabe auf stdout/stderr → `timed_out` (Warnung, wenn > `max_duration`) |
|
|
596
|
+
| `cancel_grace` | Dauer | nein | `30s` | | Wartezeit zwischen `SIGTERM` und `SIGKILL` an die Prozessgruppe bei Cancel/Timeout |
|
|
597
|
+
|
|
598
|
+
Workspace-Regeln: Liegt der gerenderte Pfad strikt unter `workspace.base_dir`, wird er sicher angelegt
|
|
599
|
+
(Symlink-geprüft) und gemäß `workspace.cleanup` gelöscht. Liegt er außerhalb, wird er wie konfiguriert angelegt,
|
|
600
|
+
aber **nie** automatisch gelöscht. Der Pfad darf nicht `base_dir` selbst sein. Eine Warnung erscheint, wenn ein
|
|
601
|
+
`env`-Schlüssel nach Secret aussieht (`…_TOKEN`, `…PASSWORD`, `API_KEY`, …) und einen nicht-leeren Literalwert hat.
|
|
602
|
+
|
|
603
|
+
#### 3.4.9 `retry`
|
|
604
|
+
|
|
605
|
+
`retry.max_technical_failures` (Integer ≥ 1, Default 3): Nach so vielen **aufeinanderfolgenden** technischen
|
|
606
|
+
Fehlern (`crashed`, `start_error`, `timed_out`) derselben Workflow/Item-Kombination entsteht ein Retry-Block (§6.4).
|
|
607
|
+
|
|
608
|
+
#### 3.4.10 Hooks
|
|
609
|
+
|
|
610
|
+
Sieben Hooks, jeder eine Liste von Actions, die **sequentiell** ausgeführt werden:
|
|
611
|
+
|
|
612
|
+
| Hook | läuft … |
|
|
613
|
+
|---|---|
|
|
614
|
+
| `on_start` | vor dem Start des Workers (nach Re-Read des Items). Scheitert die Kette, startet der Worker **nicht** (→ `start_error`). |
|
|
615
|
+
| `on_success` | nach `completed` + `success` |
|
|
616
|
+
| `on_failure` | nach `completed` + `failure` (fachlich, gültige Ergebnisdatei) |
|
|
617
|
+
| `on_crash` | nach `crashed` |
|
|
618
|
+
| `on_start_error` | nach `start_error` (sofern der Run `starting` erreicht hat) |
|
|
619
|
+
| `on_cancel` | nach `cancelled` (sofern der Run `starting` erreicht hat) |
|
|
620
|
+
| `on_timeout` | nach `timed_out` |
|
|
621
|
+
|
|
622
|
+
Genau ein Abschluss-Hook pro Run; Details in §6.8. `source.update`/`source.comment` nur in Source-Workflows.
|
|
623
|
+
|
|
624
|
+
#### 3.4.11 Actions
|
|
625
|
+
|
|
626
|
+
Gemeinsame Schlüssel aller Actions:
|
|
627
|
+
|
|
628
|
+
| Schlüssel | Typ | Pflicht | Default | Bedeutung |
|
|
629
|
+
|---|---|---|---|---|
|
|
630
|
+
| `type` | Enum | ja | | `source.update`, `source.comment`, `shell` |
|
|
631
|
+
| `id` | Konfigurations-ID | nein | `<hook>.<index>` (z. B. `on_success.1`) | stabiler Name im Journal; **workflowweit** eindeutig |
|
|
632
|
+
| `continue_on_error` | Boolean | nein | `false` | nach endgültigem Scheitern trotzdem mit der nächsten Action weitermachen |
|
|
633
|
+
| `retry.max_attempts` | Integer ≥ 1 | nein | `lifecycle.action_retry.max_attempts` | Versuche insgesamt |
|
|
634
|
+
| `retry.delay` | Dauer | nein | `lifecycle.action_retry.delay` | Pause zwischen Versuchen (`0s` erlaubt) |
|
|
635
|
+
|
|
636
|
+
`source.update` – Labels des Items ändern (nur Source-Workflows):
|
|
637
|
+
|
|
638
|
+
| Schlüssel | Typ | Pflicht | Regeln |
|
|
639
|
+
|---|---|---|---|
|
|
640
|
+
| `labels.add` | Liste String | nein (`[]`) | mindestens eines von `add`/`remove` nicht leer; kein Label in beiden |
|
|
641
|
+
| `labels.remove` | Liste String | nein (`[]`) | |
|
|
642
|
+
|
|
643
|
+
Kein Template. Vor dem Schreiben wird das Item neu gelesen; nur fehlende Labels werden hinzugefügt, nur vorhandene
|
|
644
|
+
entfernt (Vergleich ohne Groß-/Kleinschreibung) → idempotent. Schreib-Identity: Workflow → Source (§6.10).
|
|
645
|
+
|
|
646
|
+
`source.comment` – Kommentar am Issue/PR (nur Source-Workflows; Konversationskommentar, kein Review):
|
|
647
|
+
|
|
648
|
+
| Schlüssel | Typ | Pflicht | Bedeutung |
|
|
649
|
+
|---|---|---|---|
|
|
650
|
+
| `message` | String | ja | Template (Modus `text`); `result.*` nicht in `on_start` |
|
|
651
|
+
| `identity` | Identity-ID | nein | Action-Identity (vor Workflow- und Source-Identity) |
|
|
652
|
+
|
|
653
|
+
`shell` – beliebiges Programm mit Run-/Ergebnis-Kontext:
|
|
654
|
+
|
|
655
|
+
| Schlüssel | Typ | Pflicht | Default | Bedeutung |
|
|
656
|
+
|---|---|---|---|---|
|
|
657
|
+
| `command` | String | ja | | §2.4 |
|
|
658
|
+
| `args` | Liste String | nein | `[]` | Templates |
|
|
659
|
+
| `env` | Map | nein | `{}` | Templates, kein `ADISPATCH_`-Präfix |
|
|
660
|
+
| `timeout` | Dauer | nein | `5m` | Laufzeitlimit pro Versuch (> 0) |
|
|
661
|
+
|
|
662
|
+
Erfolg = Exit 0; alles andere (Exit ≠ 0, Signal, Timeout, Startfehler) = gescheiterter, wiederholbarer Versuch.
|
|
663
|
+
Keine Secrets. Arbeitsverzeichnis: der Run-Workspace, falls er existiert, sonst ein eigenes Verzeichnis pro Versuch
|
|
664
|
+
unter `data-dir/runs/<run>/actions/…`. SIGTERM→SIGKILL-Frist fest 5 s, Log-Limit fest 10 MiB pro Versuch.
|
|
665
|
+
|
|
666
|
+
### 3.5 views/*.yaml
|
|
667
|
+
|
|
668
|
+
```yaml
|
|
669
|
+
# example: reference
|
|
670
|
+
# file: views/reference.yaml
|
|
671
|
+
id: reference
|
|
672
|
+
title: Offene Arbeit
|
|
673
|
+
sources: [project_a, project_b] # gleicher Adaptertyp, eindeutig
|
|
674
|
+
filter:
|
|
675
|
+
all:
|
|
676
|
+
- { field: state, operator: in, value: [open, closed] } # sonst nur offene Items
|
|
677
|
+
- { field: run.last_status, operator: not_equals, value: null }
|
|
678
|
+
fields:
|
|
679
|
+
- { field: repository, label: Repository }
|
|
680
|
+
- { field: number, label: Nr. } # Link automatisch auf url (Felddefinition)
|
|
681
|
+
- { field: title, label: Titel, link: url }
|
|
682
|
+
- { field: labels, label: Labels }
|
|
683
|
+
- { field: run.last_workflow, label: Letzter Agent }
|
|
684
|
+
- { field: run.last_status, label: Status }
|
|
685
|
+
- { field: run.last_result.message, label: Ergebnis }
|
|
686
|
+
- { field: updated_at, label: Aktualisiert }
|
|
687
|
+
sort: { field: updated_at, direction: desc }
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
| Schlüssel | Typ | Pflicht | Default | Regeln | Bedeutung |
|
|
691
|
+
|---|---|---|---|---|---|
|
|
692
|
+
| `id` | Konfigurations-ID | ja | | eindeutig über alle Views | |
|
|
693
|
+
| `title` | String | nein | `id` | nicht leer | |
|
|
694
|
+
| `sources` | Liste Source-ID | ja | | ≥ 1, existieren, eindeutig, gleicher Adaptertyp | Datenquellen |
|
|
695
|
+
| `filter` | Filter-AST | nein | alles | §4; inkl. `run.last_*` | nur Items mit Ergebnis `true` werden gezeigt |
|
|
696
|
+
| `shell_filter` | – | – | | **verboten** (Fehler) | Views führen nie Skripte aus |
|
|
697
|
+
| `fields` | Liste | ja | | ≥ 1 | Spalten in Reihenfolge |
|
|
698
|
+
| `fields[].field` | String | ja | | muss im Feldkatalog des Adaptertyps oder in §4.8 stehen | Feldname (§4.7, §4.8) |
|
|
699
|
+
| `fields[].label` | String | nein | Feldname | | Spaltenüberschrift |
|
|
700
|
+
| `fields[].link` | String (Feldname, geprüft wie `field`) | nein | `link` der Felddefinition (`number`, `title` → `url`) | | Feld, dessen Wert (nur absolute `http(s)`-URL) als Link dient |
|
|
701
|
+
| `sort.field` | String | ja (wenn `sort`) | | geprüft wie `fields[].field` | Sortierfeld |
|
|
702
|
+
| `sort.direction` | Enum | nein | `asc` | `asc`, `desc` | leere Werte immer zuletzt; Gleichstand nach Item-Schlüssel (natürlich) |
|
|
703
|
+
|
|
704
|
+
Laden: beim Öffnen/Refresh aus allen Sources (parallel; eine fehlerhafte Source erscheint als Fehlerzeile, die
|
|
705
|
+
anderen liefern trotzdem), Cache 30 s, max. 2 000 Zeilen. Nicht auflösbare Felder (z. B. fehlende API-Rechte) bleiben erkennbar leer; unbekannte Feldnamen sind bereits
|
|
706
|
+
ein Konfigurationsfehler (§4.3).
|
|
707
|
+
|
|
708
|
+
### 3.6 Bekannte Grenzen (V1)
|
|
709
|
+
|
|
710
|
+
- `shell_filter` hat nur `command` und `timeout`: **keine `args`**, kein `env`. Das Item kommt ausschließlich über
|
|
711
|
+
`ADISPATCH_CHECK_CONTEXT_FILE`; Parameter für das Skript über einen Wrapper bzw. die Kontextdatei.
|
|
712
|
+
- Conditions (`conditions.workflow`, `conditions.item`) haben `args`, aber **kein `env`**; die Umgebung ist die
|
|
713
|
+
Minimalumgebung plus `ADISPATCH_CHECK_CONTEXT_FILE` (§6.11).
|
|
714
|
+
- `shell`-Actions: Frist zwischen `SIGTERM` und `SIGKILL` fest **5 s**, Log-Limit fest **10 MiB** pro Versuch –
|
|
715
|
+
nicht konfigurierbar (`execute.cancel_grace` und `retention.max_log_size_per_run` gelten nur für den Haupt-Worker).
|
|
716
|
+
- Prüfskripte (Conditions, `shell_filter`): Ausgabe wird bis 16 KiB je Stream erfasst, SIGTERM→SIGKILL nach 2 s.
|
|
717
|
+
- Nur Adapter `github` (github.com), nur Identity-Typ `github_app`, nur Kanaltyp `webhook`, nur Executor `shell`.
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## 4. Filter-Sprache
|
|
722
|
+
|
|
723
|
+
Gemeinsam für `filter` (Workflows), `item_priority.rules[].when` und `filter` (Views).
|
|
724
|
+
|
|
725
|
+
### 4.1 Grammatik
|
|
726
|
+
|
|
727
|
+
```text
|
|
728
|
+
Knoten := All | Any | Not | Prädikat
|
|
729
|
+
All := { all: [ Knoten, … ] } # einziger Schlüssel; [] = true
|
|
730
|
+
Any := { any: [ Knoten, … ] } # einziger Schlüssel; [] = false
|
|
731
|
+
Not := { not: Knoten } # einziger Schlüssel
|
|
732
|
+
Prädikat := { field: <String>, operator: <Operator>, value: <Wert> } # genau diese drei Schlüssel
|
|
733
|
+
Operator := equals | not_equals | contains | in | greater_than | less_than
|
|
734
|
+
Wert := String | Zahl | Boolean | null # equals, not_equals, contains
|
|
735
|
+
| [ Skalar, … ] (nicht leer) # in
|
|
736
|
+
| Zahl | String (ISO-Datum) # greater_than, less_than
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
`value` ist immer Pflicht (`null` explizit schreiben). Strukturfehler werden exakt verortet
|
|
740
|
+
(`filter.all[1].operator`). Eine Liste als `value` nimmt **nur** `in`; alle anderen Operatoren verlangen einen
|
|
741
|
+
Skalar (`greater_than`/`less_than`: Zahl oder Datums-String) – sonst Fehler.
|
|
742
|
+
|
|
743
|
+
### 4.2 Operatoren – exakte Semantik
|
|
744
|
+
|
|
745
|
+
`a` = Feldwert, `v` = `value`. Ergebnis `true`/`false`/`unknown`; „unknown (Typ)“ heißt: inkompatible Typen (bei
|
|
746
|
+
katalogisierten Feldern verhindert die Werttyp-Prüfung beim Laden diesen Fall, Tabelle unten).
|
|
747
|
+
|
|
748
|
+
| Operator | Skalares Feld (string/number/boolean) | Datumsfeld (`date`) | Listenfeld (`string[]`) |
|
|
749
|
+
|---|---|---|---|
|
|
750
|
+
| `equals` | gleicher JS-Typ und gleicher Wert; anderer Typ → unknown (Typ). `'1'` vs `1`, `'true'` vs `true` → unknown | beide als ISO-8601 geparst, Vergleich als Zeitpunkt; nicht parsebar → unknown | nicht erlaubt (Validierungsfehler) |
|
|
751
|
+
| `not_equals` | Negation von `equals`; unknown bleibt unknown | dito | nicht erlaubt |
|
|
752
|
+
| `contains` | nur String-Feld: Teilstring (Groß-/Kleinschreibung beachtet) | nicht erlaubt | `true`, wenn ein Element `=== v`; leere Liste → `false`; kein Element vom Typ von `v` → unknown |
|
|
753
|
+
| `in` | `true`, wenn `equals` mit einem Listenelement `true` ist, sonst `false` (string/number) | nicht erlaubt | `true` bei nichtleerer Schnittmenge; leere Feldliste → `false`; keinerlei Typgleichheit → unknown |
|
|
754
|
+
| `greater_than` / `less_than` | Zahl vs Zahl numerisch (String-Felder: nicht erlaubt) | ISO-8601-Zeitpunkte | nicht erlaubt |
|
|
755
|
+
|
|
756
|
+
`null`-Regeln (ein Feldwert `null` bedeutet „bekannt leer“, z. B. kein Autor):
|
|
757
|
+
|
|
758
|
+
- `equals null` ist genau dann `true`, wenn der Feldwert `null` ist; `equals x` auf `null`-Feld → `false`;
|
|
759
|
+
`not_equals` entsprechend umgekehrt.
|
|
760
|
+
- `contains`/`in` auf `null`-Feld → `false`; `null` als Wert ist nur bei `equals`/`not_equals` erlaubt.
|
|
761
|
+
- `greater_than`/`less_than` mit `null` auf einer Seite → unknown.
|
|
762
|
+
|
|
763
|
+
Datumswerte: `YYYY-MM-DD` oder `YYYY-MM-DDTHH:MM[:SS[.fff]][Z|±HH:MM]`; ohne Offset = UTC. Relative Angaben
|
|
764
|
+
(„vor 7 Tagen“) gibt es nicht. Datumssemantik gilt nur für Felder, die der Adapter als `date` deklariert.
|
|
765
|
+
|
|
766
|
+
**Erlaubte Operatoren je Feldtyp** (beim Laden geprüft, Spalte „Operatoren“ in §4.7/§4.8):
|
|
767
|
+
|
|
768
|
+
| Feldtyp | Operatoren | Erlaubte `value`-Typen |
|
|
769
|
+
|---|---|---|
|
|
770
|
+
| `string` | equals, not_equals, contains, in | String (`in`: Liste von Strings); `null` bei equals/not_equals |
|
|
771
|
+
| `number` | equals, not_equals, in, greater_than, less_than | Zahl (`in`: Liste von Zahlen); `null` bei equals/not_equals |
|
|
772
|
+
| `boolean` | equals, not_equals | Boolean (nicht `'true'`); `null` bei equals/not_equals |
|
|
773
|
+
| `date` | equals, not_equals, greater_than, less_than | ISO-8601-String (s. o.); `null` bei equals/not_equals |
|
|
774
|
+
| `string[]` | contains, in | String bzw. Liste von Strings |
|
|
775
|
+
|
|
776
|
+
**Groß-/Kleinschreibung:** `labels` (GitHub) vergleicht ohne Beachtung der Groß-/Kleinschreibung – wie GitHub
|
|
777
|
+
und `source.update` (`Ready` passt auf `ready`). Alle anderen String-Vergleiche beachten sie.
|
|
778
|
+
|
|
779
|
+
### 4.3 Fehlende und unbekannte Felder
|
|
780
|
+
|
|
781
|
+
- **Beim Laden geprüft** gegen den statischen Feldkatalog des Adaptertyps der Source (Workflows: `filter`,
|
|
782
|
+
`item_priority.rules[].when`; Views: `filter`, `fields[].field`, `fields[].link`, `sort.field`): unbekannte
|
|
783
|
+
Feldnamen (mit Vorschlag „did you mean …“), nicht erlaubte Operatoren, falsche Werttypen, ungültige Datumswerte.
|
|
784
|
+
- `run.last_*` ist nur in Views erlaubt; in Workflows ist es ein Fehler („only available in views“).
|
|
785
|
+
- Kann ein bekanntes Feld zur Laufzeit nicht ermittelt werden (fehlender API-Zugriff, z. B. Cross-Repo-Blocker
|
|
786
|
+
ohne Rechte), ist es **unknown** – nie 0, leer oder `false` („fail closed“). `workflow dry-run` zeigt pro Regel
|
|
787
|
+
den Wert und den Grund.
|
|
788
|
+
|
|
789
|
+
### 4.4 Dreiwertige Logik (Kleene)
|
|
790
|
+
|
|
791
|
+
| A | B | `all [A, B]` | `any [A, B]` | `not A` |
|
|
792
|
+
|---|---|---|---|---|
|
|
793
|
+
| true | true | true | true | false |
|
|
794
|
+
| true | false | false | true | false |
|
|
795
|
+
| true | unknown | unknown | true | false |
|
|
796
|
+
| false | false | false | false | true |
|
|
797
|
+
| false | unknown | false | unknown | true |
|
|
798
|
+
| unknown | unknown | unknown | unknown | unknown |
|
|
799
|
+
|
|
800
|
+
`all`: `false` dominiert, sonst unknown falls ein Kind unknown, sonst `true`. `any`: `true` dominiert, sonst
|
|
801
|
+
unknown falls ein Kind unknown, sonst `false`. Ergebnis für den Start: **nur `true` startet**. Bei Views wird nur
|
|
802
|
+
`true` angezeigt (unknown wird gezählt), in `item_priority` trifft nur `true`.
|
|
803
|
+
|
|
804
|
+
### 4.5 Auswertungsreihenfolge, teure Felder, Pushdown
|
|
805
|
+
|
|
806
|
+
- **Teure Felder** (GitHub: `dependencies.*`) stehen nicht in der Listenabfrage und werden lazy nachgeladen – pro
|
|
807
|
+
Item in **einer** Anfragerunde für alle benötigten teuren Felder, gültig für einen Planungsschritt/View-Aufruf.
|
|
808
|
+
- Innerhalb von `all`/`any` werden zuerst Kinder ausgewertet, die nur billige Felder brauchen; sobald das Ergebnis
|
|
809
|
+
feststeht, wird abgebrochen (Rest = `not_evaluated` in der Diagnose). Workflow-Filter laufen zweistufig: erst nur
|
|
810
|
+
billige Felder (teure = unknown); ist das Ergebnis schon `true` oder `false`, ist es endgültig, nur bei unknown
|
|
811
|
+
werden teure Felder später (nach Lock/Block/Conditions) aufgelöst.
|
|
812
|
+
- **Pushdown:** Nur Prädikate der obersten Konjunktion (`all`, verschachtelte `all` werden abgeflacht; ein
|
|
813
|
+
einzelnes Prädikat zählt auch) mit billigem Feld und in `pushdown` deklariertem Operator gehen in die API-Abfrage.
|
|
814
|
+
`any`/`not` werden nie gepusht. Der vollständige Filter wird **immer** zusätzlich lokal ausgewertet.
|
|
815
|
+
- **GitHub-Abfrage:** `state` = `open` (Default!), `closed` oder `all`, abgeleitet aus `state equals open|closed`
|
|
816
|
+
bzw. `state in [...]` auf oberster Ebene; `labels contains "x"` (ohne Komma) → `labels=x` (alle müssen
|
|
817
|
+
vorhanden sein). **Ohne ein solches `state`-Prädikat werden nur offene Issues/PRs gelesen.**
|
|
818
|
+
- **Regel für `state`** (Workflow- und View-`filter`, geprüft beim Laden): `state` darf nur auf oberster Ebene
|
|
819
|
+
stehen (direkt oder in oberster `all`), nur mit `equals` oder `in` und nur mit den Werten `open`/`closed`.
|
|
820
|
+
Unter `any`/`not`, mit `not_equals`/`contains` oder mit anderen Werten wäre es stillschweigend wirkungslos
|
|
821
|
+
(es würden trotzdem nur offene Items gelesen) und ist daher ein Fehler. In `item_priority` ist `state` frei
|
|
822
|
+
verwendbar (beeinflusst die Abfrage nicht).
|
|
823
|
+
|
|
824
|
+
### 4.6 Workflow-Filter vs. View-Filter
|
|
825
|
+
|
|
826
|
+
| | Workflow `filter` | View `filter` |
|
|
827
|
+
|---|---|---|
|
|
828
|
+
| Ergebnis unknown | kein Start (Diagnose `filter_unknown`) | Zeile ausgeblendet, gezählt in `filterUnknown` |
|
|
829
|
+
| Felder | Adapterfelder | Adapterfelder + `run.last_*` |
|
|
830
|
+
| Shell | zusätzlich `shell_filter`, Conditions | nie |
|
|
831
|
+
| Zeitpunkt | jeder Planungsschritt; vor `on_start` erneut der billige Teil auf dem frisch gelesenen Item | Öffnen/Refresh (Cache 30 s) |
|
|
832
|
+
|
|
833
|
+
### 4.7 Feldkatalog des GitHub-Adapters
|
|
834
|
+
|
|
835
|
+
| Feld | Typ | Sinnvolle Operatoren | Pushdown | Teuer | Bedeutung |
|
|
836
|
+
|---|---|---|---|---|---|
|
|
837
|
+
| `number` | number | equals, not_equals, in, greater_than, less_than | | | Issue-/PR-Nummer (Link → `url`) |
|
|
838
|
+
| `title` | string | equals, not_equals, contains, in | | | Titel (Link → `url`) |
|
|
839
|
+
| `state` | string | equals, not_equals, contains, in – im `filter` nur `equals`/`in` auf oberster Ebene (§4.5) | equals, in | | `open` \| `closed` |
|
|
840
|
+
| `labels` | string[] | contains, in | contains | | Label-Namen; Vergleich **ohne** Groß-/Kleinschreibung |
|
|
841
|
+
| `assignees` | string[] | contains, in | | | Logins der Assignees |
|
|
842
|
+
| `author` | string | equals, not_equals, contains, in | | | Login des Autors (`null` falls unbekannt) |
|
|
843
|
+
| `is_pull_request` | boolean | equals, not_equals | | | `true` für Pull Requests |
|
|
844
|
+
| `created_at` | date | equals, not_equals, greater_than, less_than | | | Erstellungszeit |
|
|
845
|
+
| `updated_at` | date | equals, not_equals, greater_than, less_than | | | letzte Änderung |
|
|
846
|
+
| `url` | string | equals, not_equals, contains, in | | | Web-URL |
|
|
847
|
+
| `repository` | string | equals, not_equals, contains, in | | | `owner/repository` (wie in `sources.yaml`) |
|
|
848
|
+
| `dependencies.blocked_by_total` | number | equals, not_equals, in, greater_than, less_than | | ja | Anzahl blockierender Issues (native „blocked by“, inkl. Cross-Repo) |
|
|
849
|
+
| `dependencies.blocked_by_open` | number | equals, not_equals, in, greater_than, less_than | | ja | davon offen; unknown ohne Zugriff |
|
|
850
|
+
| `dependencies.blocked` | boolean | equals, not_equals | | ja | mindestens ein Blocker offen; unknown ohne Zugriff |
|
|
851
|
+
| `dependencies.status` | string | equals, not_equals, contains, in | | ja | `ok` = vollständig aufgelöst, sonst `unknown` (dieses Feld selbst ist dann bekannt) |
|
|
852
|
+
|
|
853
|
+
Abhängigkeiten sind unknown, wenn die API-Zusammenfassung fehlt, ein Blocker einen unerwarteten Status hat oder die
|
|
854
|
+
Identity weniger Blocker sieht, als GitHub meldet. „Offen ⇒ kein Start“ gilt nur, wenn der Filter das ausdrücklich
|
|
855
|
+
verlangt (z. B. `dependencies.blocked_by_open equals 0`).
|
|
856
|
+
|
|
857
|
+
### 4.8 Run-Felder für Views
|
|
858
|
+
|
|
859
|
+
Pro Item der **zuletzt erzeugte** Run (über alle Workflows, globaler Item-Schlüssel). Ohne Run: alle `null`
|
|
860
|
+
(bekannt leer, also z. B. `run.last_status equals null` = „noch nie bearbeitet“).
|
|
861
|
+
|
|
862
|
+
Operatoren je Typ wie in §4.2 (`string`: equals, not_equals, contains, in; `date`: equals, not_equals,
|
|
863
|
+
greater_than, less_than – als Zeitpunkt verglichen).
|
|
864
|
+
|
|
865
|
+
| Feld | Typ | Bedeutung |
|
|
866
|
+
|---|---|---|
|
|
867
|
+
| `run.last_id` | string | Run-ID |
|
|
868
|
+
| `run.last_workflow` | string | Workflow-ID |
|
|
869
|
+
| `run.last_status` | string | technischer Status (`running`, `completed`, `crashed`, …) |
|
|
870
|
+
| `run.last_result` | string | fachliches Ergebnis `success`/`failure` (sonst `null`) |
|
|
871
|
+
| `run.last_result.message` | string | `message` der Ergebnisdatei |
|
|
872
|
+
| `run.last_status_message` | string | letzte Status-Meldung des Workers (`@@ADISPATCH_EVENT` `status`) |
|
|
873
|
+
| `run.last_started_at` | date | Start |
|
|
874
|
+
| `run.last_finished_at` | date | Ende |
|
|
875
|
+
|
|
876
|
+
---
|
|
877
|
+
|
|
878
|
+
## 5. Platzhalter-Referenz
|
|
879
|
+
|
|
880
|
+
### 5.1 Werte
|
|
881
|
+
|
|
882
|
+
| Platzhalter | Wert | Fehlt, wenn … |
|
|
883
|
+
|---|---|---|
|
|
884
|
+
| `{workflow.id}` | Workflow-ID | nie |
|
|
885
|
+
| `{workflow.name}` | `name` (Default `id`) | nie |
|
|
886
|
+
| `{workspace.base_dir}` | absoluter, normalisierter `workspace.base_dir` | nie |
|
|
887
|
+
| `{timestamp}` | aktuelle UTC-Zeit beim Rendern, `YYYYMMDDTHHMMSSZ` | nie |
|
|
888
|
+
| `{run.id}` | Run-ID `run-YYYYMMDDTHHMMSSmmmZ-xxxxxxxx` | – |
|
|
889
|
+
| `{run.trigger}` | `source` \| `schedule` \| `manual` | – |
|
|
890
|
+
| `{run.started_at}` | ISO-8601 mit ms (UTC) | in `on_start` und in Hooks von Runs, deren Worker nie lief |
|
|
891
|
+
| `{workspace.path}` | absoluter Workspace-Pfad des Runs | in `on_start`; in Abschluss-Hooks, wenn der Worker nie gespawnt wurde |
|
|
892
|
+
| `{source.id}` | Source-ID | Run ohne Item |
|
|
893
|
+
| `{source.type}` | `github` | Run ohne Item |
|
|
894
|
+
| `{source.repository}` | `owner/repository` | Run ohne Item |
|
|
895
|
+
| `{item.id}` | Item-ID (GitHub: Nummer als String) | Run ohne Item |
|
|
896
|
+
| `{item.number}` | Nummer (nur bei numerischer Item-ID) | Run ohne Item |
|
|
897
|
+
| `{item.key}` | globaler Schlüssel `<source>:<id>` | Run ohne Item |
|
|
898
|
+
| `{item.url}` | Web-URL | Run ohne Item |
|
|
899
|
+
| `{item.title}` | Titel (frisch gelesen bzw. aus der Planung) | Run ohne Item |
|
|
900
|
+
| `{result.status}` | technischer Endstatus (`completed`, `crashed`, `start_error`, `timed_out`, `cancelled`) | – |
|
|
901
|
+
| `{result.result}` | `success` \| `failure` | bei technischem Fehler/Cancel |
|
|
902
|
+
| `{result.message}` | `message` der Ergebnisdatei | keine/ungültige Ergebnisdatei oder ohne `message` |
|
|
903
|
+
| `{parameters.<name>}` | Parameterwert (`true`, `5`, Text) | optionaler Parameter ohne Wert und ohne Default |
|
|
904
|
+
|
|
905
|
+
Fehlender Wert: Modus `text` → leerer String; Modus `path` → Fehler (`start_error`).
|
|
906
|
+
|
|
907
|
+
### 5.2 Scope – wo welcher Platzhalter erlaubt ist
|
|
908
|
+
|
|
909
|
+
✓ = erlaubt, – = Konfigurationsfehler, (s) = nur wenn `trigger.type: source`, (leer) = erlaubt, aber dort leer.
|
|
910
|
+
|
|
911
|
+
| Platzhalter | `execute.working_directory` (path) | `execute.args`, `execute.env` | `on_start` (`message`, `args`, `env`) | übrige Hooks | `conditions.workflow[].args` | `conditions.item[].args` |
|
|
912
|
+
|---|---|---|---|---|---|---|
|
|
913
|
+
| `workflow.*`, `workspace.base_dir`, `timestamp` | ✓ (`base_dir` nur am Anfang) | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
914
|
+
| `run.id`, `run.trigger` | ✓ | ✓ | ✓ | ✓ | – | – |
|
|
915
|
+
| `run.started_at` | ✓ | ✓ | ✓ (leer) | ✓ | – | – |
|
|
916
|
+
| `workspace.path` | – | ✓ | ✓ (leer) | ✓ | – | – |
|
|
917
|
+
| `source.*`, `item.*` | ✓ (s) | ✓ (s) | ✓ (s) | ✓ (s) | – | ✓ |
|
|
918
|
+
| `result.*` | – | – | – | ✓ | – | – |
|
|
919
|
+
| `parameters.<name>` | ✓ nur `required` oder mit `default` | ✓ | ✓ | ✓ | – | – |
|
|
920
|
+
|
|
921
|
+
Nicht als Template ausgewertet (Platzhalter bleiben wörtlich stehen): alle `command`, `source.update.labels`,
|
|
922
|
+
`shell_filter`, Filterwerte, `secrets`, alle anderen Felder.
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## 6. Auswertungsablauf
|
|
927
|
+
|
|
928
|
+
### 6.1 Planungsschritt (Source-Workflows)
|
|
929
|
+
|
|
930
|
+
Ein Planungsschritt läuft sofort beim Start, danach jeweils `polling_interval` nach Ende des vorherigen sowie direkt
|
|
931
|
+
nach jedem erfolgreichen Reload. Schritte überlappen nie; es gibt keine persistente Kandidaten-Warteschlange.
|
|
932
|
+
|
|
933
|
+
0. **Gate:** Ist der Controller nicht bereit (Recovery läuft), pausiert oder im Drain/Shutdown, wird der ganze
|
|
934
|
+
Schritt übersprungen.
|
|
935
|
+
1. **Pollen** (alle aktiven Source-Workflows parallel; deaktivierte erscheinen nur als `disabled`): Liste der Source
|
|
936
|
+
mit dem pushbaren Filterteil (gleiche Source + gleicher Pushdown = ein API-Aufruf pro Schritt). Fehler einer
|
|
937
|
+
Source betreffen nur deren Workflows (`source_error`).
|
|
938
|
+
2. **Deduplizieren** pro `(Source, Item, Workflow)`.
|
|
939
|
+
3. **Billiger Filter** (teure Felder = unknown): `false` → verworfen. **Item-Priorität** bestimmen (darf teure
|
|
940
|
+
Felder laden).
|
|
941
|
+
4. **Sortieren** aller Kandidaten aller Workflows (§6.2).
|
|
942
|
+
5. Pro Kandidat **sequentiell** in dieser Reihenfolge:
|
|
943
|
+
1. freie Kapazität global und für den Workflow? (zuerst, damit volle Limits keine API-Aufrufe kosten)
|
|
944
|
+
2. aktiver Run desselben Workflows für dieses Item (Lock) → `locked`
|
|
945
|
+
3. aktiver Retry-Block → `blocked`
|
|
946
|
+
4. Workflow-Conditions (einmal pro Schritt und Workflow, ggf. aus dem `cache`)
|
|
947
|
+
5. teurer deklarativer Filter (nur wenn der billige Teil unknown war): `false` → `filtered`, unknown → `filter_unknown`
|
|
948
|
+
6. `shell_filter`
|
|
949
|
+
7. Item-Conditions
|
|
950
|
+
8. synchron und atomar: Gate erneut, Workflow noch aktiv **und unverändert** (sonst `workflow_unavailable`),
|
|
951
|
+
Kapazität erneut, Reservierung in SQLite (scheitert bei Lock/Block)
|
|
952
|
+
9. Start an den Run-Controller (§6.5)
|
|
953
|
+
6. Ein nicht startfähiger Kandidat blockiert nie die folgenden.
|
|
954
|
+
|
|
955
|
+
Diagnose je Workflow (letzter Schritt, max. 500 Kandidaten) unter `GET /api/v1/workflows/:id` bzw. im Dry-Run.
|
|
956
|
+
|
|
957
|
+
### 6.2 Priorität und Tie-Breaker
|
|
958
|
+
|
|
959
|
+
Totale Ordnung über alle Kandidaten: (1) **Workflow-Priorität** absteigend (effektiv, inkl. Override),
|
|
960
|
+
(2) **Item-Priorität** absteigend, (3) Erstellungszeit des Items aufsteigend (fehlend/ungültig zuletzt),
|
|
961
|
+
(4) Item-Schlüssel natürlich aufsteigend (`repo:9` < `repo:10`), (5) Workflow-ID aufsteigend. Ein Item mit hoher
|
|
962
|
+
Item-Priorität überholt also keinen Workflow mit höherer Workflow-Priorität. Kein Aging.
|
|
963
|
+
|
|
964
|
+
### 6.3 Limits
|
|
965
|
+
|
|
966
|
+
- Global `scheduler.max_concurrent` und pro Workflow `max_concurrent` gelten nur für **automatische** Starts
|
|
967
|
+
(Source, Cron). Aktive Runs **aller** Trigger zählen (inkl. manueller und `recovery_pending`).
|
|
968
|
+
- Aktiv = `reserved`, `starting`, `running`, `cancelling`, `finalizing`, `recovery_pending` (also auch während
|
|
969
|
+
Abschluss-Hooks laufen).
|
|
970
|
+
- Overrides wirken sofort.
|
|
971
|
+
|
|
972
|
+
### 6.4 Locks und Retry-Blocks
|
|
973
|
+
|
|
974
|
+
- **Lock:** höchstens ein aktiver Run pro `(Workflow, Item)`. Verschiedene Workflows dürfen dasselbe Item parallel
|
|
975
|
+
bearbeiten. Cron-Runs: Lock pro Slot; manuelle Runs ohne Item: nie Konflikt.
|
|
976
|
+
- **Zählung technischer Fehler** pro `(Workflow, Item)` bzw. pro Workflow bei Runs ohne Item:
|
|
977
|
+
`crashed`, `start_error`, `timed_out` erhöhen; `completed`+`success` setzt auf 0 (neue Zählperiode);
|
|
978
|
+
`completed`+`failure` und `cancelled` ändern nichts. Auch manuelle Runs zählen.
|
|
979
|
+
- Erreicht der Zähler `retry.max_technical_failures`, entsteht in **derselben Transaktion** wie der Übergang nach
|
|
980
|
+
`finalizing` ein Retry-Block – also bevor `on_crash`/`on_timeout` z. B. das Label zurücksetzen. Der Block
|
|
981
|
+
verhindert automatische Starts, bis er entfernt wird (UI „Remove Block“, `POST /api/v1/blocks/:id/clear`);
|
|
982
|
+
Entfernen startet eine neue Zählperiode. Benachrichtigung `retry_block_created`.
|
|
983
|
+
|
|
984
|
+
### 6.5 Run-Start
|
|
985
|
+
|
|
986
|
+
1. Source-Runs: Item frisch lesen. Nicht mehr vorhanden → `cancelled` (ohne Hooks, nicht gezählt). Bei
|
|
987
|
+
automatischen Runs zusätzlich billiger Filterteil auf dem frischen Item: `false` → `cancelled` ohne Hooks
|
|
988
|
+
(unknown startet trotzdem). Source fehlt oder Lesefehler → `start_error` **ohne** Hooks (gezählt).
|
|
989
|
+
2. `starting`, `on_start`-Kette. Scheitert sie → Worker startet nicht, `start_error`, `on_start_error`.
|
|
990
|
+
3. Workspace-Pfad, `args`/`env` rendern, Secrets holen, Kontextdatei schreiben; Fehler → `start_error`, `on_start_error`.
|
|
991
|
+
4. Worker als eigene Prozessgruppe starten → `running`.
|
|
992
|
+
5. Ergebnis klassifizieren (§6.8), Abschluss-Hook, Workspace-Cleanup, Endstatus, Lock frei.
|
|
993
|
+
|
|
994
|
+
### 6.6 Manueller Start
|
|
995
|
+
|
|
996
|
+
UI oder `POST /api/v1/workflows/:id/start` `{"item"?: "…", "parameters"?: {…}}`:
|
|
997
|
+
|
|
998
|
+
- **Umgeht:** `enabled`/Overrides, Pause, Filter, `shell_filter`, Conditions, Retry-Blocks, Prioritäten, Limits,
|
|
999
|
+
den Filter-Recheck vor `on_start`.
|
|
1000
|
+
- **Umgeht nicht:** den Lock pro Workflow+Item (HTTP 409 `lock_conflict`), Drain/Shutdown, laufende Recovery,
|
|
1001
|
+
Parametervalidierung.
|
|
1002
|
+
- Source-Workflow: Item-Referenz Pflicht – `123`, `#123`, `owner/repo#123` oder
|
|
1003
|
+
`https://github.com/owner/repo/issues/123` bzw. `/pull/123` (Owner/Repo ohne Groß-/Kleinschreibung, muss zur
|
|
1004
|
+
Source passen); das Item muss existieren. Andere Trigger: Item verboten.
|
|
1005
|
+
- Manuelle Runs zählen zu den Limits automatischer Starts und zum Fehlerzähler.
|
|
1006
|
+
|
|
1007
|
+
### 6.7 Cron
|
|
1008
|
+
|
|
1009
|
+
- Slots berechnet croner in der `timezone` des Triggers (Default `UTC`), Sekundengenauigkeit.
|
|
1010
|
+
- **Höchstens ein Run pro Slot**; jeder behandelte Slot bekommt einen `cron_firings`-Eintrag mit Entscheidung.
|
|
1011
|
+
- Pro Slot: aktiv? Gate offen? Limits frei? kein Workflow-Block? → Workflow-Conditions → nochmals prüfen →
|
|
1012
|
+
Eintrag + Reservierung in einer Transaktion. Sonst `skipped_disabled|paused|not_ready|draining|limit|blocked|
|
|
1013
|
+
condition|condition_unknown`.
|
|
1014
|
+
- **Verpasste Slots werden nie nachgeholt.** Toleranz 60 s: Ein Slot, der höchstens so spät behandelt wird, läuft
|
|
1015
|
+
normal; ältere werden `skipped_missed`. Nach Neustart: ab dem letzten persistierten Slot; nur der jüngste (falls
|
|
1016
|
+
in der Toleranz) kann noch laufen. Neuer Workflow/geänderter Ausdruck: ab „jetzt“.
|
|
1017
|
+
- **DST:** Jede Wanduhrzeit höchstens einmal (beim ersten Auftreten); tägliche Zeiten in der Frühjahrslücke laufen
|
|
1018
|
+
einmal, um die Lückenlänge verschoben; stündliche Slots in der Lücke entfallen. Exakte Intervalle → `UTC`.
|
|
1019
|
+
- Details und Tabellen: [cron-semantics.md](cron-semantics.md).
|
|
1020
|
+
|
|
1021
|
+
### 6.8 Outcome → Status → Hook
|
|
1022
|
+
|
|
1023
|
+
| Situation | Technischer Status | Fachlich | Hook | Technischer Fehler (zählt)? |
|
|
1024
|
+
|---|---|---|---|---|
|
|
1025
|
+
| Item vor dem Start verschwunden / passt nicht mehr | `cancelled` | – | keiner | nein |
|
|
1026
|
+
| Cancel vor `starting` | `cancelled` | – | keiner | nein |
|
|
1027
|
+
| Source nicht verfügbar / Item-Re-Read scheitert | `start_error` | – | **keiner** | ja |
|
|
1028
|
+
| `on_start`-Kette gestoppt | `start_error` | – | `on_start_error` | ja |
|
|
1029
|
+
| Cancel während `on_start` oder vor Spawn | `cancelled` | – | `on_cancel` | nein |
|
|
1030
|
+
| Vorbereitung scheitert (Template, Workspace, Secret-Token) | `start_error` | – | `on_start_error` | ja |
|
|
1031
|
+
| Spawn unmöglich (ENOENT, keine Exec-Rechte) | `start_error` | – | `on_start_error` | ja |
|
|
1032
|
+
| kontrollierter Abbruch (UI/API/Cancel-All) | `cancelled` | – | `on_cancel` | nein |
|
|
1033
|
+
| `max_duration` oder `idle_timeout` überschritten | `timed_out` | – | `on_timeout` | ja |
|
|
1034
|
+
| durch Signal beendet (nicht Cancel/Timeout) | `crashed` | – | `on_crash` | ja |
|
|
1035
|
+
| Exit ≠ 0 (Ergebnisdatei wird ignoriert) | `crashed` | – | `on_crash` | ja |
|
|
1036
|
+
| Exit 0 + ungültige/zu große Ergebnisdatei | `crashed` | – | `on_crash` | ja |
|
|
1037
|
+
| Exit 0 + keine Ergebnisdatei | `completed` | `success` | `on_success` | nein (setzt Zähler zurück) |
|
|
1038
|
+
| Exit 0 + `"result": "success"` | `completed` | `success` | `on_success` | nein (setzt Zähler zurück) |
|
|
1039
|
+
| Exit 0 + `"result": "failure"` | `completed` | `failure` | `on_failure` | nein (Zähler unverändert) |
|
|
1040
|
+
| Controller-Neustart, Worker nachweislich weg | `crashed` (bzw. `cancelled`, wenn Cancel lief) | – | `on_crash` / `on_cancel` | `crashed`: ja |
|
|
1041
|
+
| Controller-Neustart, Prozess nicht sicher zuordenbar | `recovery_pending` (hält Lock) | – | – | – |
|
|
1042
|
+
|
|
1043
|
+
Eine fachliche `failure` gibt es nur über eine gültige Ergebnisdatei; ein Exit-Code allein ist nie „failure“.
|
|
1044
|
+
Ergebnisdatei-Format: [worker-protocol.md](worker-protocol.md).
|
|
1045
|
+
|
|
1046
|
+
### 6.9 Action-Ketten
|
|
1047
|
+
|
|
1048
|
+
- Actions eines Hooks laufen **sequentiell** in Listenreihenfolge; jeder Zustand wird vor dem Weiterarbeiten
|
|
1049
|
+
persistiert (Journal `pending → running → succeeded|failed|skipped`, jeder Versuch einzeln).
|
|
1050
|
+
- **Retry:** bis `max_attempts` Versuche, Pause `delay` (mindestens ein `Retry-After`). Sofort endgültig (ohne
|
|
1051
|
+
weitere Versuche) bei nicht wiederholbaren Fehlern: Template-/Konfigurationsfehler, Authentifizierung, Item
|
|
1052
|
+
nicht gefunden, ungültige Anfrage. Wiederholbar: transiente API-Fehler, Rate-Limits, jeder `shell`-Fehlschlag.
|
|
1053
|
+
- **Stopp:** Scheitert eine Action endgültig ohne `continue_on_error`, bleibt die Kette dort stehen; folgende
|
|
1054
|
+
Actions bleiben `pending`, der Run zeigt „Lifecycle failed“, Benachrichtigung `lifecycle_action_failed`. Mit
|
|
1055
|
+
`continue_on_error: true` geht es weiter (keine Benachrichtigung).
|
|
1056
|
+
- Der Run-Status hängt nicht vom Hook-Ergebnis ab – Ausnahme `on_start` (→ `start_error`).
|
|
1057
|
+
- **Manuell:** `POST /api/v1/actions/:id/retry` (frische Versuchsrunde, danach läuft die Kette weiter; nicht für
|
|
1058
|
+
`on_start`, wenn der Run nicht mehr `starting` ist) und `POST /api/v1/actions/:id/skip` (failed/pending →
|
|
1059
|
+
`skipped`, Kette läuft weiter). Der Haupt-Worker wird nie neu gestartet; erfolgreiche Actions laufen nie erneut.
|
|
1060
|
+
- **Idempotenz:** `source.update` liest vor dem Schreiben neu und ändert nur, was fehlt. `source.comment` hängt als
|
|
1061
|
+
letzte Zeile `<!-- adispatchd:<run-id>:<journal-action-id> -->` an; vor jeder Wiederholung (und nach Neustart) wird ein
|
|
1062
|
+
Kommentar mit diesem Marker gesucht, der von der eigenen App stammt → kein Duplikat. `<!--` im gerenderten Text
|
|
1063
|
+
wird zu `<!--`.
|
|
1064
|
+
- **Neustart:** unterbrochene `source.*`-Actions werden fortgesetzt; eine unterbrochene `shell`-Action wird
|
|
1065
|
+
`failed` und nie automatisch wiederholt (manueller Retry). Eine unterbrochene `on_start`-Kette wird verworfen.
|
|
1066
|
+
|
|
1067
|
+
### 6.10 Identitäten je Zugriff
|
|
1068
|
+
|
|
1069
|
+
| Zugriff | Identity |
|
|
1070
|
+
|---|---|
|
|
1071
|
+
| Pollen, Item lesen, Dependencies auflösen, Item-Re-Read vor `on_start` | `sources.<id>.identity` |
|
|
1072
|
+
| `source.update` (inkl. Re-Read vor dem Schreiben) | Workflow-`identity` → Source-`identity` |
|
|
1073
|
+
| `source.comment` (inkl. Marker-Suche) | Action-`identity` → Workflow-`identity` → Source-`identity` |
|
|
1074
|
+
| `execute.secrets.<NAME>` | die angegebene Identity (Installation-Token, ca. 1 h gültig, beim Start geholt) |
|
|
1075
|
+
| `shell`-Actions, Conditions, `shell_filter` | keine (keine Secrets) |
|
|
1076
|
+
|
|
1077
|
+
### 6.11 Umgebung von Worker, Actions und Prüfskripten
|
|
1078
|
+
|
|
1079
|
+
| | Haupt-Worker | `shell`-Action | Conditions / `shell_filter` |
|
|
1080
|
+
|---|---|---|---|
|
|
1081
|
+
| Basis | `PATH`, `HOME`, `LANG`, `TZ`, `USER`, `LOGNAME`, `TMPDIR` aus der Controller-Umgebung (PATH-Fallback `/usr/local/bin:/usr/bin:/bin`) | wie Worker | dieselben Variablen (ohne PATH-Fallback) |
|
|
1082
|
+
| Konfiguriert | `execute.env`, dann `execute.secrets` | Action-`env` | – |
|
|
1083
|
+
| Orchestrator | `ADISPATCH_RUN_ID`, `ADISPATCH_WORKFLOW_ID`, `ADISPATCH_CONTEXT_FILE`, `ADISPATCH_RESULT_FILE`, `ADISPATCH_WORKSPACE`; bei Item zusätzlich `ADISPATCH_SOURCE_TYPE`, `ADISPATCH_SOURCE_ID`, `ADISPATCH_SOURCE_ITEM_ID`, `ADISPATCH_SOURCE_URL` | dieselben (`ADISPATCH_WORKSPACE` = Arbeitsverzeichnis der Action; Kontext `kind: lifecycle_action` mit Hook, Action, `result`) | `ADISPATCH_CHECK_CONTEXT_FILE` (JSON: `kind`, `workflow.id`, bei Item `source{id,type,item_id,item_key,url,item_type,created_at,attributes}`) |
|
|
1084
|
+
| Arbeitsverzeichnis | Workspace | Workspace, falls vorhanden, sonst Versuchsverzeichnis | temporäres Verzeichnis |
|
|
1085
|
+
|
|
1086
|
+
Vorrang (spätere gewinnen): Basis < `env` < `secrets` < `ADISPATCH_*`. Die übrige Controller-Umgebung wird **nie**
|
|
1087
|
+
vererbt. Details zu Kontextdatei, Ergebnisdatei, Events und Cancel: [worker-protocol.md](worker-protocol.md).
|
|
1088
|
+
|
|
1089
|
+
### 6.12 Workspace-Cleanup
|
|
1090
|
+
|
|
1091
|
+
Nach dem Abschluss-Hook: `always` = immer, `on_success` = nur `completed`+`success`, `never` = nie. Gelöscht wird nur
|
|
1092
|
+
ein Workspace strikt unter `workspace.base_dir` (auch nach Auflösen von Symlinks; ein Workspace, der selbst ein
|
|
1093
|
+
Symlink ist, wird nie gelöscht). Später entfernt die Retention auch verbliebene Workspaces gelöschter Runs.
|
|
1094
|
+
|
|
1095
|
+
### 6.13 Dry-Run
|
|
1096
|
+
|
|
1097
|
+
`adispatch workflow dry-run <id>` (gegen die laufende Instanz, sonst mit `--config-dir` lokal) bzw.
|
|
1098
|
+
`POST /api/v1/workflows/:id/dry-run`: Source-Abfrage, Filter-Trace pro Regel (inkl. Dependencies), Prioritäten,
|
|
1099
|
+
Locks/Blocks, Kapazität (in Prioritätsreihenfolge simuliert). Reserviert nichts, startet keine Worker oder Actions
|
|
1100
|
+
und führt **nie** `shell_filter`/Conditions aus (`not_evaluated`, Entscheidung `pending_checks`). Cron-Workflows:
|
|
1101
|
+
nächste 5 Slots und Prognose; Manual-Workflows: nur Hinweise.
|
|
1102
|
+
|
|
1103
|
+
---
|
|
1104
|
+
|
|
1105
|
+
## 7. Vollständige Beispiele
|
|
1106
|
+
|
|
1107
|
+
### 7.1 Minimal-Setup (ohne GitHub)
|
|
1108
|
+
|
|
1109
|
+
Ein manueller Workflow, keine Sources/Identities nötig.
|
|
1110
|
+
|
|
1111
|
+
```yaml
|
|
1112
|
+
# example: minimal
|
|
1113
|
+
# file: config.yaml
|
|
1114
|
+
# Alles optional – nur zur Verdeutlichung zwei Werte.
|
|
1115
|
+
server:
|
|
1116
|
+
port: 8787
|
|
1117
|
+
scheduler:
|
|
1118
|
+
max_concurrent: 2
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
```yaml
|
|
1122
|
+
# example: minimal
|
|
1123
|
+
# file: workflows/hello.yaml
|
|
1124
|
+
id: hello
|
|
1125
|
+
name: Hello World
|
|
1126
|
+
trigger: { type: manual }
|
|
1127
|
+
parameters:
|
|
1128
|
+
name: { type: string, default: world, description: Wen grüßen? }
|
|
1129
|
+
execute:
|
|
1130
|
+
type: shell
|
|
1131
|
+
command: scripts/hello.sh # relativ zum config-dir
|
|
1132
|
+
args: ['{parameters.name}']
|
|
1133
|
+
timeout: { max_duration: 10m }
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
### 7.2 Coder-/Reviewer-Paar über Labels
|
|
1137
|
+
|
|
1138
|
+
Label-Fluss auf dem Issue: `ready-for-coding` → `coding` → `ready-for-review` → `reviewing` → `ready-to-merge`
|
|
1139
|
+
(bzw. zurück zu `ready-for-coding` oder `needs-maintainer`). Der Reviewer hat die höhere Workflow-Priorität, damit
|
|
1140
|
+
fertige Arbeit zuerst geprüft wird.
|
|
1141
|
+
|
|
1142
|
+
```yaml
|
|
1143
|
+
# example: github
|
|
1144
|
+
# file: config.yaml
|
|
1145
|
+
scheduler:
|
|
1146
|
+
polling_interval: 60s
|
|
1147
|
+
max_concurrent: 4
|
|
1148
|
+
workspace:
|
|
1149
|
+
cleanup: on_success
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
```yaml
|
|
1153
|
+
# example: github
|
|
1154
|
+
# file: identities.yaml
|
|
1155
|
+
identities:
|
|
1156
|
+
maintainer: { type: github_app, app_id: 100001, installation_id: 200001, private_key_env: GITHUB_MAINTAINER_PRIVATE_KEY }
|
|
1157
|
+
coder: { type: github_app, app_id: 100002, installation_id: 200002, private_key_env: GITHUB_CODER_PRIVATE_KEY }
|
|
1158
|
+
reviewer: { type: github_app, app_id: 100003, installation_id: 200003, private_key_env: GITHUB_REVIEWER_PRIVATE_KEY }
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
```yaml
|
|
1162
|
+
# example: github
|
|
1163
|
+
# file: sources.yaml
|
|
1164
|
+
sources:
|
|
1165
|
+
project_a: { type: github, owner: example, repository: project-a, identity: maintainer }
|
|
1166
|
+
project_b: { type: github, owner: example, repository: project-b, identity: maintainer }
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
```yaml
|
|
1170
|
+
# example: github
|
|
1171
|
+
# file: workflows/coder.yaml
|
|
1172
|
+
id: coder
|
|
1173
|
+
name: Coder – Project A
|
|
1174
|
+
priority: 50
|
|
1175
|
+
max_concurrent: 2
|
|
1176
|
+
identity: coder
|
|
1177
|
+
trigger: { type: source, source: project_a }
|
|
1178
|
+
filter:
|
|
1179
|
+
all:
|
|
1180
|
+
- { field: labels, operator: contains, value: ready-for-coding } # Pushdown: labels=ready-for-coding
|
|
1181
|
+
- { field: is_pull_request, operator: equals, value: false }
|
|
1182
|
+
- { field: dependencies.blocked_by_open, operator: equals, value: 0 } # teuer, lazy
|
|
1183
|
+
item_priority:
|
|
1184
|
+
rules:
|
|
1185
|
+
- when: { field: labels, operator: contains, value: 'priority:high' }
|
|
1186
|
+
value: 50
|
|
1187
|
+
conditions:
|
|
1188
|
+
workflow:
|
|
1189
|
+
- { type: shell, command: scripts/check-credits.sh, timeout: 15s, cache: 60s }
|
|
1190
|
+
execute:
|
|
1191
|
+
type: shell
|
|
1192
|
+
command: scripts/coder.sh
|
|
1193
|
+
args: ['{item.url}']
|
|
1194
|
+
secrets:
|
|
1195
|
+
GH_TOKEN: { identity: coder }
|
|
1196
|
+
timeout: { max_duration: 6h, idle_timeout: 45m }
|
|
1197
|
+
retry: { max_technical_failures: 3 }
|
|
1198
|
+
on_start:
|
|
1199
|
+
- type: source.update
|
|
1200
|
+
labels: { add: [coding], remove: [ready-for-coding] }
|
|
1201
|
+
on_success:
|
|
1202
|
+
- type: source.update
|
|
1203
|
+
labels: { add: [ready-for-review], remove: [coding] }
|
|
1204
|
+
- type: source.comment
|
|
1205
|
+
message: 'Coder fertig (Run {run.id}): {result.message}'
|
|
1206
|
+
continue_on_error: true
|
|
1207
|
+
on_failure:
|
|
1208
|
+
- type: source.update
|
|
1209
|
+
labels: { add: [needs-maintainer], remove: [coding] }
|
|
1210
|
+
- type: source.comment
|
|
1211
|
+
message: 'Needs maintainer: {result.message}'
|
|
1212
|
+
on_crash: [ { type: source.update, labels: { add: [ready-for-coding], remove: [coding] } } ]
|
|
1213
|
+
on_start_error: [ { type: source.update, labels: { add: [ready-for-coding], remove: [coding] } } ]
|
|
1214
|
+
on_timeout: [ { type: source.update, labels: { add: [ready-for-coding], remove: [coding] } } ]
|
|
1215
|
+
on_cancel: [ { type: source.update, labels: { add: [ready-for-coding], remove: [coding] } } ]
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
```yaml
|
|
1219
|
+
# example: github
|
|
1220
|
+
# file: workflows/reviewer.yaml
|
|
1221
|
+
id: reviewer
|
|
1222
|
+
name: Reviewer – Project A
|
|
1223
|
+
priority: 60
|
|
1224
|
+
identity: reviewer
|
|
1225
|
+
trigger: { type: source, source: project_a }
|
|
1226
|
+
filter:
|
|
1227
|
+
all:
|
|
1228
|
+
- { field: labels, operator: contains, value: ready-for-review }
|
|
1229
|
+
- { field: is_pull_request, operator: equals, value: false }
|
|
1230
|
+
execute:
|
|
1231
|
+
type: shell
|
|
1232
|
+
command: scripts/reviewer.sh
|
|
1233
|
+
args: ['{item.url}']
|
|
1234
|
+
secrets:
|
|
1235
|
+
GH_TOKEN: { identity: reviewer }
|
|
1236
|
+
timeout: { max_duration: 2h }
|
|
1237
|
+
on_start:
|
|
1238
|
+
- type: source.update
|
|
1239
|
+
labels: { add: [reviewing], remove: [ready-for-review] }
|
|
1240
|
+
on_success: # Review bestanden
|
|
1241
|
+
- type: source.update
|
|
1242
|
+
labels: { add: [ready-to-merge], remove: [reviewing] }
|
|
1243
|
+
on_failure: # Änderungen nötig → zurück an den Coder
|
|
1244
|
+
- type: source.update
|
|
1245
|
+
labels: { add: [ready-for-coding], remove: [reviewing] }
|
|
1246
|
+
- type: source.comment
|
|
1247
|
+
message: 'Review: {result.message}'
|
|
1248
|
+
on_crash: [ { type: source.update, labels: { add: [ready-for-review], remove: [reviewing] } } ]
|
|
1249
|
+
on_start_error: [ { type: source.update, labels: { add: [ready-for-review], remove: [reviewing] } } ]
|
|
1250
|
+
on_timeout: [ { type: source.update, labels: { add: [ready-for-review], remove: [reviewing] } } ]
|
|
1251
|
+
on_cancel: [ { type: source.update, labels: { add: [ready-for-review], remove: [reviewing] } } ]
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
### 7.3 View über zwei Repositories
|
|
1255
|
+
|
|
1256
|
+
```yaml
|
|
1257
|
+
# example: github
|
|
1258
|
+
# file: views/needs-maintainer.yaml
|
|
1259
|
+
id: needs-maintainer
|
|
1260
|
+
title: Needs Maintainer
|
|
1261
|
+
sources: [project_a, project_b]
|
|
1262
|
+
filter:
|
|
1263
|
+
all:
|
|
1264
|
+
- { field: labels, operator: contains, value: needs-maintainer }
|
|
1265
|
+
fields:
|
|
1266
|
+
- { field: repository, label: Repository }
|
|
1267
|
+
- { field: number, label: Issue }
|
|
1268
|
+
- { field: title, label: Titel, link: url }
|
|
1269
|
+
- { field: run.last_workflow, label: Letzter Agent }
|
|
1270
|
+
- { field: run.last_result.message, label: Ergebnis }
|
|
1271
|
+
- { field: updated_at, label: Aktualisiert }
|
|
1272
|
+
sort: { field: updated_at, direction: desc }
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
### 7.4 Cron-Workflow
|
|
1276
|
+
|
|
1277
|
+
```yaml
|
|
1278
|
+
# example: cron
|
|
1279
|
+
# extends: minimal
|
|
1280
|
+
# file: workflows/nightly-maintenance.yaml
|
|
1281
|
+
id: nightly-maintenance
|
|
1282
|
+
name: Nightly Maintenance
|
|
1283
|
+
priority: 20
|
|
1284
|
+
trigger:
|
|
1285
|
+
type: schedule
|
|
1286
|
+
cron: '0 3 * * *' # täglich 03:00 Ortszeit
|
|
1287
|
+
timezone: Europe/Berlin
|
|
1288
|
+
conditions:
|
|
1289
|
+
workflow:
|
|
1290
|
+
- type: shell
|
|
1291
|
+
command: scripts/maintenance-window.sh
|
|
1292
|
+
args: ['{timestamp}']
|
|
1293
|
+
execute:
|
|
1294
|
+
type: shell
|
|
1295
|
+
command: scripts/maintenance.sh
|
|
1296
|
+
args: ['--run', '{run.id}', '--trigger', '{run.trigger}']
|
|
1297
|
+
timeout: { max_duration: 1h }
|
|
1298
|
+
retry: { max_technical_failures: 2 } # Block im Workflow-Scope (kein Item)
|
|
1299
|
+
on_failure:
|
|
1300
|
+
- type: shell
|
|
1301
|
+
command: scripts/notify.sh
|
|
1302
|
+
args: ['{workflow.name}: {result.message}']
|
|
1303
|
+
on_crash:
|
|
1304
|
+
- type: shell
|
|
1305
|
+
command: scripts/notify.sh
|
|
1306
|
+
args: ['{workflow.name} abgestürzt ({result.status}), Run {run.id}']
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
### 7.5 Manual-Workflow mit Parametern
|
|
1310
|
+
|
|
1311
|
+
```yaml
|
|
1312
|
+
# example: manual-params
|
|
1313
|
+
# extends: minimal
|
|
1314
|
+
# file: workflows/deploy.yaml
|
|
1315
|
+
id: deploy
|
|
1316
|
+
name: Deploy
|
|
1317
|
+
trigger: { type: manual }
|
|
1318
|
+
parameters:
|
|
1319
|
+
environment:
|
|
1320
|
+
type: enum
|
|
1321
|
+
values: [staging, production]
|
|
1322
|
+
required: true
|
|
1323
|
+
description: Zielumgebung
|
|
1324
|
+
version:
|
|
1325
|
+
type: string
|
|
1326
|
+
required: true
|
|
1327
|
+
dry_run:
|
|
1328
|
+
type: boolean
|
|
1329
|
+
default: false
|
|
1330
|
+
replicas:
|
|
1331
|
+
type: number
|
|
1332
|
+
default: 2
|
|
1333
|
+
execute:
|
|
1334
|
+
type: shell
|
|
1335
|
+
command: scripts/deploy.sh
|
|
1336
|
+
# environment ist Pflicht → im Pfad erlaubt (wird zu einem sicheren Segment)
|
|
1337
|
+
working_directory: '{workspace.base_dir}/deploy/{parameters.environment}/{run.id}'
|
|
1338
|
+
args: ['--version={parameters.version}', '--dry-run={parameters.dry_run}', '--replicas={parameters.replicas}']
|
|
1339
|
+
env:
|
|
1340
|
+
DEPLOY_ENV: '{parameters.environment}'
|
|
1341
|
+
timeout: { max_duration: 30m }
|
|
1342
|
+
on_success:
|
|
1343
|
+
- type: shell
|
|
1344
|
+
command: scripts/notify.sh
|
|
1345
|
+
args: ['Deploy {parameters.version} nach {parameters.environment}: {result.message}']
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
Start: `curl -X POST -H 'X-Adispatch-Request: 1' -H 'Content-Type: application/json'
|
|
1349
|
+
-d '{"parameters":{"environment":"staging","version":"1.4.2"}}' http://127.0.0.1:8787/api/v1/workflows/deploy/start`.
|
|
1350
|
+
|
|
1351
|
+
### 7.6 Notifications per Webhook
|
|
1352
|
+
|
|
1353
|
+
```yaml
|
|
1354
|
+
# example: notifications
|
|
1355
|
+
# extends: minimal
|
|
1356
|
+
# file: config.yaml
|
|
1357
|
+
notifications:
|
|
1358
|
+
on: [retry_block_created, lifecycle_action_failed, recovery_failed]
|
|
1359
|
+
channels:
|
|
1360
|
+
- id: slack # Slack-Incoming-Webhook; URL enthält ein Token → aus der Umgebung
|
|
1361
|
+
type: webhook
|
|
1362
|
+
url_env: SLACK_WEBHOOK_URL
|
|
1363
|
+
- id: ops
|
|
1364
|
+
type: webhook
|
|
1365
|
+
url: https://ops.example.com/hooks/orchestrator
|
|
1366
|
+
headers_env:
|
|
1367
|
+
Authorization: OPS_WEBHOOK_AUTH # z. B. "Bearer …" in der Env-Datei
|
|
1368
|
+
on: [recovery_failed] # nur dieses Ereignis
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
Umgebung (z. B. `/etc/default/adispatchd`): `SLACK_WEBHOOK_URL=https://hooks.slack.com/services/…`,
|
|
1372
|
+
`OPS_WEBHOOK_AUTH=Bearer …`. Danach **Neustart** (Env-Änderung) bzw. `config reload` (nur YAML-Änderung).
|
|
1373
|
+
|
|
1374
|
+
---
|
|
1375
|
+
|
|
1376
|
+
## 8. Häufige Validierungsfehler
|
|
1377
|
+
|
|
1378
|
+
Format der Ausgabe von `config validate`: `datei:zeile: pfad: meldung`. Alle folgenden Blöcke sind bewusst
|
|
1379
|
+
ungültig und werden vom Test gegen die angegebene Meldung geprüft.
|
|
1380
|
+
|
|
1381
|
+
**Tippfehler im Schlüssel** – alle Objekte sind strikt.
|
|
1382
|
+
|
|
1383
|
+
```yaml
|
|
1384
|
+
# example: err-unknown-key
|
|
1385
|
+
# extends: minimal
|
|
1386
|
+
# invalid
|
|
1387
|
+
# expect: config.yaml: scheduler: Unknown key(s): "max_concurent"
|
|
1388
|
+
# file: config.yaml
|
|
1389
|
+
scheduler:
|
|
1390
|
+
max_concurent: 2
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
Lösung: Schreibweise laut §3 korrigieren.
|
|
1394
|
+
|
|
1395
|
+
**Falsches Dauerformat** – keine zusammengesetzten Dauern, keine Leerzeichen.
|
|
1396
|
+
|
|
1397
|
+
```yaml
|
|
1398
|
+
# example: err-duration
|
|
1399
|
+
# extends: minimal
|
|
1400
|
+
# invalid
|
|
1401
|
+
# expect: workflows/hello.yaml: execute.timeout.max_duration: Invalid duration "1h30m" (expected e.g. 100ms, 30s, 45m, 6h, 30d)
|
|
1402
|
+
# file: workflows/hello.yaml
|
|
1403
|
+
id: hello
|
|
1404
|
+
trigger: { type: manual }
|
|
1405
|
+
execute:
|
|
1406
|
+
type: shell
|
|
1407
|
+
command: scripts/hello.sh
|
|
1408
|
+
timeout: { max_duration: 1h30m }
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
Lösung: `90m`. Eine unquotierte Zahl (`max_duration: 30`) meldet `Expected a duration such as 30s, 45m, 6h`.
|
|
1412
|
+
|
|
1413
|
+
**Unbekannte Referenz** (Source, Identity) – auch Folgefehler einer kaputten `identities.yaml`.
|
|
1414
|
+
|
|
1415
|
+
```yaml
|
|
1416
|
+
# example: err-unknown-source
|
|
1417
|
+
# extends: github
|
|
1418
|
+
# invalid
|
|
1419
|
+
# expect: workflows/coder.yaml: trigger.source: Unknown source "project_x"
|
|
1420
|
+
# expect: workflows/coder.yaml: identity: Unknown identity "codr"
|
|
1421
|
+
# file: workflows/coder.yaml
|
|
1422
|
+
id: coder
|
|
1423
|
+
identity: codr
|
|
1424
|
+
trigger: { type: source, source: project_x }
|
|
1425
|
+
execute: { type: shell, command: scripts/coder.sh }
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
**Unbekannter oder unpassender Platzhalter** – `{result.*}` gibt es in `on_start` noch nicht.
|
|
1429
|
+
|
|
1430
|
+
```yaml
|
|
1431
|
+
# example: err-placeholder
|
|
1432
|
+
# extends: github
|
|
1433
|
+
# invalid
|
|
1434
|
+
# expect: workflows/coder.yaml: on_start[0].message: Placeholder "{result.message}" is not available in this context
|
|
1435
|
+
# expect: workflows/coder.yaml: on_start[0].message: Unknown placeholder "{item.body}"
|
|
1436
|
+
# file: workflows/coder.yaml
|
|
1437
|
+
id: coder
|
|
1438
|
+
trigger: { type: source, source: project_a }
|
|
1439
|
+
execute: { type: shell, command: scripts/coder.sh }
|
|
1440
|
+
on_start:
|
|
1441
|
+
- type: source.comment
|
|
1442
|
+
message: 'Start: {result.message} – {item.body}'
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
Lösung: nur Platzhalter aus §5 im erlaubten Scope; literale Klammern als `{{`/`}}`.
|
|
1446
|
+
|
|
1447
|
+
**Source-Elemente in Workflows ohne Source-Trigger**
|
|
1448
|
+
|
|
1449
|
+
```yaml
|
|
1450
|
+
# example: err-source-only
|
|
1451
|
+
# extends: minimal
|
|
1452
|
+
# invalid
|
|
1453
|
+
# expect: workflows/nightly.yaml: filter: "filter" is only allowed for workflows with a source trigger (trigger is "schedule")
|
|
1454
|
+
# expect: workflows/nightly.yaml: on_success[0].type: "source.comment" needs a source item; the workflow has no source trigger
|
|
1455
|
+
# file: workflows/nightly.yaml
|
|
1456
|
+
id: nightly
|
|
1457
|
+
trigger: { type: schedule, cron: '0 3 * * *' }
|
|
1458
|
+
filter: { field: state, operator: equals, value: open }
|
|
1459
|
+
execute: { type: shell, command: scripts/nightly.sh }
|
|
1460
|
+
on_success:
|
|
1461
|
+
- type: source.comment
|
|
1462
|
+
message: done
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
Lösung: `filter`, `shell_filter`, `item_priority`, `conditions.item`, `source.update`, `source.comment` nur mit
|
|
1466
|
+
`trigger.type: source`; für Cron/Manual `shell`-Actions verwenden.
|
|
1467
|
+
|
|
1468
|
+
**Filter-Struktur** – `in` braucht eine Liste, `value` ist Pflicht.
|
|
1469
|
+
|
|
1470
|
+
```yaml
|
|
1471
|
+
# example: err-filter
|
|
1472
|
+
# extends: github
|
|
1473
|
+
# invalid
|
|
1474
|
+
# expect: workflows/coder.yaml: filter.all[0].value: Operator "in" requires a non-empty list of strings, numbers or booleans
|
|
1475
|
+
# expect: workflows/coder.yaml: filter.all[1].operator: Predicate requires "operator", one of: equals, not_equals, contains, in, greater_than, less_than
|
|
1476
|
+
# expect: workflows/coder.yaml: filter.all[2].value: Predicate requires a "value" (use null explicitly if intended)
|
|
1477
|
+
# expect: workflows/coder.yaml: filter.all[3].value: Operator "contains" requires a scalar value (string, number, boolean or null)
|
|
1478
|
+
# expect: workflows/coder.yaml: filter.all[4].value: Operator "equals" requires a scalar value (string, number, boolean or null)
|
|
1479
|
+
# file: workflows/coder.yaml
|
|
1480
|
+
id: coder
|
|
1481
|
+
trigger: { type: source, source: project_a }
|
|
1482
|
+
filter:
|
|
1483
|
+
all:
|
|
1484
|
+
- { field: state, operator: in, value: open }
|
|
1485
|
+
- { field: labels, operator: has, value: ready }
|
|
1486
|
+
- { field: author, operator: equals }
|
|
1487
|
+
- { field: labels, operator: contains, value: [a, b] } # Liste nur mit "in"
|
|
1488
|
+
- { field: author, operator: equals, value: [alice, bob] }
|
|
1489
|
+
execute: { type: shell, command: scripts/coder.sh }
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
**Relatives `working_directory` / Wurzel nicht am Anfang**
|
|
1493
|
+
|
|
1494
|
+
```yaml
|
|
1495
|
+
# example: err-working-directory
|
|
1496
|
+
# extends: minimal
|
|
1497
|
+
# invalid
|
|
1498
|
+
# expect: workflows/hello.yaml: execute.working_directory: Must be absolute: start with "/" or "{workspace.base_dir}"
|
|
1499
|
+
# file: workflows/hello.yaml
|
|
1500
|
+
id: hello
|
|
1501
|
+
trigger: { type: manual }
|
|
1502
|
+
execute:
|
|
1503
|
+
type: shell
|
|
1504
|
+
command: scripts/hello.sh
|
|
1505
|
+
working_directory: 'work/{run.id}'
|
|
1506
|
+
```
|
|
1507
|
+
|
|
1508
|
+
Weitere Meldungen: `Placeholder "{workspace.base_dir}" must be followed by "/" or the end of the path template`
|
|
1509
|
+
bzw. `… is a directory root and must start the path template`.
|
|
1510
|
+
|
|
1511
|
+
**Parameter im Pfad bzw. in automatischen Workflows**
|
|
1512
|
+
|
|
1513
|
+
```yaml
|
|
1514
|
+
# example: err-parameters
|
|
1515
|
+
# extends: minimal
|
|
1516
|
+
# invalid
|
|
1517
|
+
# expect: workflows/hello.yaml: execute.working_directory: Parameter "name" is optional without default and cannot be used in a path
|
|
1518
|
+
# expect: workflows/hello.yaml: execute.args[0]: Parameter "nme" is not declared
|
|
1519
|
+
# file: workflows/hello.yaml
|
|
1520
|
+
id: hello
|
|
1521
|
+
trigger: { type: manual }
|
|
1522
|
+
parameters:
|
|
1523
|
+
name: { type: string }
|
|
1524
|
+
execute:
|
|
1525
|
+
type: shell
|
|
1526
|
+
command: scripts/hello.sh
|
|
1527
|
+
working_directory: '{workspace.base_dir}/{parameters.name}'
|
|
1528
|
+
args: ['{parameters.nme}']
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
```yaml
|
|
1532
|
+
# example: err-required-parameter
|
|
1533
|
+
# extends: minimal
|
|
1534
|
+
# invalid
|
|
1535
|
+
# expect: workflows/nightly.yaml: parameters.target.required: Required parameter without default in an automatically triggered workflow: automatic runs could never supply it
|
|
1536
|
+
# file: workflows/nightly.yaml
|
|
1537
|
+
id: nightly
|
|
1538
|
+
trigger: { type: schedule, cron: '0 3 * * *' }
|
|
1539
|
+
parameters:
|
|
1540
|
+
target: { type: string, required: true }
|
|
1541
|
+
execute: { type: shell, command: scripts/nightly.sh }
|
|
1542
|
+
```
|
|
1543
|
+
|
|
1544
|
+
**Reservierte oder doppelte Env-Namen**
|
|
1545
|
+
|
|
1546
|
+
```yaml
|
|
1547
|
+
# example: err-env
|
|
1548
|
+
# extends: github
|
|
1549
|
+
# invalid
|
|
1550
|
+
# expect: workflows/coder.yaml: execute.env.ADISPATCH_MODE: Environment variables with prefix "ADISPATCH_" are reserved
|
|
1551
|
+
# expect: workflows/coder.yaml: execute.secrets.GH_TOKEN: "GH_TOKEN" is defined both in "env" and "secrets"
|
|
1552
|
+
# file: workflows/coder.yaml
|
|
1553
|
+
id: coder
|
|
1554
|
+
trigger: { type: source, source: project_a }
|
|
1555
|
+
execute:
|
|
1556
|
+
type: shell
|
|
1557
|
+
command: scripts/coder.sh
|
|
1558
|
+
env: { ADISPATCH_MODE: fast, GH_TOKEN: '' }
|
|
1559
|
+
secrets: { GH_TOKEN: { identity: coder } }
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
**Cron und Zeitzone**
|
|
1563
|
+
|
|
1564
|
+
```yaml
|
|
1565
|
+
# example: err-cron
|
|
1566
|
+
# extends: minimal
|
|
1567
|
+
# invalid
|
|
1568
|
+
# expect: workflows/nightly.yaml: trigger.cron: Invalid cron expression "0 25 * * *"
|
|
1569
|
+
# expect: workflows/nightly.yaml: trigger.timezone: Invalid time zone "Europe/Berln"
|
|
1570
|
+
# file: workflows/nightly.yaml
|
|
1571
|
+
id: nightly
|
|
1572
|
+
trigger: { type: schedule, cron: '0 25 * * *', timezone: Europe/Berln }
|
|
1573
|
+
execute: { type: shell, command: scripts/nightly.sh }
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
Ein Ausdruck, der nie feuert, meldet `Cron expression "…" never fires`.
|
|
1577
|
+
|
|
1578
|
+
**Webhook-Kanal**
|
|
1579
|
+
|
|
1580
|
+
```yaml
|
|
1581
|
+
# example: err-webhook
|
|
1582
|
+
# extends: minimal
|
|
1583
|
+
# invalid
|
|
1584
|
+
# expect: config.yaml: notifications.channels[0]: Webhook channel needs exactly one of "url" or "url_env"
|
|
1585
|
+
# expect: config.yaml: notifications.channels[1].url: Webhook "url" must be an absolute http(s) URL
|
|
1586
|
+
# file: config.yaml
|
|
1587
|
+
notifications:
|
|
1588
|
+
channels:
|
|
1589
|
+
- { type: webhook, url: 'https://hooks.example.com/x', url_env: HOOK_URL }
|
|
1590
|
+
- { type: webhook, url: 'hooks.example.com/x' }
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
**Labels und Action-IDs**
|
|
1594
|
+
|
|
1595
|
+
```yaml
|
|
1596
|
+
# example: err-actions
|
|
1597
|
+
# extends: github
|
|
1598
|
+
# invalid
|
|
1599
|
+
# expect: workflows/coder.yaml: on_start[0].labels: Label "coding" is both added and removed
|
|
1600
|
+
# expect: workflows/coder.yaml: on_success[0].labels: "source.update" must add or remove at least one label
|
|
1601
|
+
# expect: workflows/coder.yaml: on_success[1].id: Duplicate action ID "notify" (also used at on_start[1])
|
|
1602
|
+
# file: workflows/coder.yaml
|
|
1603
|
+
id: coder
|
|
1604
|
+
trigger: { type: source, source: project_a }
|
|
1605
|
+
execute: { type: shell, command: scripts/coder.sh }
|
|
1606
|
+
on_start:
|
|
1607
|
+
- type: source.update
|
|
1608
|
+
labels: { add: [coding], remove: [coding] }
|
|
1609
|
+
- { id: notify, type: shell, command: scripts/notify.sh }
|
|
1610
|
+
on_success:
|
|
1611
|
+
- type: source.update
|
|
1612
|
+
labels: { add: [] }
|
|
1613
|
+
- { id: notify, type: shell, command: scripts/notify.sh }
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
**Views ohne Shell, gleiche Quellen eindeutig**
|
|
1617
|
+
|
|
1618
|
+
```yaml
|
|
1619
|
+
# example: err-view
|
|
1620
|
+
# extends: github
|
|
1621
|
+
# invalid
|
|
1622
|
+
# expect: views/needs-maintainer.yaml: shell_filter: Views do not support "shell_filter"; views never run shell scripts (Spec §17)
|
|
1623
|
+
# expect: views/needs-maintainer.yaml: sources: Sources of a view must be unique
|
|
1624
|
+
# file: views/needs-maintainer.yaml
|
|
1625
|
+
id: needs-maintainer
|
|
1626
|
+
sources: [project_a, project_a]
|
|
1627
|
+
shell_filter: { command: scripts/filter.sh }
|
|
1628
|
+
fields: [ { field: title } ]
|
|
1629
|
+
```
|
|
1630
|
+
|
|
1631
|
+
**Ungültige ID und YAML-Typen**
|
|
1632
|
+
|
|
1633
|
+
```yaml
|
|
1634
|
+
# example: err-types
|
|
1635
|
+
# extends: minimal
|
|
1636
|
+
# invalid
|
|
1637
|
+
# expect: workflows/hello.yaml: id: Invalid ID (allowed: letters, digits, "_" and "-", must start with a letter or digit, max. 64)
|
|
1638
|
+
# expect: workflows/hello.yaml: enabled: Invalid input: expected boolean, received string
|
|
1639
|
+
# expect: workflows/hello.yaml: trigger.type: Invalid discriminator value. Expected 'source' | 'schedule' | 'manual'
|
|
1640
|
+
# file: workflows/hello.yaml
|
|
1641
|
+
id: hello world
|
|
1642
|
+
enabled: yes # YAML 1.2: String, kein Boolean
|
|
1643
|
+
trigger: { type: cron }
|
|
1644
|
+
execute: { type: shell, command: scripts/hello.sh }
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
**Unbekannte Felder, unpassende Operatoren und Werttypen** – Prüfung gegen den Feldkatalog (§4.7, §4.8).
|
|
1648
|
+
|
|
1649
|
+
```yaml
|
|
1650
|
+
# example: err-fields
|
|
1651
|
+
# extends: github
|
|
1652
|
+
# invalid
|
|
1653
|
+
# expect: workflows/coder.yaml: filter.all[0].field: Unknown field "labls" for source type github (did you mean "labels"?)
|
|
1654
|
+
# expect: workflows/coder.yaml: filter.all[1].operator: Operator "greater_than" is not supported for field "labels" (type string[]); allowed: contains, in
|
|
1655
|
+
# expect: workflows/coder.yaml: filter.all[2].value: Value for field "is_pull_request" (type boolean) must be a boolean, got string "false"
|
|
1656
|
+
# expect: workflows/coder.yaml: filter.all[3].value: Value "last week" for date field "updated_at" is not an ISO-8601 date
|
|
1657
|
+
# expect: workflows/coder.yaml: filter.all[4].field: Field "run.last_status" is only available in views, not in workflows
|
|
1658
|
+
# file: workflows/coder.yaml
|
|
1659
|
+
id: coder
|
|
1660
|
+
trigger: { type: source, source: project_a }
|
|
1661
|
+
filter:
|
|
1662
|
+
all:
|
|
1663
|
+
- { field: labls, operator: contains, value: ready-for-coding }
|
|
1664
|
+
- { field: labels, operator: greater_than, value: 2 }
|
|
1665
|
+
- { field: is_pull_request, operator: equals, value: 'false' }
|
|
1666
|
+
- { field: updated_at, operator: greater_than, value: last week }
|
|
1667
|
+
- { field: run.last_status, operator: equals, value: completed }
|
|
1668
|
+
execute: { type: shell, command: scripts/coder.sh }
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
Lösung: Feldnamen aus §4.7 (Views zusätzlich §4.8), Operatoren laut §4.2, Booleans unquotiert, Datumswerte ISO.
|
|
1672
|
+
In Views werden auch `fields[].field`, `fields[].link` und `sort.field` so geprüft.
|
|
1673
|
+
|
|
1674
|
+
**`state` an wirkungsloser Stelle** – die Source liest ohne `state`-Selektor auf oberster Ebene nur offene Items.
|
|
1675
|
+
|
|
1676
|
+
```yaml
|
|
1677
|
+
# example: err-state
|
|
1678
|
+
# extends: github
|
|
1679
|
+
# invalid
|
|
1680
|
+
# expect: workflows/coder.yaml: filter.any[0].field: "state" only works at the top level of the filter (directly or inside top-level "all"): the source lists only open issues/PRs unless the top-level filter selects others (state equals/in open|closed), so under "any"/"not" it would silently never see them
|
|
1681
|
+
# expect: views/needs-maintainer.yaml: filter.all[0].operator: "state" must use "equals" or "in": only these select which items are listed (otherwise only open issues/PRs are seen)
|
|
1682
|
+
# file: workflows/coder.yaml
|
|
1683
|
+
id: coder
|
|
1684
|
+
trigger: { type: source, source: project_a }
|
|
1685
|
+
filter:
|
|
1686
|
+
any:
|
|
1687
|
+
- { field: state, operator: equals, value: closed }
|
|
1688
|
+
- { field: labels, operator: contains, value: reopen }
|
|
1689
|
+
execute: { type: shell, command: scripts/coder.sh }
|
|
1690
|
+
```
|
|
1691
|
+
|
|
1692
|
+
```yaml
|
|
1693
|
+
# example: err-state
|
|
1694
|
+
# file: views/needs-maintainer.yaml
|
|
1695
|
+
id: needs-maintainer
|
|
1696
|
+
sources: [project_a, project_b]
|
|
1697
|
+
filter:
|
|
1698
|
+
all:
|
|
1699
|
+
- { field: state, operator: not_equals, value: open }
|
|
1700
|
+
fields: [ { field: title } ]
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
Lösung: `state` als oberstes Prädikat, z. B. `all: [{ field: state, operator: in, value: [open, closed] }, { any: [...] }]`.
|
|
1704
|
+
|
|
1705
|
+
**Null-Intervalle und -Timeouts**
|
|
1706
|
+
|
|
1707
|
+
```yaml
|
|
1708
|
+
# example: err-zero-durations
|
|
1709
|
+
# extends: minimal
|
|
1710
|
+
# invalid
|
|
1711
|
+
# expect: config.yaml: scheduler.polling_interval: Duration must be at least 1s
|
|
1712
|
+
# expect: config.yaml: retention.cleanup_interval: Duration must be at least 1s
|
|
1713
|
+
# file: config.yaml
|
|
1714
|
+
scheduler:
|
|
1715
|
+
polling_interval: 0s
|
|
1716
|
+
retention:
|
|
1717
|
+
cleanup_interval: 500ms
|
|
1718
|
+
lifecycle:
|
|
1719
|
+
action_retry: { delay: 0s } # erlaubt: sofort wiederholen
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
`timeout: 0s` bei Conditions, `shell_filter` oder `shell`-Actions meldet `Duration must be greater than 0`.
|
|
1723
|
+
|
|
1724
|
+
**Warnungen** (blockieren nicht):
|
|
1725
|
+
|
|
1726
|
+
```yaml
|
|
1727
|
+
# example: warn-common
|
|
1728
|
+
# extends: minimal
|
|
1729
|
+
# expect-warning: workflows/hello.yaml: id: Workflow ID "greeter" differs from file name "hello.yaml"
|
|
1730
|
+
# expect-warning: workflows/hello.yaml: execute.env.API_TOKEN: Looks like a secret stored literally in YAML
|
|
1731
|
+
# expect-warning: workflows/hello.yaml: execute.timeout.idle_timeout: "idle_timeout" is longer than "max_duration"
|
|
1732
|
+
# file: workflows/hello.yaml
|
|
1733
|
+
id: greeter
|
|
1734
|
+
trigger: { type: manual }
|
|
1735
|
+
execute:
|
|
1736
|
+
type: shell
|
|
1737
|
+
command: scripts/hello.sh
|
|
1738
|
+
env: { API_TOKEN: abc123 }
|
|
1739
|
+
timeout: { max_duration: 1m, idle_timeout: 5m }
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
Weitere häufige Meldungen (ohne eigenes Beispiel):
|
|
1743
|
+
|
|
1744
|
+
| Meldung | Ursache / Lösung |
|
|
1745
|
+
|---|---|
|
|
1746
|
+
| `YAML syntax error: Map keys must be unique` | Schlüssel doppelt im selben Mapping |
|
|
1747
|
+
| `YAML syntax error: …` | Einrückung, nicht geschlossene `{`/`[`, Tabs |
|
|
1748
|
+
| `Top level must be a mapping` | Datei beginnt mit einer Liste (`- …`) |
|
|
1749
|
+
| `Invalid input: expected object, received undefined` (Pfad `execute`/`trigger`) | Pflichtblock fehlt |
|
|
1750
|
+
| `Invalid discriminator value. Expected 'github'` | falscher `type` in Source/Identity/Kanal |
|
|
1751
|
+
| `Duplicate workflow ID "x" (already defined in workflows/a.yaml)` | zwei Dateien mit gleicher `id` |
|
|
1752
|
+
| `Default does not match parameter type: expected …` | `default` passt nicht zu `type`/`values` |
|
|
1753
|
+
| `"cache" is only supported for workflow conditions` | `cache` unter `conditions.item` |
|
|
1754
|
+
| `Duration must be greater than 0` | `max_duration`/`idle_timeout: 0s` |
|
|
1755
|
+
| `Invalid host name (expected "name" or "name:port")` | Schema/Pfad in `allowed_hosts` (`https://…`) |
|
|
1756
|
+
| `Invalid environment variable name` | z. B. `private_key_env: my-key` |
|
|
1757
|
+
| `Expected a size such as 100MB` | Größe als Zahl statt String (`max_log_size_per_run: 100`) |
|
|
1758
|
+
| `Invalid size "100" (expected e.g. 512KB, 100MB, 1GB)` | String ohne oder mit unbekannter Einheit (`'100'`, `'100MiBs'`) |
|
|
1759
|
+
| `config.yaml not found, using defaults` (Warnung) | `config.yaml` fehlt |
|
|
1760
|
+
|
|
1761
|
+
Laufzeitfehler, die `config validate` **nicht** findet: fehlende Umgebungsvariablen (Keys, Webhooks),
|
|
1762
|
+
nicht existierende/nicht ausführbare Skripte, fehlende App-Rechte (→ Felder unknown bzw. Actions scheitern). Dafür: `workflow dry-run` und die Run-Details.
|
|
1763
|
+
|
|
1764
|
+
---
|
|
1765
|
+
|
|
1766
|
+
## 9. Checkliste „neuen Workflow anlegen“
|
|
1767
|
+
|
|
1768
|
+
1. **Trigger wählen:** `source` (Items), `schedule` (Cron) oder `manual`.
|
|
1769
|
+
2. **Voraussetzungen:** Source in `sources.yaml`, Identities in `identities.yaml`, Private Keys als
|
|
1770
|
+
Umgebungsvariablen des Dienstes (Env-Datei, danach Neustart), App-Rechte prüfen.
|
|
1771
|
+
3. **Datei** `workflows/<id>.yaml` anlegen (Dateiname = `id`), am besten mit `enabled: false` beginnen.
|
|
1772
|
+
4. **Filter** nur mit Feldern aus §4.7; Labels exakt schreiben; für geschlossene Items ein `state`-Prädikat auf
|
|
1773
|
+
oberster Ebene; teure `dependencies.*` nur wenn nötig.
|
|
1774
|
+
5. **execute:** Skript ausführbar machen (`chmod +x`), Pfad relativ zum config-dir oder absolut; Argumente als
|
|
1775
|
+
Liste; Timeouts setzen; Secrets nur über `secrets`.
|
|
1776
|
+
6. **Hooks:** Label-Übergänge für jeden Ausgang (`on_start`, `on_success`, `on_failure`, `on_crash`,
|
|
1777
|
+
`on_start_error`, `on_timeout`, `on_cancel`) so wählen, dass das Item nicht hängen bleibt und nicht sofort wieder
|
|
1778
|
+
matcht (Retry-Blocks fangen Endlosschleifen technischer Fehler ab).
|
|
1779
|
+
7. **Validieren:** `adispatch config validate --config-dir <dir>` → Exit 0, Warnungen lesen.
|
|
1780
|
+
8. **Trockenlauf:** `adispatch workflow dry-run <id> --config-dir <dir>` – Kandidaten, Filter-Trace
|
|
1781
|
+
(kein `unknown` durch Tippfehler?), Prioritäten, Locks/Blocks, Kapazität.
|
|
1782
|
+
9. **Aktivieren:** `enabled: true` setzen und `adispatch config reload --config-dir <dir>` (bei Fehlern
|
|
1783
|
+
bleibt die alte Konfiguration aktiv). Alternativ zuerst ein manueller Start über die UI.
|
|
1784
|
+
10. **Beobachten:** erster Run in der UI (Status, Logs, Lifecycle-Journal), ggf. `max_concurrent` per Override
|
|
1785
|
+
anpassen und danach in YAML übernehmen (Overrides gehen beim Neustart verloren).
|