133 lines
4.7 KiB
Markdown
133 lines
4.7 KiB
Markdown
# PRD prd-new: Fold bot-bottle-orchestrator into this repo
|
|
|
|
- **Status:** Active
|
|
- **Author:** didericis
|
|
- **Created:** 2026-07-01
|
|
- **Issue:** #321
|
|
|
|
## Summary
|
|
|
|
Move the `bot-bottle-orchestrator` binary into `bot_bottle/orchestrator/` as a
|
|
first-class subpackage. `pip install bot-bottle` gets you everything; the
|
|
orchestrator's entry point becomes `python -m bot_bottle.orchestrator run`. The
|
|
cross-repo CLI contract becomes an internal boundary, and the forge integration
|
|
layer (`GiteaClient`, `ScopedForge`, `SqliteForgeStateStore`) is promoted to
|
|
`bot_bottle/contrib/` where it belongs.
|
|
|
|
## Problem
|
|
|
|
The orchestrator and bot-bottle are tightly coupled:
|
|
- It always deploys on the same host.
|
|
- It imports from `bot_bottle` for the forge/state layer.
|
|
- Its runner shims (`start --headless`, `commit`, `resume`) map 1:1 to CLI
|
|
commands in `cli.py` — a breaking CLI change silently breaks the orchestrator
|
|
with no CI signal.
|
|
- Two repos means two version pins, two CI pipelines, and two install steps
|
|
every time the deploy environment is rebuilt.
|
|
|
|
## Goals / Success Criteria
|
|
|
|
- All orchestrator modules live under `bot_bottle/orchestrator/` and the package
|
|
is importable as `from bot_bottle.orchestrator import ...`.
|
|
- `python -m bot_bottle.orchestrator run` starts the webhook server.
|
|
- `python -m bot_bottle.orchestrator status` prints tracked runs.
|
|
- The forge integration layer (`GiteaClient`, `GiteaForge`, `ScopedForge`,
|
|
`ForgeState`, `SqliteForgeStateStore`) lives in `bot_bottle/contrib/` and is
|
|
covered by tests in `tests/unit/orchestrator/`.
|
|
- All orchestrator unit tests pass under bot-bottle's existing CI
|
|
(`python -m unittest discover -s tests/unit`).
|
|
- No functional change to the orchestrator's external behaviour: same
|
|
HTTP surface, same webhook protocol, same env-var config, same CLI flags.
|
|
|
|
## Non-goals
|
|
|
|
- Replacing `SubprocessBottleRunner` with a direct programmatic runner — the
|
|
subprocess shim stays; the `BottleRunner` protocol remains the internal
|
|
abstraction point.
|
|
- Merging the orchestrator's SQLite DB with any other bot-bottle state store.
|
|
- Archiving `bot-bottle-orchestrator` (that happens after this ships and the
|
|
deploy is updated; out of scope for this PR).
|
|
|
|
## Design
|
|
|
|
### Package layout
|
|
|
|
```
|
|
bot_bottle/
|
|
orchestrator/
|
|
__init__.py
|
|
__main__.py # python -m bot_bottle.orchestrator
|
|
bootstrap.py # wires contrib modules → orchestrator core
|
|
config.py
|
|
events.py
|
|
lifecycle.py
|
|
model.py
|
|
provenance.py
|
|
runner.py
|
|
sidecar.py
|
|
store.py
|
|
targeting.py
|
|
watchdog.py
|
|
webhook.py
|
|
contrib/
|
|
forge/
|
|
__init__.py
|
|
base.py # ScopedForge: read-anywhere / write-scoped wrapper
|
|
gitea/
|
|
client.py # GiteaClient (urllib.request), GiteaForge
|
|
forge_state.py # ForgeState dataclass + SqliteForgeStateStore
|
|
|
|
tests/unit/orchestrator/
|
|
__init__.py
|
|
_fakes.py
|
|
test_config.py
|
|
test_events.py
|
|
test_lifecycle.py
|
|
test_provenance.py
|
|
test_runner.py
|
|
test_sidecar.py
|
|
test_store.py
|
|
test_targeting.py
|
|
test_watchdog.py
|
|
test_webhook.py
|
|
```
|
|
|
|
### Module moves
|
|
|
|
Every `orchestrator/` source file moves verbatim into `bot_bottle/orchestrator/`.
|
|
Internal imports are already relative (`from .config import Config`) so no
|
|
changes are needed inside the orchestrator modules themselves.
|
|
|
|
`bootstrap.py` is the only file that changes meaningfully: the lazy `bot_bottle`
|
|
imports become direct relative imports (`from ..contrib.gitea.client import …`),
|
|
and the `_require_bot_bottle()` guard is removed since the package is always
|
|
present.
|
|
|
|
### New contrib modules
|
|
|
|
**`bot_bottle/contrib/forge/base.py` — `ScopedForge`**
|
|
|
|
Wraps any forge object and enforces read-anywhere / write-scoped access: reads
|
|
pass through unconditionally; `post_comment` and `update_description` raise
|
|
`PermissionError` for issue/PR numbers outside the assigned set.
|
|
|
|
**`bot_bottle/contrib/gitea/client.py` — `GiteaClient`, `GiteaForge`**
|
|
|
|
`GiteaClient` is a thin `urllib.request`-only HTTP wrapper (no new Python
|
|
dependencies). `GiteaForge` composes a client and exposes the forge protocol:
|
|
`is_org_member`, `read_issue`, `read_pr`, `read_comments`, `post_comment`,
|
|
`update_description`.
|
|
|
|
**`bot_bottle/contrib/gitea/forge_state.py` — `ForgeState`, `SqliteForgeStateStore`**
|
|
|
|
`ForgeState` is a dataclass mirroring `RunRecord` field-for-field. `SqliteForgeStateStore`
|
|
backs it with SQLite (stdlib `sqlite3`): a single `forge_state` table with one
|
|
row per (owner, repo, issue\_number).
|
|
|
|
### Test migration
|
|
|
|
All orchestrator test files move to `tests/unit/orchestrator/` with absolute
|
|
imports updated from `orchestrator.X` to `bot_bottle.orchestrator.X`. The unit
|
|
discovery command (`-s tests/unit`) picks them up automatically — no CI changes
|
|
required.
|