mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 10:46:25 +00:00
build: containerize the control layer and publish it to GHCR
Add a second image for the handler control CLI (spawn/list/kill, approve/ reject, forge-init, CI poller) alongside the existing API image. It shares the package, database, and /var/lib/handler volume but runs the control process instead of uvicorn. - Dockerfile.control: git + tmux baked in for live spawning; default CMD is the `poll-ci --watch` loop; RUN_MIGRATIONS toggle reuses docker-entrypoint.sh. - docker-control.yml: builds/pushes ghcr.io/<repo>/control (multi-arch), scoped gha cache so it doesn't clobber the API build. - docker-compose.yml: new `control` service, RUN_MIGRATIONS=false, depends on the API (which owns migrations) being healthy. - README: Containers section documenting both images and compose usage.
This commit is contained in:
@@ -15,6 +15,7 @@ docs
|
|||||||
!.env.example
|
!.env.example
|
||||||
*.db
|
*.db
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
Dockerfile.control
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
.dockerignore
|
.dockerignore
|
||||||
.mise.toml
|
.mise.toml
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: docker-control
|
||||||
|
|
||||||
|
# Builds the control-layer image (the `handler` CLI / CI poller) from Dockerfile.control
|
||||||
|
# and publishes it to GHCR alongside the API image built by docker.yml.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ["v*"]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: ghcr.io/${{ github.repository }}/control
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Tags: branch name on branch pushes, semver on v* tags, short SHA always,
|
||||||
|
# `latest` only on the default branch. metadata-action lowercases the repo
|
||||||
|
# name (GHCR requires lowercase).
|
||||||
|
- name: Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.IMAGE }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=sha
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.control
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
# PRs build (to catch Dockerfile breakage) but never push.
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha,scope=control
|
||||||
|
cache-to: type=gha,mode=max,scope=control
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# Control-layer image: the `handler` CLI (the write side — spawn/list/kill agents,
|
||||||
|
# approve/reject branches, forge-init, and the CI poller). It shares the package, the
|
||||||
|
# database, and the /var/lib/handler data volume with the API image (see Dockerfile),
|
||||||
|
# but runs the control process instead of uvicorn.
|
||||||
|
|
||||||
|
# ---- build stage: install the package + deps into an isolated venv ----
|
||||||
|
FROM python:3.11-slim AS builder
|
||||||
|
|
||||||
|
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1
|
||||||
|
|
||||||
|
RUN python -m venv /opt/venv
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY pyproject.toml README.md ./
|
||||||
|
COPY src ./src
|
||||||
|
RUN pip install .
|
||||||
|
|
||||||
|
# ---- runtime stage ----
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# git + tmux are the live-spawning dependencies the control layer shells out to
|
||||||
|
# (README "Requirements"); openssh-client covers git-over-ssh remotes. The `claude`
|
||||||
|
# and `forge` binaries are bring-your-own — layer or mount them in for live agent
|
||||||
|
# spawning and CI resolution; the poller degrades gracefully when forge is absent.
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends git tmux openssh-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH" \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
# SQLite fallback lives on the /var/lib/handler volume; point DATABASE_URL at the
|
||||||
|
# same Postgres the API uses for a shared, real deploy (see docker-compose.yml).
|
||||||
|
DATABASE_URL="sqlite:////var/lib/handler/handler.db" \
|
||||||
|
PROJECTS_ROOT="/var/lib/handler/projects"
|
||||||
|
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
|
||||||
|
# Ship alembic alongside the package so the image can migrate standalone if asked
|
||||||
|
# (RUN_MIGRATIONS=true). In the compose stack the API owns migrations and the control
|
||||||
|
# service runs with RUN_MIGRATIONS=false to avoid a startup race.
|
||||||
|
WORKDIR /app
|
||||||
|
COPY alembic.ini ./
|
||||||
|
COPY src/handler/migrations ./src/handler/migrations
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
RUN useradd --system --home-dir /var/lib/handler --create-home handler \
|
||||||
|
&& mkdir -p /var/lib/handler/projects \
|
||||||
|
&& chown -R handler:handler /var/lib/handler \
|
||||||
|
&& chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
USER handler
|
||||||
|
VOLUME /var/lib/handler
|
||||||
|
|
||||||
|
# Liveness: exercises the CLI end-to-end and confirms the database is reachable.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD handler list >/dev/null 2>&1 || exit 1
|
||||||
|
|
||||||
|
# Default to the CI poller — the one long-running control process. Override the command
|
||||||
|
# for one-shot control operations, e.g. `docker compose run --rm control handler list`.
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
|
CMD ["handler", "poll-ci", "--watch"]
|
||||||
@@ -143,6 +143,32 @@ curl -s -X POST $BASE/projects/leeworks-api/agents/api/resume -H "$TOKEN" \
|
|||||||
-H 'Content-Type: application/json' -d '{}'
|
-H 'Content-Type: application/json' -d '{}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Containers
|
||||||
|
|
||||||
|
Two images are published to GHCR, one per process, sharing the package, the database, and
|
||||||
|
the `/var/lib/handler` data volume:
|
||||||
|
|
||||||
|
| Image | Dockerfile | Runs | Workflow |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ghcr.io/0xwheatyz/handler` | [`Dockerfile`](Dockerfile) | the API (`uvicorn`) — also applies migrations on start | [`docker.yml`](.github/workflows/docker.yml) |
|
||||||
|
| `ghcr.io/0xwheatyz/handler/control` | [`Dockerfile.control`](Dockerfile.control) | the control layer (`handler poll-ci --watch`) | [`docker-control.yml`](.github/workflows/docker-control.yml) |
|
||||||
|
|
||||||
|
The control image bakes in `git` + `tmux`; the `claude` and `forge` binaries are
|
||||||
|
bring-your-own (layer or mount them in for live agent spawning — the CI poller degrades
|
||||||
|
gracefully without `forge`).
|
||||||
|
|
||||||
|
[`docker-compose.yml`](docker-compose.yml) wires both up with Postgres. The API owns
|
||||||
|
migrations, so the control service runs with `RUN_MIGRATIONS=false` and waits for the API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AUTH_TOKEN="$(openssl rand -hex 32)"
|
||||||
|
docker compose up -d # db + api + control (CI poller)
|
||||||
|
|
||||||
|
# One-shot control commands run against the same image:
|
||||||
|
docker compose run --rm control handler list
|
||||||
|
docker compose run --rm control handler spawn --project leeworks-api --name junior --task "…"
|
||||||
|
```
|
||||||
|
|
||||||
## Control CLI
|
## Control CLI
|
||||||
|
|
||||||
The `handler` command manages agent processes (the write side):
|
The `handler` command manages agent processes (the write side):
|
||||||
|
|||||||
@@ -22,6 +22,33 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Control layer: the `handler` CLI running the CI poller loop. Shares the database and
|
||||||
|
# the handler-data volume with the API. It waits for the API (which owns migrations),
|
||||||
|
# so RUN_MIGRATIONS is off here to avoid a startup race. Run one-shot control commands
|
||||||
|
# against the same image with, e.g., `docker compose run --rm control handler list`.
|
||||||
|
# Live agent spawning also needs `git`/`tmux` (baked in) plus bring-your-own
|
||||||
|
# `claude`/`forge` binaries — layer or mount those in.
|
||||||
|
control:
|
||||||
|
image: ghcr.io/0xwheatyz/handler/control:latest
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.control
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql+psycopg://handler:handler@db:5432/handler
|
||||||
|
RUN_MIGRATIONS: "false"
|
||||||
|
PROJECTS_ROOT: /var/lib/handler/projects
|
||||||
|
# Per-project forge credentials are resolved from credential_ref pointers at spawn;
|
||||||
|
# export the referenced vars here when spawning agents from this container.
|
||||||
|
FORGE_VERSION: ${FORGE_VERSION:-}
|
||||||
|
volumes:
|
||||||
|
- handler-data:/var/lib/handler
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
Reference in New Issue
Block a user