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,1308 @@
|
|
|
1
|
+
# Manuelle Integrationstests gegen echtes GitHub (Spec §22.1 „GitHub“, §22.2, §24.1)
|
|
2
|
+
|
|
3
|
+
Automatisierte Tests laufen nur gegen `FakeGitHub` (`src/sources/github/testing/`). Diese Anleitung prüft dieselben
|
|
4
|
+
Annahmen Schritt für Schritt gegen die echte GitHub-API, mit eigenen Test-Apps und Wegwerf-Repos. Sie ist so geschrieben,
|
|
5
|
+
dass die Befehle der Reihe nach kopiert werden können. Zeitbedarf: Einrichtung etwa 45 Minuten, Testfälle etwa 2 Stunden.
|
|
6
|
+
Der natürliche Token-Ablauf (T2b) braucht mindestens 60 Minuten Laufzeit am Stück. Die anderen Testfälle laufen in
|
|
7
|
+
dieser Zeit nebenher.
|
|
8
|
+
|
|
9
|
+
Inhalt:
|
|
10
|
+
[0 Annahmen und Doku-Stand](#0-annahmen-und-doku-stand) ·
|
|
11
|
+
[1 Voraussetzungen und Testaufbau](#1-voraussetzungen-und-testaufbau) ·
|
|
12
|
+
[2 Testkonfiguration](#2-testkonfiguration) ·
|
|
13
|
+
[3 Testdaten](#3-testdaten) ·
|
|
14
|
+
[4 Testfälle](#4-testfälle) ·
|
|
15
|
+
[5 Auswertung und Aufräumen](#5-auswertung-und-aufräumen)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 0 Annahmen und Doku-Stand
|
|
20
|
+
|
|
21
|
+
### 0.1 Annahmen im Code (A1–A11)
|
|
22
|
+
|
|
23
|
+
| # | Annahme (im Code umgesetzt) | Stelle | Doku-Stand 2026-10-17 | Testfall |
|
|
24
|
+
|---|---|---|---|---|
|
|
25
|
+
| A1 | `X-GitHub-Api-Version: 2026-03-10` wird akzeptiert und enthält die Dependencies-API. | `GITHUB_API_VERSION` in `src/sources/github/http.ts` | **bestätigt**: neueste unterstützte Version ([R1](#r1)) | T1, T11 |
|
|
26
|
+
| A2 | `GET /repos/{o}/{r}/issues/{n}/dependencies/blocked_by` liefert ein Array von Issue-Objekten mit `state` und `repository_url`, paginiert über `Link` (`per_page` ≤ 100), Status 200/301/404/410. | `resolveDependencies` in `adapter.ts` | **bestätigt** ([R2](#r2)) | T11 |
|
|
27
|
+
| A3 | Cross-Repo-Blocker erscheinen in der Liste, **sofern** die App Zugriff auf das Blocker-Repo hat. | dito | nicht dokumentiert | T12 |
|
|
28
|
+
| A4 | Blocker in Repos ohne App-Zugriff fehlen in der Liste (oder führen zu 403/404). `issue_dependencies_summary.total_blocked_by` des frisch gelesenen Issues zählt sie mit. Ist der Wert größer als die sichtbare Liste oder fehlt das Summary → `dependencies.status = unknown` (fail-closed). | `resolveDependencies` | nicht dokumentiert ([R3](#r3), [R11](#r11)) | T13 |
|
|
29
|
+
| A5 | `issue_dependencies_summary.blocked_by` = Anzahl **offener** Blocker, `total_blocked_by` = alle. Genutzt wird nur `total_blocked_by`. | dito | Felder dokumentiert, Bedeutung **nicht** ([R3](#r3)) | T11, T14 |
|
|
30
|
+
| A6 | Fehlende Rechte am Dependencies-Endpunkt → 403 (`Resource not accessible by integration`) oder 404. Beides → `unknown`. | dito | Endpunkt verlangt *Issues: read* ([R2](#r2)) | T13, T19 |
|
|
31
|
+
| A7 | Benötigte App-Rechte: *Issues: read* (Lesen inkl. Dependencies), *Issues: write* (Labels/Kommentare an Issues), *Pull requests: write* (Labels/Kommentare an PRs über die Issues-API), *Metadata: read*. | Doku (`README.md`, `init`-Vorlage) | **teilweise anders**: Labels/Kommentare akzeptieren *Issues: write* **oder** *Pull requests: write*; die Dependencies-Endpunkte verlangen *Issues* read/write ([R4](#r4)) | T19 |
|
|
32
|
+
| A8 | Primäres Rate-Limit: 403/429 mit `x-ratelimit-remaining: 0` + `x-ratelimit-reset`. Sekundäres: 403/429 mit `retry-after` bzw. Meldung „rate limit“. | `GitHubHttpClient.toError` | **bestätigt** ([R8](#r8)) | T22 |
|
|
33
|
+
| A9 | Installation-Tokens laufen nach 1 h ab (`expires_at`). Ein JWT mit `iat = now-60 s`, `exp = now+9 min`, `iss = App-ID` wird akzeptiert. | `src/identities/` | **bestätigt**. Hinweis: GitHub *empfiehlt* inzwischen die Client-ID als `iss`, die App-ID bleibt zulässig ([R6](#r6)) | T1, T2 |
|
|
34
|
+
| A10 | Transferierte Issues leiten per 301 auf das neue Repo um. `repository_url` passt dann nicht → `getItem` liefert `null`. | `fetchIssue` | **bestätigt** (301, gelöscht 410, kein Zugriff 404) ([R9](#r9)) | T23 |
|
|
35
|
+
| A11 | Von einer App per Installation-Token erstellte Kommentare tragen `performed_via_github_app.id = <App-ID>` und `user.type = "Bot"`. Sonst findet `findCommentByMarker` eigene Kommentare nicht (Folge: Duplikat statt Unterdrückung). | `authorPredicate` in `adapter.ts` | Feld im Schema dokumentiert, `Bot`/`[bot]` **nicht** ([R7](#r7)) | T6, T7 |
|
|
36
|
+
|
|
37
|
+
### 0.2 Rechercheergebnisse (offizielle GitHub-Doku, abgerufen 2026-10-17)
|
|
38
|
+
|
|
39
|
+
<a id="r1"></a>**R1 – API-Version.** Unterstützt sind `2026-03-10` (Supportende „Not yet scheduled“) und `2022-11-28`
|
|
40
|
+
(Supportende 10.03.2028). Ohne Header gilt `2022-11-28`, eine nicht mehr unterstützte Version gibt `410`.
|
|
41
|
+
Breaking Changes in `2026-03-10`, die den Orchestrator betreffen könnten:
|
|
42
|
+
- Das Feld `assignee` (Einzahl) ist aus Issues/PRs entfernt. Der Code liest nur `assignees` und ist nicht betroffen.
|
|
43
|
+
- `GET /rate_limit` hat kein `rate` mehr, stattdessen `resources.core`. Der Code nutzt den Endpunkt nicht, nur T22.
|
|
44
|
+
- `DELETE /app/installations/{id}` gibt 202. Der Code nutzt den Endpunkt nicht.
|
|
45
|
+
- Labels und Issue-Kommentare sind unverändert.
|
|
46
|
+
|
|
47
|
+
Quellen: [API Versions](https://docs.github.com/en/rest/about-the-rest-api/api-versions),
|
|
48
|
+
[Breaking changes](https://docs.github.com/en/rest/about-the-rest-api/breaking-changes).
|
|
49
|
+
|
|
50
|
+
<a id="r2"></a>**R2 – Issue-Dependencies-API.** Quelle: [Issue dependencies](https://docs.github.com/en/rest/issues/issue-dependencies),
|
|
51
|
+
[Creating issue dependencies](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/creating-issue-dependencies).
|
|
52
|
+
|
|
53
|
+
| Operation | Methode, Pfad | Status | Recht (Fine-grained/App) |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| Blocker auflisten | `GET /repos/{o}/{r}/issues/{issue_number}/dependencies/blocked_by` (`per_page` ≤ 100, `page`) | 200, 301, 404, 410 | *Issues: read* |
|
|
56
|
+
| Blocker hinzufügen | `POST …/dependencies/blocked_by`, Body `{"issue_id": <int>}` | 201, 301, 403, 404, 410, 422 | *Issues: write* |
|
|
57
|
+
| Blocker entfernen | `DELETE …/dependencies/blocked_by/{issue_id}` | **200** (nicht 204), 301, 400, 401, 403, 404, 410 | *Issues: write* |
|
|
58
|
+
| Blockierte auflisten | `GET …/dependencies/blocking` | 200, 301, 404, 410 | *Issues: read* |
|
|
59
|
+
|
|
60
|
+
- `issue_id` ist die **Datenbank-ID** des blockierenden Issues (`.id`), **nicht** die Nummer.
|
|
61
|
+
- Die Antwort ist ein Array vollständiger Issue-Objekte mit `repository_url` (Pflicht) und optional `repository`.
|
|
62
|
+
- *Pull requests* ist bei keinem der vier Endpunkte als alternatives Recht genannt.
|
|
63
|
+
- `gh` kann Abhängigkeiten selbst setzen (`gh issue edit N --add-blocked-by <nr|url>`, `--remove-blocked-by`;
|
|
64
|
+
`gh issue create --blocked-by`). Das wurde mit gh 2.101.0 geprüft.
|
|
65
|
+
- Laut [Changelog 2025-08-21](https://github.blog/changelog/2025-08-21-dependencies-on-issues) sind bis zu 50 Issues je
|
|
66
|
+
Beziehungstyp möglich.
|
|
67
|
+
|
|
68
|
+
<a id="r3"></a>**R3 – `issue_dependencies_summary`.** Das Feld ist Teil des Issue-Schemas (Get/Create/List) mit den
|
|
69
|
+
Pflicht-Integern `blocked_by`, `blocking`, `total_blocked_by` und `total_blocking`. Die Bedeutung ist **nicht
|
|
70
|
+
dokumentiert**. Dass `blocked_by` die offenen und `total_*` alle Blocker zählt, ist eine Annahme (A5, T11/T14).
|
|
71
|
+
Ebenfalls nicht dokumentiert ist, ob unsichtbare Blocker mitzählen (A4, T13).
|
|
72
|
+
|
|
73
|
+
<a id="r4"></a>**R4 – Rechte je Endpunkt** (Kästen „Fine-grained access tokens“, Übersicht
|
|
74
|
+
[Permissions required for GitHub Apps](https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps)):
|
|
75
|
+
- **Nur *Issues: read*:**
|
|
76
|
+
- `GET /repos/{o}/{r}/issues`. Die Liste enthält auch PRs, erkennbar an `pull_request`.
|
|
77
|
+
- `GET …/issues/{n}`.
|
|
78
|
+
- ***Issues: write* oder *Pull requests: write*** (eines von beiden genügt):
|
|
79
|
+
- `POST …/issues/{n}/labels`
|
|
80
|
+
- `DELETE …/labels/{name}`
|
|
81
|
+
- `POST …/issues/{n}/comments`
|
|
82
|
+
- `POST /repos/{o}/{r}/labels`
|
|
83
|
+
- ***Issues: read* oder *Pull requests: read*:** `GET …/issues/{n}/comments`.
|
|
84
|
+
- **Metadata:** *Metadata: read* setzt GitHub automatisch.
|
|
85
|
+
|
|
86
|
+
Die Doku sagt **nicht**, welches der beiden Rechte bei einem PR greift. Deshalb behält diese Anleitung für PR-schreibende
|
|
87
|
+
Apps vorsichtshalber beide Rechte und prüft die Frage in T19d. Die fehlende Berechtigung nennt der Antwort-Header
|
|
88
|
+
`X-Accepted-GitHub-Permissions`. Quellen: [Issues](https://docs.github.com/en/rest/issues/issues),
|
|
89
|
+
[Labels](https://docs.github.com/en/rest/issues/labels), [Comments](https://docs.github.com/en/rest/issues/comments).
|
|
90
|
+
|
|
91
|
+
<a id="r5"></a>**R5 – App anlegen und installieren.**
|
|
92
|
+
- **Anlegen:** [Registering a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)
|
|
93
|
+
(Webhook „Active“ abwählen, „Only on this account“).
|
|
94
|
+
- **Schlüssel:** [Managing private keys](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps).
|
|
95
|
+
Der PEM ist im Format PKCS#1 (`BEGIN RSA PRIVATE KEY`), läuft nicht ab, und je App sind bis zu 25 Schlüssel möglich.
|
|
96
|
+
- **Installieren:** [Installing your own GitHub App](https://docs.github.com/en/apps/using-github-apps/installing-your-own-github-app),
|
|
97
|
+
mit „Only select repositories“.
|
|
98
|
+
- **Installation-ID:** laut [Generating an installation access token](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app)
|
|
99
|
+
über das JWT mit `GET /repos/{o}/{r}/installation`, `GET /users/{u}/installation`, `GET /orgs/{org}/installation` oder
|
|
100
|
+
`GET /app/installations`. Die URL `…/settings/installations/<ID>` funktioniert, ist aber nicht offiziell dokumentiert.
|
|
101
|
+
|
|
102
|
+
<a id="r6"></a>**R6 – Installation-Token und JWT.**
|
|
103
|
+
- **Token:** `POST /app/installations/{id}/access_tokens` → `201 {token, expires_at, permissions, repository_selection}`.
|
|
104
|
+
Das Token läuft nach einer Stunde ab, danach kommt `401`. Mit `repositories`/`permissions` im Body lässt sich ein Token
|
|
105
|
+
weiter einschränken, aber nie erweitern.
|
|
106
|
+
- **Neues Token-Format:** Seit dem 27.04.2026 stellt GitHub auf das zustandslose Format `ghs_<APPID>_<JWT>` um (länger als
|
|
107
|
+
40 Zeichen). Der Code setzt keine Länge voraus.
|
|
108
|
+
- **JWT:**
|
|
109
|
+
- Algorithmus RS256.
|
|
110
|
+
- `iat` 60 s in der Vergangenheit empfohlen.
|
|
111
|
+
- `exp` höchstens 10 min in der Zukunft.
|
|
112
|
+
- `iss` darf Client-ID **oder** App-ID sein; die Client-ID wird empfohlen.
|
|
113
|
+
|
|
114
|
+
Quellen: [Create an installation access token](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app),
|
|
115
|
+
[Generating a JWT](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app).
|
|
116
|
+
|
|
117
|
+
<a id="r7"></a>**R7 – Kommentare einer App.** Das Kommentar-Schema enthält `performed_via_github_app` (`null` oder ein
|
|
118
|
+
App-Objekt mit `id`, `slug`, `client_id` …) und `user.type`. Laut Doku werden Requests einer Installation „der App
|
|
119
|
+
zugeschrieben“
|
|
120
|
+
([Authenticating as an installation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation)).
|
|
121
|
+
`user.type = "Bot"` und der Login `<slug>[bot]` sind **nicht dokumentiert** und werden in T6 gemessen.
|
|
122
|
+
|
|
123
|
+
<a id="r8"></a>**R8 – Rate-Limits** ([Rate limits for the REST API](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api),
|
|
124
|
+
[Best practices](https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api)).
|
|
125
|
+
- **Primäres Limit je Installation:** mindestens 5 000/h. Mit mehr Repos/Nutzern skaliert es bis 12 500/h, unter
|
|
126
|
+
Enterprise Cloud sind es 15 000/h.
|
|
127
|
+
- **Header:** `x-ratelimit-limit|remaining|used|reset|resource`. `GET /rate_limit` zählt nicht zum primären Limit.
|
|
128
|
+
- **Sekundäre Limits:**
|
|
129
|
+
- 100 parallele Requests.
|
|
130
|
+
- 900 Punkte/min je Endpunkt (GET = 1 Punkt, Schreiben = 5).
|
|
131
|
+
- 80 content-erzeugende Requests/min bzw. 500/h.
|
|
132
|
+
- **Antwort bei Überschreitung:** `403` **oder** `429`. Mit `retry-after` gilt dessen Wartezeit, bei `remaining: 0` die
|
|
133
|
+
bis `reset`, sonst mindestens 1 min mit Backoff.
|
|
134
|
+
- **Empfehlung:** Schreibende Requests seriell senden, mit mindestens 1 s Abstand. Der Code drosselt das nicht, siehe
|
|
135
|
+
[5.2](#52-bei-abweichungen).
|
|
136
|
+
|
|
137
|
+
<a id="r9"></a>**R9 – Transfer/Löschung** ([Get an issue](https://docs.github.com/en/rest/issues/issues#get-an-issue)):
|
|
138
|
+
- Transferiert in ein lesbares Repo → `301`.
|
|
139
|
+
- Transferiert in ein nicht lesbares Repo oder dort gelöscht → `404`.
|
|
140
|
+
- In einem lesbaren Repo gelöscht → `410`.
|
|
141
|
+
|
|
142
|
+
<a id="r11"></a>**R11 – Cross-Repo-Abhängigkeiten.** Es gibt keine dokumentierte Einschränkung (gleicher Owner o. Ä.).
|
|
143
|
+
Was ein Token sieht, wenn ihm das Blocker-Repo fehlt, ist nicht dokumentiert. Das prüft T13.
|
|
144
|
+
|
|
145
|
+
**Abweichungen zwischen Code-Annahmen und Doku:**
|
|
146
|
+
- Es gibt **keine** falsche Konstante und keinen falschen Endpunkt oder Feldnamen. Code wurde nicht geändert.
|
|
147
|
+
- **A7:** Die Aussage „Pull requests: write ist für Labels/Kommentare an PRs nötig“ deckt die Doku nicht. Laut Doku genügt
|
|
148
|
+
eines der beiden Rechte. Prüfen in T19d und danach die Rechte-Hinweise in `README.md` und `src/cli/init-templates.ts`
|
|
149
|
+
anpassen.
|
|
150
|
+
- **A7 (Dependencies):** Die Dependencies-Endpunkte nennen jetzt explizit *Issues: read/write*. Die alte Aussage „Doku
|
|
151
|
+
nennt keine Rechte“ ist überholt.
|
|
152
|
+
- **A9:** `iss` = App-ID ist weiterhin zulässig. Empfohlen ist die Client-ID; ein Umstieg ist optional (Konfigfeld fehlt).
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 1 Voraussetzungen und Testaufbau
|
|
157
|
+
|
|
158
|
+
### 1.1 Werkzeuge
|
|
159
|
+
|
|
160
|
+
- Repo-Checkout mit `npm ci`, Node ≥ 22.13. Alle Befehle laufen im **Repo-Root**.
|
|
161
|
+
- `gh` ≥ 2.80 (getestet mit 2.101.0), angemeldet als Mensch mit Admin-Rechten auf die Test-Repos (`gh auth status`).
|
|
162
|
+
Zum Aufräumen zusätzlich `gh auth refresh -s delete_repo`.
|
|
163
|
+
- `curl`, `jq`, `openssl`, eine bash- oder zsh-Shell.
|
|
164
|
+
- Ein **eigener** GitHub-Account oder eine Test-Org. Keine produktiven Repos und keine produktiven Apps verwenden.
|
|
165
|
+
|
|
166
|
+
### 1.2 Repos
|
|
167
|
+
|
|
168
|
+
| Repo | Sichtbarkeit | Apps installiert | Zweck |
|
|
169
|
+
|---|---|---|---|
|
|
170
|
+
| `orch-test-a` | privat | maintainer, coder, reviewer | Source `test_a`: Coder-/Reviewer-Workflows, Dependencies |
|
|
171
|
+
| `orch-test-b` | privat | maintainer, coder, reviewer | Source `test_b`: Kommentar-Workflow, Cross-Repo-Blocker **mit** Zugriff |
|
|
172
|
+
| `orch-test-hidden` | privat | **keine** | Cross-Repo-Blocker **ohne** App-Zugriff (A4) |
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
export ADISPATCH_TEST_OWNER=<dein-account-oder-org>
|
|
176
|
+
for r in orch-test-a orch-test-b orch-test-hidden; do
|
|
177
|
+
gh repo create "$ADISPATCH_TEST_OWNER/$r" --private --add-readme --description "adispatchd manual test (Wegwerf)"
|
|
178
|
+
done
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`orch-test-hidden` muss **privat** sein, sonst kann die App es über die öffentliche API trotzdem lesen.
|
|
182
|
+
|
|
183
|
+
### 1.3 Apps
|
|
184
|
+
|
|
185
|
+
Empfohlen sind **drei** GitHub Apps, eine je Rolle wie in Spec §5.3. Nur so lässt sich prüfen, dass
|
|
186
|
+
- Kommentare der falschen App den Marker nicht „besitzen“ (A11, T6c),
|
|
187
|
+
- Action-Identitäten die Workflow-Identität überschreiben (T7),
|
|
188
|
+
- eine Rolle ohne Schreibrechte wirklich nicht schreiben kann (T19).
|
|
189
|
+
|
|
190
|
+
| App (Vorschlag Name) | Identity | Repository permissions (minimal) | Wofür |
|
|
191
|
+
|---|---|---|---|
|
|
192
|
+
| `orch-test-maintainer-<owner>` | `maintainer` | **Issues: Read-only**, Metadata: Read-only (automatisch) | Source-Identity: Polling, Dependencies, Views, Item-Lesen für Hooks und manuelle Starts |
|
|
193
|
+
| `orch-test-coder-<owner>` | `coder` | **Issues: Read and write**, **Pull requests: Read and write**, Metadata: Read-only | Labels und Kommentare in `coder-test` und `commenter-test`, `GH_TOKEN` des Workers. Liest selbst: Neu-Lesen des Items vor jedem Label-Update und die Marker-Suche nutzen die **schreibende** Identity (`applyUpdate`, `findCommentByMarker`) |
|
|
194
|
+
| `orch-test-reviewer-<owner>` | `reviewer` | **Issues: Read and write**, **Pull requests: Read and write**, Metadata: Read-only | Labels/Kommentare an PRs (`reviewer-test`), Start-Kommentar in `commenter-test`; liest wie coder selbst (Neu-Lesen, Marker-Suche) |
|
|
195
|
+
|
|
196
|
+
Schreibende Apps brauchen also immer auch Lesezugriff auf Issues (in „Read and write“ enthalten). *Issues* darf bei
|
|
197
|
+
coder/reviewer nie auf „No access“ stehen, sonst scheitert schon das Neu-Lesen vor dem Update.
|
|
198
|
+
|
|
199
|
+
Alles andere bleibt auf „No access“: Contents, Workflows, Administration, Account permissions, Organization
|
|
200
|
+
permissions. Ein echter Coder bräuchte zusätzlich *Contents: write* für Push und PR. Das ist hier nicht nötig, weil der
|
|
201
|
+
Test-Worker nichts pusht.
|
|
202
|
+
|
|
203
|
+
*Pull requests* ist bei coder und reviewer bewusst vorhanden, obwohl die Doku *Issues: write* für Labels/Kommentare an
|
|
204
|
+
PRs vermutlich genügen lässt ([R4](#r4)). T19d prüft genau das. Fällt der Test positiv aus, kann *Pull requests* später
|
|
205
|
+
entfallen.
|
|
206
|
+
|
|
207
|
+
**Minimalvariante mit zwei Apps** (weniger Aufwand, eingeschränkte Aussagekraft):
|
|
208
|
+
- Die App `maintainer` bleibt wie oben.
|
|
209
|
+
- Eine App `writer` übernimmt **coder und reviewer**: In `identities.yaml` zeigen beide Einträge auf dieselbe App-ID und
|
|
210
|
+
Installation-ID und dieselbe Key-Variable.
|
|
211
|
+
- Dann entfallen T6c (fremde App mit gleichem Marker) und der Identitätswechsel in T7. Alle anderen Fälle laufen
|
|
212
|
+
unverändert.
|
|
213
|
+
|
|
214
|
+
Eine einzige App für alles ist **nicht** sinnvoll: Dann lässt sich weder die Read-only-Source-Identität (T19a) noch eine
|
|
215
|
+
Rollentrennung prüfen.
|
|
216
|
+
|
|
217
|
+
**Anlegen** (je App, [R5](#r5)):
|
|
218
|
+
|
|
219
|
+
1. *Settings → Developer settings → GitHub Apps → New GitHub App* (bei einer Org:
|
|
220
|
+
*Org Settings → Developer settings → GitHub Apps*).
|
|
221
|
+
2. **GitHub App name** wie oben (≤ 34 Zeichen, global eindeutig). **Homepage URL**: z. B.
|
|
222
|
+
`https://github.com/<owner>/orch-test-a`.
|
|
223
|
+
3. **Webhook → Active abwählen** (der Orchestrator pollt).
|
|
224
|
+
4. **Permissions → Repository permissions** gemäß Tabelle setzen.
|
|
225
|
+
5. **Where can this GitHub App be installed? → Only on this account.** Dann *Create GitHub App*.
|
|
226
|
+
6. Auf der Seite *General*: **App ID** notieren (und die Client ID, nur zur Information).
|
|
227
|
+
7. *Private keys → Generate a private key*. Der Browser lädt `<name>.<datum>.private-key.pem` herunter. Die Datei nach
|
|
228
|
+
`~/orch-test-keys/` verschieben, `chmod 600`. Sie **nie** ins Repo legen.
|
|
229
|
+
8. *Install App → Install → Only select repositories → `orch-test-a`, `orch-test-b` → Install*.
|
|
230
|
+
**Nicht** `orch-test-hidden`.
|
|
231
|
+
9. Installation-ID notieren: Sie steht in der URL nach der Installation (`…/settings/installations/<ID>`). Zuverlässiger
|
|
232
|
+
ist der Helfer aus 2.3: `installation_id <app-id> <pem> orch-test-a`.
|
|
233
|
+
|
|
234
|
+
### 1.4 Private Keys als Umgebungsvariablen
|
|
235
|
+
|
|
236
|
+
Der Orchestrator liest den PEM aus der Variablen, die `private_key_env` nennt (`normalizePrivateKeyPem` in
|
|
237
|
+
`src/identities/jwt.ts`). Erlaubt sind echte Zeilenumbrüche, `\n`-Escapes, CRLF und umschließende Quotes. Beide Formen
|
|
238
|
+
werden getestet:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# Form 1: echte Zeilenumbrüche (direkt in der Shell)
|
|
242
|
+
export ADISPATCH_TEST_MAINTAINER_KEY="$(cat ~/orch-test-keys/orch-test-maintainer-*.private-key.pem)"
|
|
243
|
+
export ADISPATCH_TEST_REVIEWER_KEY="$(cat ~/orch-test-keys/orch-test-reviewer-*.private-key.pem)"
|
|
244
|
+
|
|
245
|
+
# Form 2: einzeilig mit literalen \n (wie in einer systemd-EnvironmentFile oder .env)
|
|
246
|
+
export ADISPATCH_TEST_CODER_KEY="$(awk 'BEGIN{ORS="\\n"}1' ~/orch-test-keys/orch-test-coder-*.private-key.pem)"
|
|
247
|
+
printf '%s' "$ADISPATCH_TEST_CODER_KEY" | head -c 40; echo # → -----BEGIN RSA PRIVATE KEY-----\nMIIE…
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Variante als Datei, die nur gesourct und nie eingecheckt wird (`/tmp/` im Repo ist in `.gitignore`):
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
mkdir -p tmp && umask 077
|
|
254
|
+
{
|
|
255
|
+
printf 'ADISPATCH_TEST_MAINTAINER_KEY="%s"\n' "$(awk 'BEGIN{ORS="\\n"}1' ~/orch-test-keys/orch-test-maintainer-*.pem)"
|
|
256
|
+
printf 'ADISPATCH_TEST_CODER_KEY="%s"\n' "$(awk 'BEGIN{ORS="\\n"}1' ~/orch-test-keys/orch-test-coder-*.pem)"
|
|
257
|
+
printf 'ADISPATCH_TEST_REVIEWER_KEY="%s"\n' "$(awk 'BEGIN{ORS="\\n"}1' ~/orch-test-keys/orch-test-reviewer-*.pem)"
|
|
258
|
+
} > tmp/orch-test.env
|
|
259
|
+
set -a; . tmp/orch-test.env; set +a
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 1.5 App-ID und Installation-ID prüfen
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
source docs/manual-tests/github-fixture/helpers.sh
|
|
266
|
+
export MAINTAINER_APP_ID=<id> CODER_APP_ID=<id> REVIEWER_APP_ID=<id>
|
|
267
|
+
K=~/orch-test-keys
|
|
268
|
+
installation_id "$MAINTAINER_APP_ID" $K/orch-test-maintainer-*.pem orch-test-a
|
|
269
|
+
installation_id "$CODER_APP_ID" $K/orch-test-coder-*.pem orch-test-a
|
|
270
|
+
installation_id "$REVIEWER_APP_ID" $K/orch-test-reviewer-*.pem orch-test-a
|
|
271
|
+
# → {"id": <Installation-ID>, "app_id": …, "app_slug": "…", "repository_selection": "selected", "permissions": {…}}
|
|
272
|
+
export MAINTAINER_INSTALLATION_ID=<id> CODER_INSTALLATION_ID=<id> REVIEWER_INSTALLATION_ID=<id>
|
|
273
|
+
# Gegenprobe: das versteckte Repo hat KEINE Installation (erwartet: 404 "Not Found")
|
|
274
|
+
installation_id "$MAINTAINER_APP_ID" $K/orch-test-maintainer-*.pem orch-test-hidden
|
|
275
|
+
# Slugs für T6 (Bot-Login = <slug>[bot]):
|
|
276
|
+
app_info() { curl -sS -H "Authorization: Bearer $(app_jwt "$1" "$2")" -H 'Accept: application/vnd.github+json' \
|
|
277
|
+
https://api.github.com/app | jq -c '{id, slug, client_id, permissions}'; }
|
|
278
|
+
app_info "$MAINTAINER_APP_ID" $K/orch-test-maintainer-*.pem
|
|
279
|
+
app_info "$CODER_APP_ID" $K/orch-test-coder-*.pem
|
|
280
|
+
app_info "$REVIEWER_APP_ID" $K/orch-test-reviewer-*.pem
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`permissions` muss der Tabelle in 1.3 entsprechen (`issues: read` bzw. `write`, `pull_requests`, `metadata: read`).
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 2 Testkonfiguration
|
|
288
|
+
|
|
289
|
+
### 2.1 Inhalt des Fixtures
|
|
290
|
+
|
|
291
|
+
Das komplette config-dir liegt unter [`github-fixture/`](github-fixture/). Der Test
|
|
292
|
+
`src/sources/github/manual-fixture.test.ts` prüft bei jedem `npm run check`:
|
|
293
|
+
- Das Fixture lädt mit dem echten Loader ohne Fehler und Warnungen.
|
|
294
|
+
- Der Test-Worker hält das Worker-Protokoll ein.
|
|
295
|
+
- Worker und Helfer pinnen dieselbe API-Version wie `GITHUB_API_VERSION`.
|
|
296
|
+
|
|
297
|
+
| Datei | Inhalt |
|
|
298
|
+
|---|---|
|
|
299
|
+
| `config.yaml` | Port **8791**, Polling **20 s**, global `max_concurrent: 4`, Action-Retry 3 × **20 s**, Workspaces behalten |
|
|
300
|
+
| `sources.yaml` | `test_a` → `orch-test-a`, `test_b` → `orch-test-b`, Lese-Identity `maintainer`; Owner-Platzhalter `ADISPATCH_TEST_OWNER` |
|
|
301
|
+
| `identities.yaml` | `maintainer`, `coder`, `reviewer`; Platzhalter-IDs `900000x` (App) / `910000x` (Installation); Keys aus `ADISPATCH_TEST_{MAINTAINER,CODER,REVIEWER}_KEY` |
|
|
302
|
+
| `workflows/coder-test.yaml` | Source `test_a`, Identity `coder`, Filter `labels contains ready-for-coding` **und** `dependencies.blocked_by_open = 0`, `max_concurrent: 2`, `max_technical_failures: 3`, Label-Hooks (`coding` → `ready-for-review` / `needs-maintainer` / zurück zu `ready-for-coding`), Kommentar bei Erfolg und `failure`, `GH_TOKEN` für den Worker |
|
|
303
|
+
| `workflows/reviewer-test.yaml` | Source `test_a`, Identity `reviewer`, nur **PRs** mit `ready-for-review` und ohne offene Blocker, Kommentar bei Erfolg |
|
|
304
|
+
| `workflows/commenter-test.yaml` | Source `test_b`, Label `ready-for-comment`. `on_start` kommentiert als **reviewer** (Action-Identity), `on_success` als **coder** mit `retry: {max_attempts: 6, delay: 20s}` (Szenario 6) |
|
|
305
|
+
| `views/needs-maintainer.yaml` | Szenario 10: beide Repos, Label `needs-maintainer`, letzter Run und Ergebnis, Link |
|
|
306
|
+
| `views/dependencies.yaml` | Beobachtung: alle Items mit Label `deps-test` und ihre `dependencies.*`-Felder (startet nichts) |
|
|
307
|
+
| `scripts/test-worker.sh` | Test-Worker; das Verhalten steuern Labels (siehe unten) |
|
|
308
|
+
| `helpers.sh` | Shell-Helfer für diese Anleitung (wird **nicht** vom Orchestrator ausgeführt) |
|
|
309
|
+
|
|
310
|
+
Labels für den Test-Worker (kombinierbar):
|
|
311
|
+
|
|
312
|
+
| Label | Verhalten | Erwarteter Run |
|
|
313
|
+
|---|---|---|
|
|
314
|
+
| (kein `test:*`) | Ergebnis `success`, Exit 0 | `completed` / `success` → `on_success` |
|
|
315
|
+
| `test:slow` | schläft `TEST_SLOW_SECONDS` (coder 90 s, sonst 60 s), alle 10 s ein `progress`-Event | wie ohne, nur später |
|
|
316
|
+
| `test:failure` | Ergebnis `failure`, Exit 0 | `completed` / `failure` → `on_failure`, kein Retry-Zähler |
|
|
317
|
+
| `test:crash` | schreibt `success`, dann Exit 1 | `crashed` (Datei ignoriert) → `on_crash`, zählt |
|
|
318
|
+
| `test:invalid-result` | ungültige Ergebnisdatei, Exit 0 | `crashed` (Protokollfehler) |
|
|
319
|
+
| `test:token` | prüft `GH_TOKEN` per `GET /repos/…/issues/<n>` | bei HTTP ≠ 200 `crashed` |
|
|
320
|
+
| `test:revoke-token` | widerruft `GH_TOKEN` per `DELETE /installation/token` | Erfolg; der Orchestrator muss das Coder-Token erneuern (T2a) |
|
|
321
|
+
|
|
322
|
+
### 2.2 Fixture kopieren und IDs eintragen
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
source docs/manual-tests/github-fixture/helpers.sh # setzt ADISPATCH_CFG=tmp/orch-cfg, ADISPATCH_DATA=tmp/orch-data, API=…:8791
|
|
326
|
+
rm -rf "$ADISPATCH_CFG" && mkdir -p "$ADISPATCH_CFG" "$ADISPATCH_DATA"
|
|
327
|
+
cp -Rp docs/manual-tests/github-fixture/. "$ADISPATCH_CFG"/
|
|
328
|
+
perl -pi -e "s/ADISPATCH_TEST_OWNER/$ADISPATCH_TEST_OWNER/g" "$ADISPATCH_CFG/sources.yaml"
|
|
329
|
+
perl -pi -e "s/9000001/$MAINTAINER_APP_ID/; s/9100001/$MAINTAINER_INSTALLATION_ID/;
|
|
330
|
+
s/9000002/$CODER_APP_ID/; s/9100002/$CODER_INSTALLATION_ID/;
|
|
331
|
+
s/9000003/$REVIEWER_APP_ID/; s/9100003/$REVIEWER_INSTALLATION_ID/" "$ADISPATCH_CFG/identities.yaml"
|
|
332
|
+
grep -nE 'owner|app_id|installation_id' "$ADISPATCH_CFG"/sources.yaml "$ADISPATCH_CFG"/identities.yaml
|
|
333
|
+
ls -l "$ADISPATCH_CFG/scripts/test-worker.sh" # muss ausführbar sein (rwx)
|
|
334
|
+
orch config validate --config-dir "$ADISPATCH_CFG"
|
|
335
|
+
# → Configuration in …/tmp/orch-cfg is valid: 3 workflow(s), 2 view(s), 2 source(s), 3 identities. Hash …
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Minimalvariante: In `identities.yaml` bei `reviewer` dieselben Werte wie bei `coder` eintragen und
|
|
339
|
+
`private_key_env: ADISPATCH_TEST_CODER_KEY` setzen.
|
|
340
|
+
|
|
341
|
+
### 2.3 Helfer
|
|
342
|
+
|
|
343
|
+
`source docs/manual-tests/github-fixture/helpers.sh` stellt diese Helfer bereit. `probe` und `ghraw` holen sich eigene
|
|
344
|
+
Tokens; die Key-Variablen aus 1.4 müssen deshalb auch in der Test-Shell gesetzt sein.
|
|
345
|
+
|
|
346
|
+
| Helfer | Wirkung |
|
|
347
|
+
|---|---|
|
|
348
|
+
| `orch …` | CLI aus dem Checkout |
|
|
349
|
+
| `api /pfad`, `post /pfad '{json}'` | Orchestrator-API; `post` setzt den CSRF-Header |
|
|
350
|
+
| `runs <wf>` | Runs als Tabelle |
|
|
351
|
+
| `run_of <source> <nr>` | neueste Run-ID eines Items |
|
|
352
|
+
| `diag <wf>` | letzte Planungsdiagnose (Kandidaten + Entscheidung) |
|
|
353
|
+
| `actions <run>` | Action-Journal |
|
|
354
|
+
| `events <run>` | Run-Events |
|
|
355
|
+
| `view <id>` | View-Zeilen, `?` = unbekannt |
|
|
356
|
+
| `mkissue <repo> <titel> [label…]` | Issue anlegen, gibt die Nummer aus |
|
|
357
|
+
| `blockedby <repo> <nr> <blocker-repo> <nr>` | Abhängigkeit setzen (über gh) |
|
|
358
|
+
| `blockedby_api` / `unblock_api` | Abhängigkeit setzen bzw. entfernen über REST (`issue_id`) |
|
|
359
|
+
| `labels <repo> <nr>` | Labels eines Issues |
|
|
360
|
+
| `comments <repo> <nr>` | Kommentare als Tabelle: id, Login, `user.type`, `performed_via_github_app.id`, letzte Zeile |
|
|
361
|
+
| `probe …` | Adapter direkt aufrufen (2.4) |
|
|
362
|
+
| `ghraw <identity> <pfad> [curl-Optionen]` | Rohantwort der API **als App** (frisches Installation-Token) |
|
|
363
|
+
| `app_jwt <app-id> <pem>`, `installation_id <app-id> <pem> <repo>` | App-JWT bzw. Installation einer App ermitteln |
|
|
364
|
+
|
|
365
|
+
### 2.4 Probe-Skript (Adapter direkt)
|
|
366
|
+
|
|
367
|
+
Für Rohbeobachtungen ohne laufende Instanz. Die Datei unter `tmp/probe.ts` anlegen; das Verzeichnis ist in `.gitignore`.
|
|
368
|
+
Sie nutzt dieselbe Verdrahtung wie `start`/`dry-run` (`SourceRegistry` aus `src/app.ts`). Jeder Aufruf holt **eigene**
|
|
369
|
+
Tokens und teilt keinen Cache mit der laufenden Instanz.
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
// tmp/probe.ts – direkter Zugriff auf Adapter/Credentials mit der Test-Konfiguration (nicht einchecken; /tmp/ ist ignoriert).
|
|
373
|
+
// Aufruf: npx tsx tmp/probe.ts <befehl> ... (config-dir aus $ADISPATCH_CFG, Logs auf stderr, Ergebnis als JSON auf stdout)
|
|
374
|
+
import { loadConfig } from '../src/config/index.js';
|
|
375
|
+
import { ConsoleLogger, SecretRedactor, systemClock } from '../src/core/index.js';
|
|
376
|
+
import { SourceRegistry } from '../src/app.js';
|
|
377
|
+
import { DEPENDENCY_FIELDS } from '../src/sources/index.js';
|
|
378
|
+
|
|
379
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
380
|
+
const loaded = await loadConfig(process.env.ADISPATCH_CFG ?? 'tmp/orch-cfg', { dataDir: process.env.ADISPATCH_DATA ?? 'tmp/orch-data' });
|
|
381
|
+
if (!loaded.ok) throw new Error(`config invalid: ${JSON.stringify(loaded.errors)}`);
|
|
382
|
+
const redactor = new SecretRedactor();
|
|
383
|
+
const logger = new ConsoleLogger({ level: 'debug', redact: redactor.asRedactor });
|
|
384
|
+
const registry = new SourceRegistry(loaded.config, { redactor, logger });
|
|
385
|
+
const out = (v: unknown): void => void process.stdout.write(`${JSON.stringify(v, null, 2)}\n`);
|
|
386
|
+
const adapter = (id: string) => registry.get(id) ?? (() => { throw new Error(`unknown source ${id}`); })();
|
|
387
|
+
const item = async (source: string, ref: string) => {
|
|
388
|
+
const a = adapter(source);
|
|
389
|
+
const id = a.parseItemRef(ref);
|
|
390
|
+
const it = id ? await a.getItem(id) : null;
|
|
391
|
+
if (!it) throw new Error(`item ${ref} not found in ${source}`);
|
|
392
|
+
return { a, it };
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
switch (cmd) {
|
|
396
|
+
case 'token': { // token <identity> → nur das Token (für curl); 1 h gültig
|
|
397
|
+
const t = await registry.getToken(args[0]!);
|
|
398
|
+
process.stderr.write(`expires_at ${t.expiresAt.toISOString()}\n`);
|
|
399
|
+
process.stdout.write(`${t.token}\n`);
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case 'list': { // list <source> [label]
|
|
403
|
+
const filter = args[1] ? { field: 'labels', operator: 'contains' as const, value: args[1] } : undefined;
|
|
404
|
+
const items = await adapter(args[0]!).listCandidates(filter ? { filter } : {});
|
|
405
|
+
out(items.map((i) => ({ key: i.key, type: i.type, state: i.fields.state, labels: i.fields.labels, title: i.fields.title })));
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
case 'item': { // item <source> <ref>
|
|
409
|
+
out((await item(args[0]!, args[1]!)).it);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case 'deps': { // deps <source> <ref>
|
|
413
|
+
const { a, it } = await item(args[0]!, args[1]!);
|
|
414
|
+
out(await a.resolveFields(it, DEPENDENCY_FIELDS));
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case 'labels': { // labels <source> <ref> <identity> +add -remove ...
|
|
418
|
+
const { a, it } = await item(args[0]!, args[1]!);
|
|
419
|
+
const add = args.slice(3).filter((l) => l.startsWith('+')).map((l) => l.slice(1));
|
|
420
|
+
const remove = args.slice(3).filter((l) => l.startsWith('-')).map((l) => l.slice(1));
|
|
421
|
+
out(await a.applyUpdate(it, { labels: { add, remove } }, args[2]!));
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case 'comment': { // comment <source> <ref> <identity> <marker> <text>
|
|
425
|
+
const { a, it } = await item(args[0]!, args[1]!);
|
|
426
|
+
out(await a.postComment(it, args[4] ?? 'probe', args[3]!, args[2]!));
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case 'find': { // find <source> <ref> <identity> <marker>
|
|
430
|
+
const { a, it } = await item(args[0]!, args[1]!);
|
|
431
|
+
out(await a.findCommentByMarker(it, args[3]!, args[2]!));
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
default:
|
|
435
|
+
process.stderr.write('usage: probe.ts token|list|item|deps|labels|comment|find ...\n');
|
|
436
|
+
process.exitCode = 2;
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Der Code wurde am 2026-10-17 gegen diesen Stand mit `tsc` geprüft. Bricht er nach Refactorings, die Importe an die
|
|
441
|
+
`index.ts`-Exporte anpassen.
|
|
442
|
+
|
|
443
|
+
### 2.5 Instanz starten
|
|
444
|
+
|
|
445
|
+
In einem **eigenen Terminal**, in dem die Key-Variablen gesetzt sind:
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
source docs/manual-tests/github-fixture/helpers.sh
|
|
449
|
+
orch start --config-dir "$ADISPATCH_CFG" --data-dir "$ADISPATCH_DATA" 2>&1 | tee -a tmp/orch.log
|
|
450
|
+
# → URL http://127.0.0.1:8791, Stop-Timeouts; Logs (JSON-Zeilen) auf stderr → tmp/orch.log
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Weboberfläche: <http://127.0.0.1:8791>. Bei Bedarf stoppen und neu starten (`Ctrl-C` = Drain, zweimal = Cancel-All).
|
|
454
|
+
Die Daten in `$ADISPATCH_DATA` bleiben erhalten.
|
|
455
|
+
|
|
456
|
+
**Während des Testdaten-Setups** die automatischen Starts anhalten, damit kein Run startet, bevor alle Abhängigkeiten
|
|
457
|
+
gesetzt sind:
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
post /scheduler/pause | jq .scheduler.state # → "paused" (Views und Dry-Run funktionieren weiter)
|
|
461
|
+
post /scheduler/resume | jq .scheduler.state # → "running" (sofortiger Planungsschritt)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## 3 Testdaten
|
|
467
|
+
|
|
468
|
+
Alle Befehle setzen `source …/helpers.sh` und `ADISPATCH_TEST_OWNER` voraus. Die Testfälle legen ihre Issues selbst an
|
|
469
|
+
(`mkissue`). Hier entstehen nur Labels, Blocker und der PR.
|
|
470
|
+
|
|
471
|
+
### 3.1 Labels (in beiden Repos)
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
for r in orch-test-a orch-test-b; do
|
|
475
|
+
for l in ready-for-coding coding ready-for-review reviewing reviewed needs-maintainer \
|
|
476
|
+
ready-for-comment commenting commented deps-test manual-keep 'priority:high' 'prio: high/urgent' \
|
|
477
|
+
test:slow test:failure test:crash test:invalid-result test:token test:revoke-token; do
|
|
478
|
+
gh label create "$l" -R "$(repo $r)" --color C5DEF5 --force >/dev/null && printf '.'
|
|
479
|
+
done; echo " $r"
|
|
480
|
+
done
|
|
481
|
+
gh label list -R "$(repo orch-test-a)" --limit 100 | wc -l # ≥ 21 (inkl. GitHub-Standardlabels)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Die Hooks setzen auch selbst Labels an, die es noch nicht gibt. GitHub legt sie dann an. Das Vorab-Anlegen sorgt nur für
|
|
485
|
+
Ordnung.
|
|
486
|
+
|
|
487
|
+
### 3.2 Blocker-Issues
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
post /scheduler/pause >/dev/null
|
|
491
|
+
export A_BLOCKER=$(mkissue orch-test-a "Blocker A (same repo)")
|
|
492
|
+
export B_BLOCKER=$(mkissue orch-test-b "Blocker B (cross repo, App hat Zugriff)")
|
|
493
|
+
export H_BLOCKER=$(mkissue orch-test-hidden "Blocker H (cross repo, KEIN App-Zugriff)")
|
|
494
|
+
echo "A_BLOCKER=$A_BLOCKER B_BLOCKER=$B_BLOCKER H_BLOCKER=$H_BLOCKER" | tee -a tmp/testdata.env
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### 3.3 Pull Request in `orch-test-a`
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
R=$(repo orch-test-a)
|
|
501
|
+
sha=$(gh api repos/$R/git/ref/heads/main --jq .object.sha)
|
|
502
|
+
gh api -X POST repos/$R/git/refs -f ref=refs/heads/pr-test -f sha="$sha" >/dev/null
|
|
503
|
+
gh api -X PUT repos/$R/contents/pr-test.txt -f message="PR-Test" -f branch=pr-test \
|
|
504
|
+
-f content="$(printf 'manual test\n' | base64)" >/dev/null
|
|
505
|
+
export A_PR=$(gh pr create -R $R --head pr-test --base main --title "PR-Test (Reviewer)" --body "manual test" | sed 's#.*/##')
|
|
506
|
+
echo "A_PR=$A_PR" | tee -a tmp/testdata.env
|
|
507
|
+
post /scheduler/resume >/dev/null
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Variablen in einer neuen Shell wiederherstellen: `set -a; . tmp/testdata.env; set +a`.
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## 4 Testfälle
|
|
515
|
+
|
|
516
|
+
Format je Fall: **Ziel** (Bezug), **Schritte**, **Erwartet**, **Ist**. Die Ist-Zeile wird im Protokoll ([5.1](#51-ergebnisse-festhalten))
|
|
517
|
+
oder direkt hier abgehakt. „Nächster Poll“ heißt: höchstens 20 s plus Dauer des Planungsschritts. Die Instanz läuft
|
|
518
|
+
während aller Fälle (2.5), sofern nicht anders angegeben.
|
|
519
|
+
|
|
520
|
+
Reihenfolge: T1–T2 zuerst, dann beliebig. T2b läuft nebenher. T19 und T7 verändern App-Installationen: anschließend
|
|
521
|
+
**immer zurücksetzen**.
|
|
522
|
+
|
|
523
|
+
### T1 – App-Auth: JWT → Installation-Token (A1, A9)
|
|
524
|
+
|
|
525
|
+
**Ziel:** Alle drei Identities bekommen Tokens. Das JWT-Format wird akzeptiert, `expires_at` liegt etwa 1 h in der
|
|
526
|
+
Zukunft, und jedes Token sieht genau die installierten Repos.
|
|
527
|
+
|
|
528
|
+
**Schritte:**
|
|
529
|
+
```bash
|
|
530
|
+
grep 'Obtained GitHub installation token' tmp/orch.log | tail -5
|
|
531
|
+
for i in maintainer coder reviewer; do
|
|
532
|
+
echo "== $i"; ghraw $i /installation/repositories | jq -r '.repositories[].full_name'
|
|
533
|
+
done
|
|
534
|
+
ghraw coder /installation/repositories -o /dev/null -D - | grep -i -E '^x-github-api-version-selected|^x-ratelimit-limit'
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Erwartet:**
|
|
538
|
+
- Das Log hat je benutzter Identity eine Zeile `Obtained GitHub installation token` mit `identity` und `expiresAt`
|
|
539
|
+
(≈ jetzt + 60 min), aber **ohne** Token- oder Key-Inhalt.
|
|
540
|
+
- Jede App listet genau `orch-test-a` und `orch-test-b`, **nicht** `orch-test-hidden`.
|
|
541
|
+
- Der Header `x-github-api-version-selected: 2026-03-10` ist vorhanden. Steht eine andere Version darin, ist A1
|
|
542
|
+
abweichend. Fehlt der Header ganz, nur notieren.
|
|
543
|
+
|
|
544
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
545
|
+
|
|
546
|
+
### T2 – Token-Erneuerung (A9, §22.1 „Token-Rotation“)
|
|
547
|
+
|
|
548
|
+
**T2a – erzwungen (Token widerrufen).**
|
|
549
|
+
|
|
550
|
+
**Ziel:** Ein ungültiges gecachtes Token (401) führt zu `invalidate` und einem neuen Token. Die Aktion gelingt ohne
|
|
551
|
+
sichtbaren Fehler. Der Worker bekommt dasselbe gecachte Coder-Token (`execute.secrets`) und widerruft es.
|
|
552
|
+
|
|
553
|
+
**Schritte:**
|
|
554
|
+
```bash
|
|
555
|
+
N=$(mkissue orch-test-a "T2a revoke token" ready-for-coding test:revoke-token)
|
|
556
|
+
sleep 45; RUN=$(run_of test_a $N); echo $RUN
|
|
557
|
+
api "/runs/$RUN/logs?stream=stdout" | jq -r .data | grep DELETE
|
|
558
|
+
actions $RUN
|
|
559
|
+
grep -c '"identity":"coder"' <(grep 'Obtained GitHub installation token' tmp/orch.log)
|
|
560
|
+
labels orch-test-a $N
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Erwartet:**
|
|
564
|
+
- Das stdout des Workers enthält `DELETE /installation/token → HTTP 204`.
|
|
565
|
+
- Die `on_success`-Actions sind `succeeded` mit `attempts=1`. Der 401 wird im Adapter einmal wiederholt und zählt nicht
|
|
566
|
+
als Action-Versuch.
|
|
567
|
+
- Das Log enthält **eine weitere** Zeile `Obtained GitHub installation token` für `coder`, kurz nach dem Run-Ende.
|
|
568
|
+
- Labels am Ende: `ready-for-review,test:revoke-token`.
|
|
569
|
+
|
|
570
|
+
Abweichung: Ist die Action `failed` mit HTTP 401, wiederholt der Adapter nach 401 nicht. Bleibt die zweite Token-Zeile
|
|
571
|
+
aus, obwohl alles gelang, bekam der Worker ein anderes Token als der Adapter. Das ist kein Fehler, aber zu notieren.
|
|
572
|
+
|
|
573
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
574
|
+
|
|
575
|
+
**T2b – natürlicher Ablauf (läuft nebenher).**
|
|
576
|
+
|
|
577
|
+
**Ziel:** Das Token wird **vor** dem Ablauf erneuert (Refresh-Marge 5 min). Es gibt keinen 401 im Log.
|
|
578
|
+
|
|
579
|
+
**Schritte:** Die Instanz ≥ 65 min ohne Neustart laufen lassen. Dann:
|
|
580
|
+
```bash
|
|
581
|
+
grep 'Obtained GitHub installation token' tmp/orch.log | jq -c '{time, identity, expiresAt}'
|
|
582
|
+
grep -E 'HTTP 401|"kind":"auth"' tmp/orch.log | head
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**Erwartet:**
|
|
586
|
+
- Für `maintainer` (pollt ständig) kommt etwa alle 55 min eine neue Zeile; `expiresAt` der Vorgängerzeile minus etwa
|
|
587
|
+
5 min ≈ Zeit der neuen Zeile.
|
|
588
|
+
- Es gibt keine 401-Zeilen, außer der aus T2a.
|
|
589
|
+
|
|
590
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
591
|
+
|
|
592
|
+
### T3 – Polling, Label-Filter, Pushdown (§6, §7)
|
|
593
|
+
|
|
594
|
+
**Ziel:** Nur offene Items mit `ready-for-coding` werden gepollt (Pushdown `labels=`, `state=open`). Items ohne Label und
|
|
595
|
+
geschlossene Items erscheinen nicht.
|
|
596
|
+
|
|
597
|
+
**Schritte:**
|
|
598
|
+
```bash
|
|
599
|
+
post /scheduler/pause >/dev/null
|
|
600
|
+
N_OK=$(mkissue orch-test-a "T3 mit Label" ready-for-coding manual-keep); echo "N_OK=$N_OK" >> tmp/testdata.env
|
|
601
|
+
N_NO=$(mkissue orch-test-a "T3 ohne Label")
|
|
602
|
+
N_CL=$(mkissue orch-test-a "T3 geschlossen mit Label" ready-for-coding); gh issue close $N_CL -R $(repo orch-test-a)
|
|
603
|
+
orch workflow dry-run coder-test --config-dir "$ADISPATCH_CFG" --local --data-dir "$ADISPATCH_DATA" # lokal: Gate egal
|
|
604
|
+
post /scheduler/resume >/dev/null; sleep 25; runs coder-test 5 | grep "test_a:$N_OK"
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Erwartet:**
|
|
608
|
+
- Der Dry-Run zeigt `pushdown {"field":"labels","operator":"contains","value":"ready-for-coding"}`.
|
|
609
|
+
- `polled` entspricht der Zahl **offener** Items mit `ready-for-coding`.
|
|
610
|
+
- `test_a:$N_OK` ist `would_start`; `$N_NO` und `$N_CL` fehlen ganz.
|
|
611
|
+
- Nach `resume` gibt es einen Run für `test_a:$N_OK` (`runs`). `diag` zeigt nur den **letzten** Planungsschritt, in dem
|
|
612
|
+
das gestartete Item meist schon `locked` ist oder fehlt; Starts deshalb immer über `runs`/`run_of` prüfen.
|
|
613
|
+
- Über HTTP (`dry-run` ohne `--local`) wäre das Item bei pausiertem Scheduler `gate_closed` statt `would_start`.
|
|
614
|
+
|
|
615
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
616
|
+
|
|
617
|
+
### T4 – Erfolgreicher Run und Label-Hooks, manuelle Labels bleiben (§10, §24.5)
|
|
618
|
+
|
|
619
|
+
**Ziel:** `on_start` und `on_success` setzen bzw. entfernen genau die konfigurierten Labels. Das manuelle Label
|
|
620
|
+
`manual-keep` bleibt.
|
|
621
|
+
|
|
622
|
+
**Schritte:** Fortsetzung von T3.
|
|
623
|
+
```bash
|
|
624
|
+
RUN=$(run_of test_a $N_OK); sleep 10; api /runs/$RUN | jq '{status, outcome, businessResult, resultMessage, identity, workspacePath, lifecycleStatus}'
|
|
625
|
+
actions $RUN
|
|
626
|
+
labels orch-test-a $N_OK
|
|
627
|
+
gh api repos/$(repo orch-test-a)/issues/$N_OK/events --jq '.[] | select(.event|test("label")) | [.event, .label.name, .actor.login, (.performed_via_github_app.slug // "-")] | @tsv'
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
**Erwartet:**
|
|
631
|
+
- Der Run ist `completed`, `outcome: success`, mit `resultMessage: "Simulierter Erfolg fuer …"` und `identity: coder`.
|
|
632
|
+
- `on_start-labels`, `on_success-labels` und `on_success-comment` sind `succeeded` (explizite Action-IDs im Fixture).
|
|
633
|
+
- Labels: `manual-keep,ready-for-review`.
|
|
634
|
+
- Die Label-Events stammen vom Coder-Bot (`<coder-slug>[bot]`): erst `labeled coding` / `unlabeled ready-for-coding`,
|
|
635
|
+
dann `labeled ready-for-review` / `unlabeled coding`.
|
|
636
|
+
|
|
637
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
638
|
+
|
|
639
|
+
### T5 – Label-Updates idempotent, Sonderzeichen, Groß/Klein (§10, `applyUpdate`)
|
|
640
|
+
|
|
641
|
+
**Schritte:**
|
|
642
|
+
```bash
|
|
643
|
+
N=$(mkissue orch-test-a "T5 labels" manual-keep 'prio: high/urgent'); echo "N_T5=$N" >> tmp/testdata.env
|
|
644
|
+
probe labels test_a $N coder +coding -'prio: high/urgent' # 1. Aufruf
|
|
645
|
+
probe labels test_a $N coder +coding -'prio: high/urgent' # 2. Aufruf
|
|
646
|
+
probe labels test_a $N coder +CODING # Groß/Klein
|
|
647
|
+
gh issue edit $N -R $(repo orch-test-a) --add-label needs-maintainer # „manuelle“ Änderung dazwischen
|
|
648
|
+
probe labels test_a $N coder -coding
|
|
649
|
+
labels orch-test-a $N
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Erwartet:**
|
|
653
|
+
- Der 1. Aufruf gibt `{"changed": true, "applied": {"labelsAdded": ["coding"], "labelsRemoved": ["prio: high/urgent"]}}`.
|
|
654
|
+
Das Label mit Leerzeichen und Slash wird entfernt (URL-Encoding).
|
|
655
|
+
- Der 2. Aufruf und der Groß/Klein-Aufruf geben `{"changed": false}`.
|
|
656
|
+
- Am Ende: `manual-keep,needs-maintainer`. Die manuell hinzugefügten Labels bleiben.
|
|
657
|
+
|
|
658
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
659
|
+
|
|
660
|
+
### T6 – Kommentar mit Marker, Autor-Erkennung (A11, §10 Idempotenz)
|
|
661
|
+
|
|
662
|
+
**Ziel:** Kommentare tragen den unsichtbaren Marker als letzte Zeile. `performed_via_github_app.id` und `user.type`
|
|
663
|
+
entsprechen A11. `findCommentByMarker` findet nur Kommentare der **eigenen** App.
|
|
664
|
+
|
|
665
|
+
**Schritte:**
|
|
666
|
+
```bash
|
|
667
|
+
comments orch-test-a $N_OK # Kommentar aus T4 (on_success-comment)
|
|
668
|
+
MARK=$(comments orch-test-a $N_OK | tail -1 | grep -o 'adispatchd:[^ ]*')
|
|
669
|
+
probe find test_a $N_OK coder "$MARK" # a) eigene App
|
|
670
|
+
probe find test_a $N_OK reviewer "$MARK" # c) fremde App, gleicher Marker
|
|
671
|
+
N=$(mkissue orch-test-a "T6 marker")
|
|
672
|
+
gh issue comment $N -R $(repo orch-test-a) --body $'Mensch\n\n<!-- adispatchd:manual-human:1 -->'
|
|
673
|
+
probe find test_a $N coder adispatchd:manual-human:1 # b) Mensch, gleicher Marker
|
|
674
|
+
probe comment test_a $N reviewer adispatchd:manual-rev:1 'als reviewer'
|
|
675
|
+
probe find test_a $N coder adispatchd:manual-rev:1 # c) andere App
|
|
676
|
+
probe find test_a $N reviewer adispatchd:manual-rev:1 # a) eigene App
|
|
677
|
+
probe comment test_a $A_PR coder adispatchd:manual-pr:1 'Kommentar an PR'
|
|
678
|
+
gh pr view $A_PR -R $(repo orch-test-a) --json comments,reviews --jq '{comments: (.comments|length), reviews: (.reviews|length)}'
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Erwartet:**
|
|
682
|
+
- `comments` zeigt Login `<coder-slug>[bot]`, Typ `Bot`, App-ID = `$CODER_APP_ID`. Die letzte Zeile ist
|
|
683
|
+
`<!-- adispatchd:run-…:<action-id> -->`. Im Web-UI ist der Marker unsichtbar.
|
|
684
|
+
- a) gibt `{"id": "…", "url": "…#issuecomment-…"}`. b) und c) geben `null`.
|
|
685
|
+
- Der PR-Kommentar landet in der Konversation (`comments` +1), nicht als Review (`reviews` unverändert).
|
|
686
|
+
|
|
687
|
+
Weicht `performed_via_github_app` ab (fehlt oder `null`), ist A11 verletzt. Folge: Retries erzeugen Duplikate.
|
|
688
|
+
Betroffener Code: `authorPredicate`, siehe [5.2](#52-bei-abweichungen).
|
|
689
|
+
|
|
690
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
691
|
+
|
|
692
|
+
### T7 – Retry ohne Duplikat: Marker-Abgleich nach endgültigem Fehler (§10, Szenario 6 Teil 2, A11)
|
|
693
|
+
|
|
694
|
+
**Ziel:** Eine endgültig gescheiterte Kommentar-Action stoppt die Kette sichtbar. Nach der Behebung erkennt **Retry
|
|
695
|
+
Action** einen bereits vorhandenen Kommentar am Marker und postet **nicht** doppelt. Das simuliert „Kommentar wurde
|
|
696
|
+
gepostet, Antwort ging verloren“. Zusätzlich wird der Identitätswechsel geprüft: Der Start-Kommentar stammt vom Reviewer.
|
|
697
|
+
|
|
698
|
+
**Schritte:**
|
|
699
|
+
1. Issue in `orch-test-b` anlegen, der Worker läuft 60 s:
|
|
700
|
+
```bash
|
|
701
|
+
N=$(mkissue orch-test-b "T7 retry ohne Duplikat" ready-for-comment test:slow)
|
|
702
|
+
sleep 30; RUN=$(run_of test_b $N); actions $RUN
|
|
703
|
+
```
|
|
704
|
+
2. **Während der Worker läuft:** Die Coder-App verliert den Zugriff auf `orch-test-b`. Dazu
|
|
705
|
+
*Settings → Applications → Installed GitHub Apps → orch-test-coder-… → Configure → Repository access* öffnen,
|
|
706
|
+
`orch-test-b` entfernen und *Save* klicken.
|
|
707
|
+
3. Das Run-Ende abwarten (ca. 40 s), dann:
|
|
708
|
+
```bash
|
|
709
|
+
actions $RUN; api /runs/$RUN | jq '{status, outcome, lifecycleStatus, lifecycleFailed}'
|
|
710
|
+
api /actions/failed | jq '.actions[] | {id, hook, name, status, attempts, err: .lastError.message}'
|
|
711
|
+
api '/notifications?limit=3' | jq '.notifications[] | {eventType, deliveryState}'
|
|
712
|
+
```
|
|
713
|
+
4. Zugriff für die Coder-App wiederherstellen (`orch-test-b` wieder auswählen). Dann den Kommentar **mit genau dem
|
|
714
|
+
Marker** der gescheiterten Action als Coder posten. Das ist die Simulation „gepostet, Antwort verloren“:
|
|
715
|
+
```bash
|
|
716
|
+
AID=$(api /runs/$RUN | jq -r '.actions[] | select(.name=="done-comment") | .id')
|
|
717
|
+
probe comment test_b $N coder "adispatchd:$RUN:$AID" 'Kommentar-Test fertig (vorab, simuliert)'
|
|
718
|
+
post /actions/$AID/retry | jq .accepted
|
|
719
|
+
sleep 5; actions $RUN; events $RUN | grep -i reconciled
|
|
720
|
+
comments orch-test-b $N
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
**Erwartet:**
|
|
724
|
+
- Nach Schritt 1 ist `on_start-labels` `succeeded` und `start-comment` `succeeded`. Der erste Kommentar stammt von
|
|
725
|
+
`<reviewer-slug>[bot]` mit App-ID `$REVIEWER_APP_ID` (Action-Identity überschreibt die Workflow-Identity).
|
|
726
|
+
- Schritt 3:
|
|
727
|
+
- Der Run ist `completed`, `outcome: success`, mit `lifecycleStatus: failed` und `lifecycleFailed: true`.
|
|
728
|
+
- `done-comment` ist `failed` nach **1** Versuch, mit HTTP 404 bzw. 403. Das ist nicht wiederholbar, bewusst nach
|
|
729
|
+
WI-07.
|
|
730
|
+
- Das folgende `on_success-labels` bleibt `pending`.
|
|
731
|
+
- Es gibt eine Notification `lifecycle_action_failed`.
|
|
732
|
+
- Schritt 4:
|
|
733
|
+
- Der Retry endet mit `done-comment` `succeeded` und `externalKey` = ID des Vorab-Kommentars. Das Event
|
|
734
|
+
`lifecycle.action` hat `reconciled: true`.
|
|
735
|
+
- Danach läuft `on_success-labels`: Labels `commented,test:slow`.
|
|
736
|
+
- `comments` zeigt **genau einen** Kommentar mit diesem Marker. Der Worker wurde **nicht** neu gestartet (nur ein
|
|
737
|
+
`run.spawn` in `events`).
|
|
738
|
+
|
|
739
|
+
Variante ohne manuelles Posten: Wird Schritt 4 ohne `probe comment` ausgeführt, postet der Retry genau einmal.
|
|
740
|
+
|
|
741
|
+
Gelingt `done-comment` in Schritt 3 trotz entferntem Zugriff, war das gecachte Token noch gültig. Die Beobachtung
|
|
742
|
+
notieren und den Test so wiederholen: Den Zugriff **vor** dem Anlegen des Issues entziehen und die Instanz neu starten.
|
|
743
|
+
`on_start` scheitert dann allerdings ebenfalls, siehe T19b.
|
|
744
|
+
|
|
745
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
746
|
+
|
|
747
|
+
### T8 – Temporärer Kommentarfehler → automatischer Action-Retry (Szenario 6)
|
|
748
|
+
|
|
749
|
+
**Ziel:** Netzfehler beim Kommentar sind `transient`. Die Action wird nach 20 s wiederholt, der Worker läuft **nicht**
|
|
750
|
+
erneut, und es entsteht genau ein Kommentar.
|
|
751
|
+
|
|
752
|
+
**Schritte:**
|
|
753
|
+
```bash
|
|
754
|
+
N=$(mkissue orch-test-b "T8 Netzwerk weg" ready-for-comment test:slow)
|
|
755
|
+
sleep 30; RUN=$(run_of test_b $N); api "/runs/$RUN/logs?stream=stdout&tail=200" | jq -r .data # Fortschritt beobachten
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
Kurz **vor** dem Worker-Ende (bei `working 50/60s`) das Netz kappen und nach etwa 45 s wiederherstellen:
|
|
759
|
+
|
|
760
|
+
| System | Netz aus | Netz an |
|
|
761
|
+
|---|---|---|
|
|
762
|
+
| macOS (WLAN) | `networksetup -setairportpower en0 off` | `networksetup -setairportpower en0 on` |
|
|
763
|
+
| Linux (NetworkManager) | `nmcli networking off` | `nmcli networking on` |
|
|
764
|
+
| nur GitHub-API sperren | `echo '0.0.0.0 api.github.com' \| sudo tee -a /etc/hosts` (macOS danach `sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder`) | Zeile aus `/etc/hosts` entfernen, Cache erneut leeren |
|
|
765
|
+
|
|
766
|
+
```bash
|
|
767
|
+
sleep 120; actions $RUN
|
|
768
|
+
api /runs/$RUN | jq '.actions[] | select(.name=="done-comment") | .attemptsDetail[] | {attempt, outcome, error: .error.message}'
|
|
769
|
+
events $RUN | grep -c run.spawn
|
|
770
|
+
comments orch-test-b $N | grep -c 'adispatchd:'
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
**Erwartet:**
|
|
774
|
+
- `done-comment` hat ≥ 2 Versuche. Die ersten sind `failed` mit `network error` bzw. `request timed out` (`transient`),
|
|
775
|
+
der letzte `succeeded`. Die Abstände betragen etwa 20 s (`retryInMs` in den `lifecycle.action`-Events).
|
|
776
|
+
- `run.spawn` = 1.
|
|
777
|
+
- Genau **2** Kommentare mit Marker: `start-comment` und `done-comment`. Es gibt **kein** Duplikat. Ab dem 2. Versuch
|
|
778
|
+
sucht die Engine zuerst per Marker.
|
|
779
|
+
- Bleibt das Netz länger als 6 × 20 s weg, wird `done-comment` `failed`. Dann gilt Retry Action wie in T7.
|
|
780
|
+
|
|
781
|
+
Hinweis: Wird auch das Token-Holen getroffen, steht die Meldung beim Token-Austausch. Das ist ebenfalls `transient` und
|
|
782
|
+
ebenso wiederholbar.
|
|
783
|
+
|
|
784
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
785
|
+
|
|
786
|
+
### T9 – Fachliches `failure` → `needs-maintainer` (Szenario 4)
|
|
787
|
+
|
|
788
|
+
**Schritte:**
|
|
789
|
+
```bash
|
|
790
|
+
N_FAIL=$(mkissue orch-test-a "T9 failure" ready-for-coding test:failure); echo "N_FAIL=$N_FAIL" >> tmp/testdata.env
|
|
791
|
+
sleep 45; RUN=$(run_of test_a $N_FAIL)
|
|
792
|
+
api /runs/$RUN | jq '{status, outcome, businessResult, resultMessage, countedTechnicalFailure, block}'
|
|
793
|
+
labels orch-test-a $N_FAIL; comments orch-test-a $N_FAIL
|
|
794
|
+
sleep 25; diag coder-test | grep "test_a:$N_FAIL"; api /blocks | jq '.blocks | length'
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
**Erwartet:**
|
|
798
|
+
- `completed` / `outcome: failure` / `businessResult: failure`, mit `countedTechnicalFailure: false` und `block: null`.
|
|
799
|
+
- Labels: `needs-maintainer,test:failure` (`coding` weg, `ready-for-coding` weg).
|
|
800
|
+
- Der Kommentar lautet `Needs maintainer: Simulierter fachlicher Fehler fuer …`.
|
|
801
|
+
- Kein neuer Start, weil `ready-for-coding` fehlt; `diag` zeigt das Item deshalb gar nicht mehr (Ausgabe leer).
|
|
802
|
+
Es entsteht kein Block.
|
|
803
|
+
|
|
804
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
805
|
+
|
|
806
|
+
### T10 – Issue vs. Pull Request (Reviewer-Filter, Dependencies an PRs)
|
|
807
|
+
|
|
808
|
+
**Ziel:**
|
|
809
|
+
- PRs erscheinen im Listing (`type: pull_request`, `is_pull_request: true`), obwohl die Maintainer-App nur *Issues: read*
|
|
810
|
+
hat ([R4](#r4)).
|
|
811
|
+
- `reviewer-test` startet nur den PR. Ein Issue mit gleichem Label wird gefiltert.
|
|
812
|
+
- Dependencies sind an PRs auflösbar (offen, nicht dokumentiert).
|
|
813
|
+
|
|
814
|
+
**Schritte:**
|
|
815
|
+
```bash
|
|
816
|
+
gh pr edit $A_PR -R $(repo orch-test-a) --add-label ready-for-review
|
|
817
|
+
probe list test_a ready-for-review | jq '.[] | {key, type}'
|
|
818
|
+
probe deps test_a $A_PR
|
|
819
|
+
ghraw maintainer "/repos/$(repo orch-test-a)/issues/$A_PR/dependencies/blocked_by" -w '\nHTTP %{http_code}\n'
|
|
820
|
+
sleep 25; diag reviewer-test | grep "test_a:$N_OK" # Issue: dauerhaft filtered
|
|
821
|
+
sleep 70; RUN=$(run_of test_a $A_PR); echo "PR-Run: ${RUN:-keiner}"; api /runs/$RUN | jq '{status, outcome, identity}'
|
|
822
|
+
gh pr view $A_PR -R $(repo orch-test-a) --json labels,comments --jq '{labels: [.labels[].name], last: .comments[-1].author.login}'
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
**Erwartet:**
|
|
826
|
+
- `list` enthält `$A_PR` mit `type: pull_request` und das Issue aus T4 (`$N_OK`) mit `type: issue`.
|
|
827
|
+
- Für `deps` und den Rohaufruf gibt es zwei mögliche Ergebnisse:
|
|
828
|
+
- (a) HTTP 200 `[]`, `status: ok`: Dependencies funktionieren an PRs.
|
|
829
|
+
- (b) 404/403, `status: unknown`.
|
|
830
|
+
|
|
831
|
+
**Ergebnis notieren.** Bei (b) startet `reviewer-test` den PR nie (`filter_unknown`). Dann muss der Dependency-Filter
|
|
832
|
+
für PR-Workflows dokumentiert entfallen, siehe [5.2](#52-bei-abweichungen).
|
|
833
|
+
- `diag reviewer-test`: Das Issue `test_a:$N_OK` ist `filtered` (`is_pull_request` = false), in jedem Planungsschritt.
|
|
834
|
+
- Bei (a) gibt `run_of` einen Run für den PR. `diag` eignet sich dafür nicht: Es zeigt nur den letzten Schritt, und
|
|
835
|
+
nach dem Start fehlt der PR dort (Label `ready-for-review` entfernt). Bei (b) gibt es keinen Run, `diag` zeigt
|
|
836
|
+
`filter_unknown`.
|
|
837
|
+
- Der Run ist `completed` mit `identity: reviewer`.
|
|
838
|
+
- PR-Labels: `reviewed`. Der letzte Kommentar stammt von `<reviewer-slug>[bot]`.
|
|
839
|
+
|
|
840
|
+
**Ist:** ☐ OK ☐ Abweichung: …… (PR-Dependencies: ☐ a ☐ b)
|
|
841
|
+
|
|
842
|
+
### T11 – Dependency im selben Repo (A2, A5)
|
|
843
|
+
|
|
844
|
+
**Schritte:**
|
|
845
|
+
```bash
|
|
846
|
+
post /scheduler/pause >/dev/null
|
|
847
|
+
N_SAME=$(mkissue orch-test-a "T11 blocked by same repo" deps-test); echo "N_SAME=$N_SAME" >> tmp/testdata.env
|
|
848
|
+
blockedby orch-test-a $N_SAME orch-test-a $A_BLOCKER
|
|
849
|
+
gh issue edit $N_SAME -R $(repo orch-test-a) --add-label ready-for-coding
|
|
850
|
+
post /scheduler/resume >/dev/null; sleep 25
|
|
851
|
+
view dependencies
|
|
852
|
+
probe deps test_a $N_SAME
|
|
853
|
+
ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_SAME/dependencies/blocked_by" | jq '[.[] | {number, state, repository_url}]'
|
|
854
|
+
ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_SAME" | jq .issue_dependencies_summary
|
|
855
|
+
diag coder-test | grep "test_a:$N_SAME"
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
**Erwartet:**
|
|
859
|
+
- View/`deps`: `status ok`, `blocked_by_total 1`, `blocked_by_open 1`, `blocked true`.
|
|
860
|
+
- Die Rohliste enthält genau den Blocker mit `state: "open"` und `repository_url` `…/repos/<owner>/orch-test-a`.
|
|
861
|
+
- Summary: `{"blocked_by": 1, "total_blocked_by": 1, …}` (A5: zuerst nur die Gesamtzahl).
|
|
862
|
+
- `coder-test`: `filtered` (Trace `dependencies.blocked_by_open` = 1, erwartet 0).
|
|
863
|
+
- Gegenprobe A5: `gh issue close $A_BLOCKER -R $(repo orch-test-a)`. Danach ist die Summary `blocked_by: 0`,
|
|
864
|
+
`total_blocked_by: 1`, und der nächste Poll startet `$N_SAME`. Den Blocker **wieder öffnen**
|
|
865
|
+
(`gh issue reopen $A_BLOCKER -R $(repo orch-test-a)`), er wird später nicht mehr gebraucht.
|
|
866
|
+
|
|
867
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
868
|
+
|
|
869
|
+
### T12 – Cross-Repo-Dependency mit Zugriff (A3)
|
|
870
|
+
|
|
871
|
+
**Schritte:**
|
|
872
|
+
```bash
|
|
873
|
+
post /scheduler/pause >/dev/null
|
|
874
|
+
N_CROSS=$(mkissue orch-test-a "T12/T14 blocked by orch-test-b" deps-test); echo "N_CROSS=$N_CROSS" >> tmp/testdata.env
|
|
875
|
+
blockedby_api orch-test-a $N_CROSS orch-test-b $B_BLOCKER # bewusst über REST (issue_id), prüft auch den POST
|
|
876
|
+
gh issue edit $N_CROSS -R $(repo orch-test-a) --add-label ready-for-coding
|
|
877
|
+
post /scheduler/resume >/dev/null; sleep 25
|
|
878
|
+
view dependencies | grep -E "Nr|$N_CROSS"
|
|
879
|
+
ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_CROSS/dependencies/blocked_by" | jq '[.[] | {number, state, repository_url, repo: .repository.full_name}]'
|
|
880
|
+
diag coder-test | grep "test_a:$N_CROSS"
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
**Erwartet:**
|
|
884
|
+
- `blockedby_api` gibt `blocked_by added: #<nr>` aus (HTTP 201; `gh api` zeigt den Status nicht). Laut Doku ist die
|
|
885
|
+
Antwort ein Issue-Objekt: notieren, ob `<nr>` das blockierte Issue (`$N_CROSS`) oder der Blocker (`$B_BLOCKER`) ist.
|
|
886
|
+
- View: `ok / 1 / 1 / true`.
|
|
887
|
+
- Die Rohliste enthält den Blocker aus `orch-test-b` (`repository_url …/orch-test-b`).
|
|
888
|
+
- `coder-test`: `filtered`.
|
|
889
|
+
|
|
890
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
891
|
+
|
|
892
|
+
### T13 – Cross-Repo-Dependency **ohne** Zugriff → `unknown` (A4, A6, §20.7 fail-closed)
|
|
893
|
+
|
|
894
|
+
**Schritte:**
|
|
895
|
+
```bash
|
|
896
|
+
post /scheduler/pause >/dev/null
|
|
897
|
+
N_HID=$(mkissue orch-test-a "T13 blocked by hidden repo" deps-test); echo "N_HID=$N_HID" >> tmp/testdata.env
|
|
898
|
+
blockedby orch-test-a $N_HID orch-test-hidden $H_BLOCKER # als Mensch, der alle Repos sieht
|
|
899
|
+
gh issue edit $N_HID -R $(repo orch-test-a) --add-label ready-for-coding
|
|
900
|
+
gh issue view $N_HID -R $(repo orch-test-a) --json blockedBy --jq '.blockedBy' # Mensch sieht den Blocker
|
|
901
|
+
post /scheduler/resume >/dev/null; sleep 25
|
|
902
|
+
view dependencies | grep -E "Nr|$N_HID"
|
|
903
|
+
probe deps test_a $N_HID
|
|
904
|
+
ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_HID/dependencies/blocked_by" -w '\nHTTP %{http_code}\n'
|
|
905
|
+
ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_HID" | jq .issue_dependencies_summary
|
|
906
|
+
diag coder-test | grep "test_a:$N_HID"
|
|
907
|
+
gh issue close $H_BLOCKER -R $(repo orch-test-hidden); sleep 25
|
|
908
|
+
view dependencies | grep -E "Nr|$N_HID"; diag coder-test | grep "test_a:$N_HID"
|
|
909
|
+
gh issue reopen $H_BLOCKER -R $(repo orch-test-hidden)
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
**Erwartet:**
|
|
913
|
+
- Der Mensch sieht den Blocker (`blockedBy` enthält `orch-test-hidden#…`).
|
|
914
|
+
- App-Rohantwort, **festhalten**, eine der folgenden Varianten:
|
|
915
|
+
- (a) HTTP 200 `[]` und Summary `total_blocked_by: 1` (A4 wie angenommen),
|
|
916
|
+
- (b) 403/404,
|
|
917
|
+
- (c) 200 mit dem Blocker trotz fehlender Installation.
|
|
918
|
+
- Bei (a) und (b):
|
|
919
|
+
- View: Status `unknown`, die anderen Spalten `?`.
|
|
920
|
+
- `deps`: `blocked_by_*` mit `known: false` und Grund `… not visible to identity …` bzw. `dependencies unavailable …`.
|
|
921
|
+
- `coder-test`: `filter_unknown`, **kein Start**.
|
|
922
|
+
- Auch nach dem Schließen des versteckten Blockers bleibt der Status `unknown`. Die App kann den Zustand nicht sehen,
|
|
923
|
+
bleibt also fail-closed.
|
|
924
|
+
- Bei (c) ist A4 falsch: Das private Repo wäre über die Abhängigkeit lesbar. Unbedingt melden.
|
|
925
|
+
|
|
926
|
+
**Ist:** ☐ OK ☐ Abweichung: …… (Variante: ☐ a ☐ b ☐ c; Rohantwort ins Protokoll)
|
|
927
|
+
|
|
928
|
+
### T14 – Blocker schließen → nächster Poll startet (Szenario 3)
|
|
929
|
+
|
|
930
|
+
**Schritte:** Fortsetzung von T12, Blocker ist offen:
|
|
931
|
+
```bash
|
|
932
|
+
diag coder-test | grep "test_a:$N_CROSS" # filtered
|
|
933
|
+
gh issue close $B_BLOCKER -R $(repo orch-test-b); date -u +%T
|
|
934
|
+
for i in 1 2 3 4; do sleep 10; date -u +%T; echo "run: $(run_of test_a $N_CROSS)"; done # leer, bis gestartet
|
|
935
|
+
ghraw maintainer "/repos/$(repo orch-test-a)/issues/$N_CROSS" | jq .issue_dependencies_summary
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
**Erwartet:**
|
|
939
|
+
- Bis zum nächsten Planungsschritt gibt es keinen Run (das Issue bleibt `filtered`); ein früherer Start ist nicht
|
|
940
|
+
möglich. Danach liefert `run_of` eine Run-ID (≤ 20 s + Schrittdauer nach dem Schließen; Zeitstempel vergleichen).
|
|
941
|
+
- Der Run ist `completed` / `success`.
|
|
942
|
+
- Summary: `blocked_by: 0`, `total_blocked_by: 1` (A5).
|
|
943
|
+
|
|
944
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
945
|
+
|
|
946
|
+
### T15 – Zwei parallele Coder mit gleicher Identität, dritter durch Limit verhindert (Szenario 1)
|
|
947
|
+
|
|
948
|
+
**Schritte:**
|
|
949
|
+
```bash
|
|
950
|
+
post /scheduler/pause >/dev/null
|
|
951
|
+
P1=$(mkissue orch-test-a "T15 parallel 1" ready-for-coding test:slow)
|
|
952
|
+
P2=$(mkissue orch-test-a "T15 parallel 2" ready-for-coding test:slow)
|
|
953
|
+
P3=$(mkissue orch-test-a "T15 parallel 3" ready-for-coding test:slow)
|
|
954
|
+
post /scheduler/resume >/dev/null; sleep 15
|
|
955
|
+
api /status | jq '.usage.byWorkflow["coder-test"]'
|
|
956
|
+
for n in $P1 $P2 $P3; do echo "#$n run: $(run_of test_a $n)"; done
|
|
957
|
+
diag coder-test | grep "test_a:$P3" # solange 2 aktiv: workflow_limit
|
|
958
|
+
for n in $P1 $P2; do api /runs/$(run_of test_a $n) | jq -c '{id, identity, workspacePath, status}'; done
|
|
959
|
+
sleep 100; echo "#$P3 run: $(run_of test_a $P3)"
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
**Erwartet:**
|
|
963
|
+
- `byWorkflow` ist `{"active": 2, "max": 2}`. `$P1` und `$P2` haben einen Run (älteste zuerst), `$P3` keinen.
|
|
964
|
+
`diag` zeigt `$P3` als `workflow_limit` (`workflow limit reached (2/2 active)`); die beiden gestarteten Items
|
|
965
|
+
fehlen dort ab dem nächsten Schritt (Label `coding`), deshalb Starts über `run_of` prüfen.
|
|
966
|
+
- Beide Runs haben `identity: coder` und **verschiedene** `workspacePath`.
|
|
967
|
+
- Im Log gibt es **keine** zusätzliche `Obtained … coder`-Zeile für den zweiten Run (gemeinsamer Token-Cache).
|
|
968
|
+
- Nach Ende eines Runs startet `$P3` beim nächsten Poll (`run_of` liefert dann eine ID).
|
|
969
|
+
|
|
970
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
971
|
+
|
|
972
|
+
### T16 – Manueller Start per URL, Lock-Konflikt (Szenario 2)
|
|
973
|
+
|
|
974
|
+
**Schritte:**
|
|
975
|
+
```bash
|
|
976
|
+
M=$(mkissue orch-test-a "T16 manuell, ohne ready-Label" test:slow)
|
|
977
|
+
post /workflows/coder-test/start "{\"item\":\"$(issue_url orch-test-a $M)\"}" | jq '{run: .run.id, trigger: .run.triggerType, status: .run.status}'
|
|
978
|
+
post /workflows/coder-test/start "{\"item\":\"$(issue_url orch-test-a $M)\"}" | jq .
|
|
979
|
+
post /workflows/coder-test/start "{\"item\":\"#$M\"}" | jq .error.code
|
|
980
|
+
post /workflows/coder-test/start "{\"item\":\"https://github.com/$ADISPATCH_TEST_OWNER/orch-test-b/issues/1\"}" | jq .error.code
|
|
981
|
+
post /workflows/coder-test/start "{\"item\":\"$(issue_url orch-test-a $N_HID)\"}" | jq '{run: .run.id, error: .error.code}'
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
**Erwartet:**
|
|
985
|
+
- Der 1. Start gibt `202` mit `triggerType: manual`, obwohl `ready-for-coding` fehlt (Filter wird umgangen).
|
|
986
|
+
- Der 2. und 3. Start geben `409`, `error.code: lock_conflict`, `details.holderRunId` = Run des 1. Starts.
|
|
987
|
+
- Eine fremde Repo-URL gibt `400 manual_item_invalid_ref`.
|
|
988
|
+
- `$N_HID` (Dependency `unknown`) startet manuell (`202`), weil Filter umgangen werden. Danach erscheinen beide Runs in
|
|
989
|
+
`runs coder-test` mit `manual`.
|
|
990
|
+
- Der manuelle Run zählt zur Auslastung: `api /status | jq .usage`.
|
|
991
|
+
|
|
992
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
993
|
+
|
|
994
|
+
### T17 – Drei Crashes → Retry-Block → Remove Block (Szenario 5)
|
|
995
|
+
|
|
996
|
+
**Schritte:**
|
|
997
|
+
```bash
|
|
998
|
+
C=$(mkissue orch-test-a "T17 crash" ready-for-coding test:crash); echo "C=$C" >> tmp/testdata.env
|
|
999
|
+
sleep 90; runs coder-test 10 | grep "test_a:$C" # noch keine 3 crashed? 20 s warten, wiederholen
|
|
1000
|
+
api /blocks | jq '.blocks[] | {id, workflowId, itemKey, reason, failureCount, affectedRunIds}'
|
|
1001
|
+
labels orch-test-a $C; sleep 25; diag coder-test | grep "test_a:$C"
|
|
1002
|
+
api '/notifications?limit=5' | jq -r '.notifications[] | .eventType' | sort | uniq -c
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
Nach dem Block das Crash-Label entfernen und den Block aufheben:
|
|
1006
|
+
```bash
|
|
1007
|
+
gh issue edit $C -R $(repo orch-test-a) --remove-label test:crash
|
|
1008
|
+
BID=$(api /blocks | jq -r ".blocks[] | select(.itemKey==\"test_a:$C\") | .id")
|
|
1009
|
+
post /blocks/$BID/clear '{"clearedBy":"manual-test"}' | jq '.block | {active, clearedBy}'
|
|
1010
|
+
sleep 15; runs coder-test 10 | grep "test_a:$C"; labels orch-test-a $C
|
|
1011
|
+
api "/runs?item=test_a:$C" | jq '.total'
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
**Erwartet:**
|
|
1015
|
+
- Drei Runs `crashed` (`finalReason` mit Exit-Code 1). Die Ergebnisdatei mit `success` wird ignoriert.
|
|
1016
|
+
- Nach jedem Crash setzt `on_crash` `ready-for-coding` wieder. Der nächste Poll startet erneut, bis zum 3. Crash.
|
|
1017
|
+
- Danach entsteht genau ein aktiver Block (`failureCount: 3`, 3 `affectedRunIds`) und eine Notification
|
|
1018
|
+
`retry_block_created`.
|
|
1019
|
+
- Das Label `ready-for-coding` bleibt stehen, und `diag` zeigt `blocked` (`retry block <id>: …`). Es gibt keinen
|
|
1020
|
+
4. Start.
|
|
1021
|
+
- Nach `clear` folgt sofort ein Planungsschritt. Der neue Run ist `completed` / `success`, die Labels werden
|
|
1022
|
+
`ready-for-review`.
|
|
1023
|
+
- `total` = 4: Die Historie bleibt vollständig.
|
|
1024
|
+
|
|
1025
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
1026
|
+
|
|
1027
|
+
### T18 – Ungültige Ergebnisdatei und Worker-Token (Worker-Protokoll, `execute.secrets`)
|
|
1028
|
+
|
|
1029
|
+
**Schritte:**
|
|
1030
|
+
```bash
|
|
1031
|
+
I=$(mkissue orch-test-a "T18 invalid result" ready-for-coding test:invalid-result)
|
|
1032
|
+
T=$(mkissue orch-test-a "T18 token" ready-for-coding test:token)
|
|
1033
|
+
sleep 45; for n in $I $T; do api /runs/$(run_of test_a $n) | jq -c '{status, outcome, finalReason}'; done
|
|
1034
|
+
api "/runs/$(run_of test_a $T)/logs?stream=stdout" | jq -r .data | grep 'HTTP'
|
|
1035
|
+
gh issue edit $I -R $(repo orch-test-a) --remove-label ready-for-coding # sonst Crash-Schleife bis Block
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
**Erwartet:**
|
|
1039
|
+
- `$I`: `crashed` mit Grund „ungültige Ergebnisdatei“/Protokollfehler, `on_crash` lief.
|
|
1040
|
+
- `$T`: `completed` / `success`. Das Log zeigt `GET issue with GH_TOKEN → HTTP 200`, das Token selbst taucht nicht auf.
|
|
1041
|
+
|
|
1042
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
1043
|
+
|
|
1044
|
+
### T19 – Fehlende Rechte/Zugänge → sichtbarer Fehler statt stiller Annahme (§20.7, A6, A7)
|
|
1045
|
+
|
|
1046
|
+
Nach jedem Teil den Ausgangszustand wiederherstellen. Installationsänderungen wirken auf **neue** Tokens. Damit die
|
|
1047
|
+
laufende Instanz sie sofort sieht, die Instanz neu starten (Drain), sonst bis zur Token-Erneuerung warten.
|
|
1048
|
+
|
|
1049
|
+
**T19a – Source-Identity verliert ein Repo.** Bei der Maintainer-App unter *Configure → Repository access* `orch-test-b`
|
|
1050
|
+
entfernen, dann:
|
|
1051
|
+
```bash
|
|
1052
|
+
sleep 25; diag commenter-test | head -1
|
|
1053
|
+
view needs-maintainer
|
|
1054
|
+
orch workflow dry-run commenter-test --config-dir "$ADISPATCH_CFG" --local --data-dir "$ADISPATCH_DATA"; echo "exit=$?"
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
Erwartet:
|
|
1058
|
+
- `status=source_error`, `error` z. B. `GET /repos/<owner>/orch-test-b/issues failed with HTTP 404: Not Found`. Kein
|
|
1059
|
+
Start, und andere Workflows laufen weiter.
|
|
1060
|
+
- Die View zeigt die Zeilen aus `test_a` plus `ERROR test_b: …`.
|
|
1061
|
+
- Der Dry-Run meldet `SOURCE ERROR`, Exit 1.
|
|
1062
|
+
- Danach `orch-test-b` wieder hinzufügen.
|
|
1063
|
+
|
|
1064
|
+
**T19b – Schreib-Identity verliert ein Repo.** Bei der Coder-App `orch-test-a` aus dem Repository access entfernen,
|
|
1065
|
+
Instanz neu starten:
|
|
1066
|
+
```bash
|
|
1067
|
+
post /scheduler/pause >/dev/null
|
|
1068
|
+
W=$(mkissue orch-test-a "T19b ohne Schreibzugriff" ready-for-coding)
|
|
1069
|
+
post /scheduler/resume >/dev/null; sleep 30; RUN=$(run_of test_a $W)
|
|
1070
|
+
api /runs/$RUN | jq '{status, finalReason, lifecycleFailed, countedTechnicalFailure}'; actions $RUN
|
|
1071
|
+
post /scheduler/pause >/dev/null; labels orch-test-a $W
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
Erwartet:
|
|
1075
|
+
- Der Run ist `start_error` mit `finalReason` `on_start action 'on_start-labels' failed`. Der Fehler der Action
|
|
1076
|
+
(`actions $RUN`, letzte Spalte) lautet `Item <owner>/orch-test-a#<nr> not found`: Das Neu-Lesen in `applyUpdate`
|
|
1077
|
+
mit der Coder-Identity bekommt 404 und wertet es als „nicht gefunden“ (`fetchIssue` → `null`). Bei 403 stünde dort
|
|
1078
|
+
`… HTTP 403 …`. Der Worker wurde **nicht**
|
|
1079
|
+
gestartet.
|
|
1080
|
+
- `on_start-labels` ist `failed`, und `on_start_error-labels` ist ebenfalls `failed` (gleiche Ursache).
|
|
1081
|
+
- `lifecycleFailed: true`, und eine Notification `lifecycle_action_failed` entsteht.
|
|
1082
|
+
- Die Labels bleiben unverändert (`ready-for-coding`). Ohne Pause würde der nächste Poll erneut starten, bis nach 3
|
|
1083
|
+
`start_error` ein Block entsteht. Das ist korrekt (Spec §11), aber zu beobachten.
|
|
1084
|
+
- Danach den Zugriff wiederherstellen, `resume`, und ggf. den Block aufheben.
|
|
1085
|
+
|
|
1086
|
+
**T19c – fehlender Key.** Instanz stoppen, `unset ADISPATCH_TEST_MAINTAINER_KEY`, dann:
|
|
1087
|
+
```bash
|
|
1088
|
+
orch workflow dry-run coder-test --config-dir "$ADISPATCH_CFG" --local --data-dir "$ADISPATCH_DATA"
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
Erwartet: `SOURCE ERROR: … Private key for identity "maintainer" missing: environment variable ADISPATCH_TEST_MAINTAINER_KEY is not set`.
|
|
1092
|
+
Mit einem kaputten Key (`export ADISPATCH_TEST_MAINTAINER_KEY=kaputt`) lautet die Meldung `… is not a valid RSA PEM key`.
|
|
1093
|
+
Keine der Ausgaben enthält Key-Material. Danach den Key wieder setzen.
|
|
1094
|
+
|
|
1095
|
+
**T19d – Rechte-Granularität (A7, [R4](#r4)).**
|
|
1096
|
+
1. Coder-App *Permissions & events*: *Pull requests* auf **No access** setzen und speichern. Die Installation übernimmt
|
|
1097
|
+
Rechte-**Reduktionen** in der Regel sofort; ob eine Bestätigung nötig ist, notieren.
|
|
1098
|
+
2. Dann:
|
|
1099
|
+
```bash
|
|
1100
|
+
probe labels test_a $A_PR coder +perm-test # Label an PR nur mit Issues: write
|
|
1101
|
+
probe comment test_a $A_PR coder adispatchd:perm-pr:1 'PR-Kommentar nur mit Issues: write'
|
|
1102
|
+
```
|
|
1103
|
+
3. *Pull requests* wieder auf **Read and write** und *Issues* auf **Read-only** setzen. Die Erhöhung muss in der
|
|
1104
|
+
Installation bestätigt werden (*Review request* bzw. *Configure → Accept new permissions*). Dann:
|
|
1105
|
+
```bash
|
|
1106
|
+
N=$(mkissue orch-test-a "T19d")
|
|
1107
|
+
probe labels test_a $N coder +perm-test # Label an Issue nur mit Pull requests: write
|
|
1108
|
+
ghraw coder "/repos/$(repo orch-test-a)/issues/$N/labels" -X POST -d '{"labels":["perm-test"]}' -D - -o /dev/null | grep -i -E '^HTTP|x-accepted-github-permissions'
|
|
1109
|
+
probe labels test_a $A_PR coder +perm-test2 # Label an PR nur mit Pull requests: write
|
|
1110
|
+
```
|
|
1111
|
+
4. Ursprüngliche Rechte wiederherstellen (Issues und Pull requests: Read and write) und bestätigen.
|
|
1112
|
+
|
|
1113
|
+
Erwartet (laut Doku): Jede der Aktionen gelingt mit **einem** der beiden Rechte. Die Ergebnisse festhalten:
|
|
1114
|
+
|
|
1115
|
+
| Ziel | nur Issues: write | nur Pull requests: write |
|
|
1116
|
+
|---|---|---|
|
|
1117
|
+
| Label an Issue | ✔ (Basis) | ☐ ✔ ☐ ✘ |
|
|
1118
|
+
| Label an PR | ☐ ✔ ☐ ✘ | ☐ ✔ ☐ ✘ |
|
|
1119
|
+
| Kommentar an PR | ☐ ✔ ☐ ✘ | – |
|
|
1120
|
+
|
|
1121
|
+
Ein ✘ zeigt sich als `forbidden`, HTTP 403 `Resource not accessible by integration`, und
|
|
1122
|
+
`x-accepted-github-permissions` nennt das nötige Recht. Ergebnis → Rechte-Tabelle 1.3, `README.md` und
|
|
1123
|
+
`src/cli/init-templates.ts` (Kommentar in `identities.yaml`) anpassen.
|
|
1124
|
+
|
|
1125
|
+
**Ist:** a ☐ b ☐ c ☐ d ☐ OK ☐ Abweichung: ……
|
|
1126
|
+
|
|
1127
|
+
### T20 – Dry-Run gegen echte Repos (§19)
|
|
1128
|
+
|
|
1129
|
+
**Schritte:**
|
|
1130
|
+
```bash
|
|
1131
|
+
post /scheduler/pause >/dev/null
|
|
1132
|
+
D=$(mkissue orch-test-a "T20 dry-run" ready-for-coding 'priority:high')
|
|
1133
|
+
# frische blockierte Kandidaten (die aus T11–T13 sind inzwischen gestartet/umgelabelt); Blocker A/H sind offen
|
|
1134
|
+
D_SAME=$(mkissue orch-test-a "T20 blocked same repo"); blockedby orch-test-a $D_SAME orch-test-a $A_BLOCKER
|
|
1135
|
+
D_HID=$(mkissue orch-test-a "T20 blocked hidden"); blockedby orch-test-a $D_HID orch-test-hidden $H_BLOCKER
|
|
1136
|
+
for n in $D_SAME $D_HID; do gh issue edit $n -R "$(repo orch-test-a)" --add-label ready-for-coding; done
|
|
1137
|
+
before_labels=$(labels orch-test-a $D); before_runs=$(api /runs | jq .total)
|
|
1138
|
+
orch workflow dry-run coder-test --config-dir "$ADISPATCH_CFG" # über die laufende Instanz (HTTP)
|
|
1139
|
+
orch workflow dry-run coder-test --config-dir "$ADISPATCH_CFG" --local --data-dir "$ADISPATCH_DATA" --json | jq '.candidates[] | {itemKey, decision, itemPriority, reason}'
|
|
1140
|
+
orch workflow dry-run reviewer-test --config-dir "$ADISPATCH_CFG"
|
|
1141
|
+
[ "$(labels orch-test-a $D)" = "$before_labels" ] && [ "$(api /runs | jq .total)" = "$before_runs" ] && echo "unverändert"
|
|
1142
|
+
post /scheduler/resume >/dev/null
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
**Erwartet:**
|
|
1146
|
+
- Über HTTP: Gate `paused`, geeignete Kandidaten `gate_closed`. Lokal: `test_a:$D` ist `would_start` mit
|
|
1147
|
+
`itemPriority: 50` und steht vor anderen Kandidaten.
|
|
1148
|
+
- `$D_HID` ist `filter_unknown`, `$D_SAME` ist `filtered` (Trace `dependencies.blocked_by_open` = 1).
|
|
1149
|
+
Nach `resume` bleiben beide ohne Run; zum Aufräumen `ready-for-coding` wieder entfernen.
|
|
1150
|
+
- Blockierte Items erscheinen als `blocked`, aktive als `locked`.
|
|
1151
|
+
- Labels, Kommentare und Runs bleiben unverändert (`unverändert`).
|
|
1152
|
+
- Die Notes nennen „shell filters and conditions are never executed“.
|
|
1153
|
+
|
|
1154
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
1155
|
+
|
|
1156
|
+
### T21 – View über zwei Repos (Szenario 10)
|
|
1157
|
+
|
|
1158
|
+
**Schritte:**
|
|
1159
|
+
```bash
|
|
1160
|
+
F=$(mkissue orch-test-b "T21 failure in repo b" ready-for-comment test:failure)
|
|
1161
|
+
sleep 45; view needs-maintainer
|
|
1162
|
+
api /views/needs-maintainer/items | jq '{cached, rows: [.rows[] | {key, url, lastRunId}], errors}'
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
Zusätzlich im Browser *Views → Needs Maintainer* öffnen.
|
|
1166
|
+
|
|
1167
|
+
**Erwartet:**
|
|
1168
|
+
- Es gibt Zeilen aus **beiden** Repos: `test_a:$N_FAIL` (T9) und `test_b:$F`. Die Spalten „Letzter Agent“ zeigen
|
|
1169
|
+
`coder-test` bzw. `commenter-test`, „Ergebnis“ die `failure`-Nachricht, „Status“ `completed`.
|
|
1170
|
+
- Außerdem erscheint `test_a:$N_T5` (T5): Das Label `needs-maintainer` wurde dort manuell gesetzt, das Issue hat
|
|
1171
|
+
**keinen** Run. Die Run-Spalten sind leer (`null`, nicht `?`), sortiert nach „Aktualisiert“. Weitere Zeilen nur für
|
|
1172
|
+
Items, die in anderen Fällen ebenfalls `needs-maintainer` bekamen.
|
|
1173
|
+
- Der Titel ist ein klickbarer Link auf das GitHub-Issue.
|
|
1174
|
+
- `errors: []`.
|
|
1175
|
+
- Ein zweiter Aufruf ohne `refresh` innerhalb von 30 s liefert `cached: true`.
|
|
1176
|
+
|
|
1177
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
1178
|
+
|
|
1179
|
+
### T22 – Rate-Limit beobachten (A8, [R8](#r8))
|
|
1180
|
+
|
|
1181
|
+
**Schritte:**
|
|
1182
|
+
```bash
|
|
1183
|
+
for i in maintainer coder; do echo "== $i"; ghraw $i /rate_limit | jq -c '.resources.core, (has("rate"))'; done
|
|
1184
|
+
ghraw maintainer /rate_limit -D - -o /dev/null | grep -i '^x-ratelimit'
|
|
1185
|
+
u1=$(ghraw maintainer /rate_limit | jq .resources.core.used); sleep 600
|
|
1186
|
+
u2=$(ghraw maintainer /rate_limit | jq .resources.core.used); echo "maintainer: $((u2 - u1)) Requests in 10 min"
|
|
1187
|
+
grep -E '"kind":"rate_limited"|retrying' tmp/orch.log | tail
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
**Erwartet:**
|
|
1191
|
+
- `resources.core.limit` ≥ 5000. `has("rate")` ist `false` (API 2026-03-10).
|
|
1192
|
+
- Die Header `x-ratelimit-limit|remaining|used|reset|resource` sind vorhanden.
|
|
1193
|
+
- Verbrauch in 10 min ≈ 30 Polls × (Listing `coder-test` + Listing `reviewer-test` + Listing `commenter-test` +
|
|
1194
|
+
2 × Anzahl Kandidaten mit Dependency-Filter, deren günstiger Filter passt) plus View-Aufrufe. Bei rund 3 Kandidaten
|
|
1195
|
+
sind das etwa 300. Der Wert muss weit unter 5000/h bleiben. Messwert festhalten.
|
|
1196
|
+
- Im Log stehen keine `rate_limited`-Einträge.
|
|
1197
|
+
|
|
1198
|
+
Ein echtes Rate-Limit wird **nicht** provoziert, weil das sekundäre Limit den Test-Account sperren kann. Die Behandlung
|
|
1199
|
+
von 403/429, `retry-after` und `x-ratelimit-reset` ist in `http.test.ts` gegen Fake-Antworten geprüft. Tritt ein Limit
|
|
1200
|
+
zufällig auf, Header und Logzeile festhalten.
|
|
1201
|
+
|
|
1202
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
1203
|
+
|
|
1204
|
+
### T23 – Transferiertes Issue (A10, optional)
|
|
1205
|
+
|
|
1206
|
+
**Schritte:**
|
|
1207
|
+
```bash
|
|
1208
|
+
X=$(mkissue orch-test-a "T23 transfer")
|
|
1209
|
+
gh issue transfer "$(issue_url orch-test-a $X)" "$(repo orch-test-b)" # URL statt Nummer: nie das Repo im cwd
|
|
1210
|
+
ghraw maintainer "/repos/$(repo orch-test-a)/issues/$X" -o /dev/null -w 'HTTP %{http_code} → %{redirect_url}\n'
|
|
1211
|
+
probe item test_a $X; echo "exit=$?"
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
**Erwartet:** Der Rohaufruf gibt `HTTP 301` mit einer Ziel-URL im neuen Repo. `probe item` meldet `item … not found`,
|
|
1215
|
+
weil `getItem` = `null` (fetch folgt dem Redirect, `repository_url` passt nicht).
|
|
1216
|
+
|
|
1217
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
1218
|
+
|
|
1219
|
+
### T24 – Keine Secrets in Logs und Daten (§20.4)
|
|
1220
|
+
|
|
1221
|
+
**Schritte:** nach allen anderen Fällen.
|
|
1222
|
+
```bash
|
|
1223
|
+
PAT='ghs_[A-Za-z0-9_]{10,}|BEGIN RSA PRIVATE KEY|BEGIN PRIVATE KEY|eyJhbGciOi'
|
|
1224
|
+
grep -rlaE "$PAT" tmp/orch.log "$ADISPATCH_DATA" && echo "TREFFER!" || echo "sauber" # -a: auch SQLite/WAL (binär)
|
|
1225
|
+
command -v sqlite3 >/dev/null && sqlite3 -readonly "$ADISPATCH_DATA/adispatchd.sqlite" .dump | grep -cE "$PAT"
|
|
1226
|
+
api /config/files | grep -c 'PRIVATE KEY'
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
**Erwartet:** `sauber`, der `sqlite3`-Dump ergibt `0` (falls `sqlite3` installiert) und `/config/files` ebenfalls `0`. Ein Treffer ist ein Fehler: die Datei notieren, **nicht** einchecken, den Key rotieren.
|
|
1230
|
+
|
|
1231
|
+
**Ist:** ☐ OK ☐ Abweichung: ……
|
|
1232
|
+
|
|
1233
|
+
---
|
|
1234
|
+
|
|
1235
|
+
## 5 Auswertung und Aufräumen
|
|
1236
|
+
|
|
1237
|
+
### 5.1 Ergebnisse festhalten
|
|
1238
|
+
|
|
1239
|
+
Je Testlauf eine Zeile pro Fall. Bei Abweichungen gehören die Rohantworten (`ghraw …`) und die passenden Logzeilen
|
|
1240
|
+
(`grep <run-id> tmp/orch.log`) dazu. Tokens vorher unkenntlich machen. Bleibt dieses Dokument im Repo, landen die
|
|
1241
|
+
Ergebnisse in einem Issue oder PR.
|
|
1242
|
+
|
|
1243
|
+
| Datum | Commit | API-Version | gh-Version | Fall | Ergebnis | Beobachtung / Rohantwort |
|
|
1244
|
+
|---|---|---|---|---|---|---|
|
|
1245
|
+
| | | 2026-03-10 | | T1 | | |
|
|
1246
|
+
|
|
1247
|
+
Zusätzlich die offenen Fragen beantworten:
|
|
1248
|
+
|
|
1249
|
+
| Frage | Testfall | Antwort |
|
|
1250
|
+
|---|---|---|
|
|
1251
|
+
| Blocker ohne Zugriff: Variante a/b/c? Summary zählt sie mit? | T13 | |
|
|
1252
|
+
| `issue_dependencies_summary.blocked_by` = offene Blocker? | T11, T14 | |
|
|
1253
|
+
| Dependencies an PRs: 200 oder 404/403? | T10 | |
|
|
1254
|
+
| Labels/Kommentare an PRs nur mit *Issues: write*? | T19d | |
|
|
1255
|
+
| `user.type = Bot`, `performed_via_github_app.id = App-ID`? | T6 | |
|
|
1256
|
+
| POST `blocked_by` gibt welches Issue zurück? | T12 | |
|
|
1257
|
+
| Reduktion von App-Rechten sofort wirksam? | T19d | |
|
|
1258
|
+
|
|
1259
|
+
### 5.2 Bei Abweichungen
|
|
1260
|
+
|
|
1261
|
+
Vorgehen nach `docs/conventions.md`:
|
|
1262
|
+
1. `FakeGitHub` an das echte Verhalten anpassen.
|
|
1263
|
+
2. Einen fehlschlagenden Test schreiben.
|
|
1264
|
+
3. Den Code korrigieren und `npm run check` laufen lassen.
|
|
1265
|
+
4. Diese Anleitung und die Annahmen-Tabelle aktualisieren.
|
|
1266
|
+
|
|
1267
|
+
| Abweichung | Betroffene Stellen |
|
|
1268
|
+
|---|---|
|
|
1269
|
+
| A1 API-Version abgelehnt/veraltet | `GITHUB_API_VERSION` (`src/sources/github/http.ts`). `GH_API_VERSION` in `github-fixture/helpers.sh` und `scripts/test-worker.sh` mitziehen, `manual-fixture.test.ts` erzwingt das |
|
|
1270
|
+
| A2/A3 andere Form der Blocker-Liste, Pagination | `resolveDependencies` (`src/sources/github/adapter.ts`), `FakeGitHub` Dependencies (`src/sources/github/testing/`) |
|
|
1271
|
+
| A4 Variante b (403/404) | nichts nötig, fail-closed über den `catch`. Nur Fake ergänzen (`denyDependencies`) |
|
|
1272
|
+
| A4 Variante c (Blocker trotz fehlendem Zugriff sichtbar) | sicherheitsrelevant: als GitHub-Verhalten dokumentieren. Der Adapter bleibt korrekt (zählt), aber Spec §20.7 und README ergänzen |
|
|
1273
|
+
| A4 Summary zählt unsichtbare Blocker **nicht** | **kritisch**: fail-closed greift nicht, `blocked_by_open` wäre 0 → Start trotz Blocker. Mit App-Rechten ist das nicht erkennbar. Sofort dokumentieren (README, Spec §6/§20.7), dass alle Blocker-Repos für die Source-Identity installiert sein **müssen**. Code-Entscheidung als eigenes Work-Item: z. B. Option, Cross-Repo-Abhängigkeiten nur aus konfigurierten Sources zu akzeptieren |
|
|
1274
|
+
| A5 Semantik vertauscht | nur relevant, falls der Code `blocked_by` nutzt (derzeit nicht) |
|
|
1275
|
+
| A6 anderer Status bei fehlendem Recht | `GitHubHttpClient.toError` (Klassifikation). Für Dependencies unkritisch, jeder Fehler → `unknown` |
|
|
1276
|
+
| A7 PR-Rechte | Doku: Tabelle 1.3, `README.md` („Rechte: …“), Kommentar in `src/cli/init-templates.ts` (`IDENTITIES_YAML`) |
|
|
1277
|
+
| A8 Rate-Limit-Header anders | `GitHubHttpClient.toError` / `retryAfter`, `http.test.ts` |
|
|
1278
|
+
| A9 Token-Laufzeit/JWT abgelehnt | `src/identities/jwt.ts` (`JWT_*`), `refreshMarginMs` in `github-app.ts`. Bei `iss`-Problemen Client-ID als neues Konfigfeld (`src/config/`, eigenes Work-Item) |
|
|
1279
|
+
| A10 Transfer anders | `fetchIssue` / `sameRepoUrl` in `adapter.ts` |
|
|
1280
|
+
| A11 Autor-Felder anders | `authorPredicate` in `adapter.ts`, `FakeGitHub.addComment` (`viaAppId`, `type`). Ohne `performed_via_github_app` ggf. `user.login === '<slug>[bot]'` über `describe()` (`IdentityDescription`, `src/core/types.ts`) |
|
|
1281
|
+
| PR-Dependencies 404 (T10 b) | Doku: Dependency-Filter nicht für PR-Workflows (`reviewer-test`, README, Beispiel-Configs). Optional: `resolveDependencies` für `type: pull_request` → `ok/0` (Entscheidung nötig: fail-closed vs. „PRs haben keine Blocker“) |
|
|
1282
|
+
| Retry erzeugt Duplikat (T7/T8) | `performSourceComment` (`src/lifecycle/performers.ts`), `findCommentByMarker` / `markerPattern` (`adapter.ts`) |
|
|
1283
|
+
| Sekundäres Rate-Limit bei vielen Hooks | Drosselung schreibender Requests (≥ 1 s Abstand laut [R8](#r8)) in `GitHubHttpClient` fehlt. Als Folge-Work-Item aufnehmen |
|
|
1284
|
+
|
|
1285
|
+
### 5.3 Aufräumen
|
|
1286
|
+
|
|
1287
|
+
```bash
|
|
1288
|
+
# 1. Instanz stoppen (im Start-Terminal Ctrl-C) bzw.:
|
|
1289
|
+
orch drain --config-dir "$ADISPATCH_CFG" --wait
|
|
1290
|
+
# 2. Lokale Testdaten und Probe entfernen
|
|
1291
|
+
rm -rf tmp/orch-cfg tmp/orch-data tmp/orch.log tmp/testdata.env tmp/orch-test.env tmp/probe.ts
|
|
1292
|
+
unset ADISPATCH_TEST_MAINTAINER_KEY ADISPATCH_TEST_CODER_KEY ADISPATCH_TEST_REVIEWER_KEY
|
|
1293
|
+
# 3. Repos löschen (braucht Scope delete_repo)
|
|
1294
|
+
gh auth refresh -s delete_repo
|
|
1295
|
+
: "${ADISPATCH_TEST_OWNER:?ADISPATCH_TEST_OWNER setzen}"; type repo >/dev/null || { echo "helpers.sh nicht geladen"; false; } &&
|
|
1296
|
+
for r in orch-test-a orch-test-b orch-test-hidden; do gh repo delete "$(repo $r)"; done # fragt je Repo nach (Namen prüfen)
|
|
1297
|
+
# 4. Private Keys lokal löschen
|
|
1298
|
+
rm -P ~/orch-test-keys/*.pem 2>/dev/null || shred -u ~/orch-test-keys/*.pem; rmdir ~/orch-test-keys
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
Danach in GitHub für jede App:
|
|
1302
|
+
1. *Settings → Applications → Installed GitHub Apps → Configure → Uninstall*.
|
|
1303
|
+
2. *Developer settings → GitHub Apps → \<App> → Advanced → Delete GitHub App*. Damit werden auch die öffentlichen
|
|
1304
|
+
Schlüssel ungültig.
|
|
1305
|
+
3. Prüfen, dass unter *Settings → Applications* keine `orch-test-*`-Installation übrig ist.
|
|
1306
|
+
|
|
1307
|
+
Soll dasselbe Setup für spätere Läufe bleiben, die Apps nur behalten, wenn die Keys sicher liegen. Zwischen den Läufen
|
|
1308
|
+
reicht es, die Test-Repos neu anzulegen (Abschnitt 1.2/3) und die Installationen auf die neuen Repos zu setzen.
|