Skip to content

Commit d47b252

Browse files
authored
Merge pull request #133 from skulidropek/issue-132
fix(shell): keep docker-git runtime state in Docker-managed volumes
2 parents b689d34 + 8ac62fa commit d47b252

File tree

286 files changed

+25619
-1226
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

286 files changed

+25619
-1226
lines changed

.github/workflows/check.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,16 @@ jobs:
136136
run: docker version && docker compose version
137137
- name: Login context notice
138138
run: bash scripts/e2e/login-context.sh
139+
140+
e2e-runtime-volumes-ssh:
141+
name: E2E (Runtime volumes + SSH)
142+
runs-on: ubuntu-latest
143+
timeout-minutes: 25
144+
steps:
145+
- uses: actions/checkout@v6
146+
- name: Install dependencies
147+
uses: ./.github/actions/setup
148+
- name: Docker info
149+
run: docker version && docker compose version
150+
- name: Runtime volumes + host SSH CLI
151+
run: bash scripts/e2e/runtime-volumes-ssh.sh

README.md

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,71 @@
11
# docker-git
22

33
`docker-git` создаёт отдельную Docker-среду для каждого репозитория, issue или PR.
4-
По умолчанию проекты лежат в `~/.docker-git`.
4+
5+
Теперь есть API-first controller mode:
6+
- хосту нужен только Docker
7+
- поднимается `docker-git-api` controller container
8+
- его state живёт в Docker volume `docker-git-projects`
9+
- controller через Docker API создаёт и обслуживает дочерние project containers
10+
- снаружи ты общаешься с системой через HTTP API или `./ctl`
511

612
## Что нужно
713

8-
- Docker Engine или Docker Desktop
14+
- Для controller mode: Docker Engine или Docker Desktop
915
- Доступ к Docker без `sudo`
10-
- Node.js и `npm`
16+
- Node.js и `npm` нужны только для legacy host CLI mode
1117

12-
## Установка
18+
## API Controller Mode
1319

1420
```bash
15-
npm i -g @prover-coder-ai/docker-git
16-
docker-git --help
21+
./ctl up
22+
./ctl health
23+
./ctl projects
1724
```
1825

19-
## Авторизация
26+
API публикуется на `http://127.0.0.1:3334` по умолчанию.
2027

2128
```bash
22-
docker-git auth github login --web
23-
docker-git auth codex login --web
24-
docker-git auth claude login --web
29+
./ctl request GET /projects
30+
./ctl request POST /projects '{"repoUrl":"https://github.com/ProverCoderAI/docker-git.git","repoRef":"main"}'
31+
```
32+
33+
Важно:
34+
- `./ctl` не требует `curl`, `node` или `pnpm` на хосте
35+
- запросы к API выполняются через `curl` внутри controller container
36+
- `.docker-git` больше не обязан лежать на host filesystem: controller хранит его в Docker volume
37+
38+
## Legacy Host CLI
39+
40+
```bash
41+
npm i -g @prover-coder-ai/docker-git
42+
docker-git --help
2543
```
2644

2745
## Пример
2846

29-
Можно передавать ссылку на репозиторий, ветку (`/tree/...`), issue или PR.
47+
Через API controller можно создать проект и потом поднять его отдельно:
3048

3149
```bash
32-
docker-git clone https://github.com/ProverCoderAI/docker-git/issues/122 --force --mcp-playwright
50+
./ctl request POST /projects '{"repoUrl":"https://github.com/ProverCoderAI/docker-git.git","repoRef":"main","up":false}'
51+
./ctl projects
3352
```
3453

35-
- `--force` пересоздаёт окружение и удаляет volumes проекта.
36-
- `--mcp-playwright` включает Playwright MCP и Chromium sidecar для браузерной автоматизации.
54+
API возвращает `projectId`, после чего можно:
55+
56+
```bash
57+
./ctl request POST /projects/<projectId>/up
58+
./ctl request GET /projects/<projectId>/logs
59+
./ctl request POST /projects/<projectId>/down
60+
```
3761

38-
Автоматический запуск агента:
62+
## Проверка Docker runtime
3963

4064
```bash
41-
docker-git clone https://github.com/ProverCoderAI/docker-git/issues/122 --force --auto
65+
pnpm run e2e:runtime-volumes-ssh
4266
```
4367

44-
- `--auto` сам выбирает Claude или Codex по доступной авторизации. Если доступны оба, выбор случайный.
45-
- `--auto=claude` или `--auto=codex` принудительно выбирает агента.
46-
- В auto-режиме агент сам выполняет задачу, создаёт PR и после завершения контейнер очищается.
68+
Сценарий доказывает, что контейнер стартует через Docker, runtime state живёт в named volumes, а SSH реально заходит в дочерний project container.
4769

4870
## Подробности
4971

ctl

Lines changed: 178 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,188 @@
11
#!/usr/bin/env bash
2-
# CHANGE: provide a minimal local orchestrator for the dev container and auth helpers
3-
# WHY: single command to manage the container and login flows
4-
# QUOTE(TZ): "команда с помощью которой можно полностью контролировать этими докер образами"
5-
# REF: user-request-2026-01-07
2+
# CHANGE: control the API-first docker-git controller container from the host
3+
# WHY: host should only need Docker while all orchestration runs inside the API controller
4+
# QUOTE(TZ): "Поднимается сервер и ты через него можешь общаться с контейнером"
5+
# REF: user-request-2026-03-15-api-controller
66
# SOURCE: n/a
7-
# FORMAT THEOREM: forall cmd: valid(cmd) -> action(cmd) terminates
7+
# FORMAT THEOREM: forall cmd: valid(cmd) -> controller_action(cmd) terminates
88
# PURITY: SHELL
99
# EFFECT: Effect<IO, Error, Env>
10-
# INVARIANT: uses repo-local docker-compose.yml and dev-ssh container
11-
# COMPLEXITY: O(1)
10+
# INVARIANT: every API request is executed from inside the controller container; host does not need curl/node/pnpm
11+
# COMPLEXITY: O(1) + network/docker
1212
set -euo pipefail
1313

1414
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1515
COMPOSE_FILE="$ROOT/docker-compose.yml"
16-
CONTAINER_NAME="dev-ssh"
17-
SSH_KEY="$ROOT/dev_ssh_key"
18-
SSH_PORT="2222"
19-
SSH_USER="dev"
20-
SSH_HOST="localhost"
16+
CONTAINER_NAME="docker-git-api"
17+
API_PORT="${DOCKER_GIT_API_PORT:-3334}"
18+
API_HOST="${DOCKER_GIT_API_BIND_HOST:-127.0.0.1}"
19+
API_BASE_URL="http://127.0.0.1:${API_PORT}"
20+
DOCKER_CMD=()
2121

2222
usage() {
2323
cat <<'USAGE'
2424
Usage: ./ctl <command>
2525
26-
Container:
27-
up Build and start the container
28-
down Stop and remove the container
29-
ps Show container status
30-
logs Tail logs
31-
restart Restart the container
32-
exec Shell into the container
33-
ssh SSH into the container
26+
Controller:
27+
up Build and start the API controller
28+
down Stop and remove the API controller
29+
ps Show controller status
30+
logs Tail controller logs
31+
restart Restart the controller
32+
shell Open a shell inside the controller
33+
url Print the published API URL
34+
health GET /health through curl running inside the controller
3435
35-
Codex auth:
36-
codex-login Device-code login flow (headless-friendly)
37-
codex-status Show auth status (exit 0 when logged in)
38-
codex-logout Remove cached credentials
36+
API:
37+
projects GET /projects
38+
request request <METHOD> <PATH> [JSON_BODY]
39+
examples:
40+
./ctl request GET /projects
41+
./ctl request POST /projects '{"repoUrl":"https://github.com/org/repo.git"}'
42+
./ctl request POST /projects/<projectId>/up
3943
4044
USAGE
4145
}
4246

4347
compose() {
44-
docker compose -f "$COMPOSE_FILE" "$@"
48+
"${DOCKER_CMD[@]}" compose -f "$COMPOSE_FILE" "$@"
4549
}
4650

51+
require_running() {
52+
if ! "${DOCKER_CMD[@]}" ps --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then
53+
echo "Controller is not running. Start it with: ./ctl up" >&2
54+
exit 1
55+
fi
56+
}
57+
58+
api_exec() {
59+
"${DOCKER_CMD[@]}" exec "$CONTAINER_NAME" "$@"
60+
}
61+
62+
normalize_api_path() {
63+
local raw_path="$1"
64+
65+
if [[ "$raw_path" != /projects/* ]]; then
66+
printf '%s' "$raw_path"
67+
return
68+
fi
69+
70+
local normalized
71+
normalized="$("${DOCKER_CMD[@]}" exec -i "$CONTAINER_NAME" node - "$raw_path" <<'NODE'
72+
const raw = process.argv[2] ?? ""
73+
const [pathname, query = ""] = raw.split(/\?(.*)/s, 2)
74+
const prefix = "/projects/"
75+
76+
const joinWithQuery = (path) => query.length > 0 ? `${path}?${query}` : path
77+
const encodeProjectPath = (projectId, suffix = "") =>
78+
joinWithQuery(`${prefix}${encodeURIComponent(projectId)}${suffix}`)
79+
80+
if (!pathname.startsWith(prefix)) {
81+
process.stdout.write(raw)
82+
process.exit(0)
83+
}
84+
85+
const remainder = pathname.slice(prefix.length)
86+
if (!remainder.startsWith("/")) {
87+
process.stdout.write(raw)
88+
process.exit(0)
89+
}
90+
91+
const patterns = [
92+
{
93+
regex: /^(.*)\/agents\/([^/]+)\/(attach|stop|logs)$/u,
94+
render: ([, projectId, agentId, action]) =>
95+
encodeProjectPath(projectId, `/agents/${encodeURIComponent(agentId)}/${action}`)
96+
},
97+
{
98+
regex: /^(.*)\/agents\/([^/]+)$/u,
99+
render: ([, projectId, agentId]) =>
100+
encodeProjectPath(projectId, `/agents/${encodeURIComponent(agentId)}`)
101+
},
102+
{
103+
regex: /^(.*)\/agents$/u,
104+
render: ([, projectId]) => encodeProjectPath(projectId, "/agents")
105+
},
106+
{
107+
regex: /^(.*)\/(up|down|recreate|ps|logs|events)$/u,
108+
render: ([, projectId, action]) => encodeProjectPath(projectId, `/${action}`)
109+
},
110+
{
111+
regex: /^(.*)$/u,
112+
render: ([, projectId]) => encodeProjectPath(projectId)
113+
}
114+
]
115+
116+
for (const { regex, render } of patterns) {
117+
const match = remainder.match(regex)
118+
if (match !== null) {
119+
process.stdout.write(render(match))
120+
process.exit(0)
121+
}
122+
}
123+
124+
process.stdout.write(raw)
125+
NODE
126+
)"
127+
printf '%s' "$normalized"
128+
}
129+
130+
api_request() {
131+
local method="$1"
132+
local path="$2"
133+
local body="${3:-}"
134+
135+
require_running
136+
local normalized_path
137+
normalized_path="$(normalize_api_path "$path")"
138+
139+
if [[ -n "$body" ]]; then
140+
printf '%s' "$body" | "${DOCKER_CMD[@]}" exec -i "$CONTAINER_NAME" sh -lc \
141+
"curl -fsS -X '$method' '$API_BASE_URL$normalized_path' -H 'content-type: application/json' --data-binary @-"
142+
printf '\n'
143+
return
144+
fi
145+
146+
"${DOCKER_CMD[@]}" exec "$CONTAINER_NAME" sh -lc "curl -fsS -X '$method' '$API_BASE_URL$normalized_path'"
147+
printf '\n'
148+
}
149+
150+
wait_for_health() {
151+
require_running
152+
local attempts=30
153+
local delay_seconds=2
154+
local attempt=1
155+
while (( attempt <= attempts )); do
156+
if "${DOCKER_CMD[@]}" exec "$CONTAINER_NAME" sh -lc "curl -fsS '$API_BASE_URL/health' >/dev/null"; then
157+
return 0
158+
fi
159+
sleep "$delay_seconds"
160+
attempt=$((attempt + 1))
161+
done
162+
163+
echo "Controller did not become healthy in time." >&2
164+
return 1
165+
}
166+
167+
resolve_docker_cmd() {
168+
if docker info >/dev/null 2>&1; then
169+
DOCKER_CMD=(docker)
170+
return
171+
fi
172+
if sudo -n docker info >/dev/null 2>&1; then
173+
DOCKER_CMD=(sudo docker)
174+
return
175+
fi
176+
DOCKER_CMD=(docker)
177+
}
178+
179+
resolve_docker_cmd
180+
47181
case "${1:-}" in
48182
up)
49183
compose up -d --build
184+
wait_for_health
185+
echo "Controller API: http://${API_HOST}:${API_PORT}"
50186
;;
51187
down)
52188
compose down
@@ -59,21 +195,27 @@ case "${1:-}" in
59195
;;
60196
restart)
61197
compose restart
198+
wait_for_health
62199
;;
63-
exec)
64-
docker exec -it "$CONTAINER_NAME" bash
200+
shell)
201+
require_running
202+
"${DOCKER_CMD[@]}" exec -it "$CONTAINER_NAME" bash
65203
;;
66-
ssh)
67-
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST"
204+
url)
205+
echo "http://${API_HOST}:${API_PORT}"
68206
;;
69-
codex-login)
70-
docker exec -it "$CONTAINER_NAME" codex login --device-auth
207+
health)
208+
api_request GET /health
71209
;;
72-
codex-status)
73-
docker exec "$CONTAINER_NAME" codex login status
210+
projects)
211+
api_request GET /projects
74212
;;
75-
codex-logout)
76-
docker exec -it "$CONTAINER_NAME" codex logout
213+
request)
214+
if [[ $# -lt 3 ]]; then
215+
echo "Usage: ./ctl request <METHOD> <PATH> [JSON_BODY]" >&2
216+
exit 1
217+
fi
218+
api_request "$2" "$3" "${4:-}"
77219
;;
78220
help|--help|-h|"")
79221
usage
@@ -83,4 +225,4 @@ case "${1:-}" in
83225
usage >&2
84226
exit 1
85227
;;
86-
esac
228+
esac

docker-compose.api.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,20 @@ services:
77
environment:
88
DOCKER_GIT_API_PORT: ${DOCKER_GIT_API_PORT:-3334}
99
DOCKER_GIT_PROJECTS_ROOT: ${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
10+
DOCKER_GIT_PROJECTS_ROOT_VOLUME: ${DOCKER_GIT_PROJECTS_ROOT_VOLUME:-docker-git-projects}
1011
DOCKER_GIT_FEDERATION_PUBLIC_ORIGIN: ${DOCKER_GIT_FEDERATION_PUBLIC_ORIGIN:-}
1112
DOCKER_GIT_FEDERATION_ACTOR: ${DOCKER_GIT_FEDERATION_ACTOR:-docker-git}
1213
ports:
1314
- "${DOCKER_GIT_API_BIND_HOST:-127.0.0.1}:${DOCKER_GIT_API_PORT:-3334}:${DOCKER_GIT_API_PORT:-3334}"
15+
dns:
16+
- 8.8.8.8
17+
- 8.8.4.4
18+
- 1.1.1.1
1419
volumes:
1520
- /var/run/docker.sock:/var/run/docker.sock
16-
- ${DOCKER_GIT_PROJECTS_ROOT_HOST:-/home/dev/.docker-git}:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
21+
- docker_git_projects:${DOCKER_GIT_PROJECTS_ROOT:-/home/dev/.docker-git}
1722
restart: unless-stopped
23+
24+
volumes:
25+
docker_git_projects:
26+
name: ${DOCKER_GIT_PROJECTS_ROOT_VOLUME:-docker-git-projects}

0 commit comments

Comments
 (0)