Skip to content

Commit c18b20c

Browse files
authored
Speed up controller Docker rebuilds (#344)
* perf(api): speed up controller Docker rebuilds * perf(ci): speed up docker e2e setup * fix(app): address remaining coderabbit review * test(app): cover controller revision ignores with properties * test(app): strengthen controller revision properties * test(app): tighten controller revision fs mock * fix(app): address coderabbit full review * fix(app): make controller image revision inspection best effort * fix(app): preserve controller image inspection errors * fix(app): keep docker inspect failures visible * fix(app): stabilize browser frontend api target * fix(app): return explicit browser api url
1 parent 525ed8b commit c18b20c

30 files changed

Lines changed: 2120 additions & 121 deletions

.dockerignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
.git
2+
.codex
3+
.github
4+
.knowledge
25
node_modules
36
**/node_modules
47
**/.cache
8+
**/.turbo
9+
coverage
10+
dist
11+
dist-test
12+
dist-web
13+
packages/*/coverage
14+
packages/*/dist
15+
packages/*/dist-test
16+
packages/*/dist-web
17+
**/.vite
18+
**/*.log
519
third_party/skiller-desktop-skills-manager/out

.github/actions/free-docker-disk/action.yml

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,51 @@ runs:
77
- name: Free disk for Docker builds
88
shell: bash
99
run: |
10-
set -euxo pipefail
10+
set -euo pipefail
1111
12-
df -h
13-
docker system df || true
12+
if command -v df >/dev/null 2>&1; then
13+
df -h || true
14+
else
15+
echo "df is not available; Docker disk cleanup will run without a free-space precheck." >&2
16+
fi
17+
18+
threshold_gib="${DOCKER_GIT_FREE_DOCKER_DISK_MIN_AVAILABLE_GIB:-40}"
19+
force_cleanup="${DOCKER_GIT_FORCE_FREE_DOCKER_DISK:-0}"
20+
force_cleanup_normalized="${force_cleanup,,}"
21+
force_cleanup_enabled=0
22+
23+
if [[ ! "$threshold_gib" =~ ^[0-9]+$ ]]; then
24+
echo "Invalid DOCKER_GIT_FREE_DOCKER_DISK_MIN_AVAILABLE_GIB: $threshold_gib" >&2
25+
exit 1
26+
fi
27+
28+
case "$force_cleanup_normalized" in
29+
1|true|yes|on)
30+
force_cleanup_enabled=1
31+
;;
32+
*)
33+
force_cleanup_enabled=0
34+
;;
35+
esac
36+
37+
if command -v df >/dev/null 2>&1; then
38+
available_kib="$(df -Pk / 2>/dev/null | awk 'NR == 2 { print $4 }' || true)"
39+
else
40+
available_kib=""
41+
fi
42+
if [[ ! "$available_kib" =~ ^[0-9]+$ ]]; then
43+
echo "Could not parse available disk space from df output; Docker disk cleanup will run." >&2
44+
available_kib=0
45+
fi
46+
threshold_kib="$((threshold_gib * 1024 * 1024))"
47+
48+
if [[ "$force_cleanup_enabled" != "1" && "$available_kib" -ge "$threshold_kib" ]]; then
49+
echo "Skipping Docker disk cleanup: / has at least ${threshold_gib}GiB available."
50+
exit 0
51+
fi
52+
53+
echo "Running Docker disk cleanup: available=${available_kib}KiB threshold=${threshold_kib}KiB force=${force_cleanup}."
54+
timeout 20s docker system df || true
1455
1556
sudo rm -rf \
1657
/opt/ghc \
@@ -21,5 +62,7 @@ runs:
2162
2263
docker system prune -af --volumes || true
2364
24-
df -h
25-
docker system df || true
65+
if command -v df >/dev/null 2>&1; then
66+
df -h || true
67+
fi
68+
timeout 20s docker system df || true

.github/workflows/check.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ jobs:
153153
name: E2E (Browser command)
154154
runs-on: ubuntu-latest
155155
timeout-minutes: 40
156+
env:
157+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
158+
DOCKER_GIT_E2E_REUSE_WORKSPACE_INSTALL: "1"
156159
steps:
157160
- uses: actions/checkout@v6
158161
with:
@@ -170,6 +173,9 @@ jobs:
170173
name: E2E (OpenCode)
171174
runs-on: ubuntu-latest
172175
timeout-minutes: 40
176+
env:
177+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
178+
DOCKER_GIT_E2E_REUSE_WORKSPACE_INSTALL: "1"
173179
steps:
174180
- uses: actions/checkout@v6
175181
with:
@@ -187,6 +193,9 @@ jobs:
187193
name: E2E (Clone cache)
188194
runs-on: ubuntu-latest
189195
timeout-minutes: 40
196+
env:
197+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
198+
DOCKER_GIT_E2E_REUSE_WORKSPACE_INSTALL: "1"
190199
steps:
191200
- uses: actions/checkout@v6
192201
with:
@@ -204,6 +213,9 @@ jobs:
204213
name: E2E (Login context)
205214
runs-on: ubuntu-latest
206215
timeout-minutes: 40
216+
env:
217+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
218+
DOCKER_GIT_E2E_REUSE_WORKSPACE_INSTALL: "1"
207219
steps:
208220
- uses: actions/checkout@v6
209221
with:
@@ -221,6 +233,9 @@ jobs:
221233
name: E2E (Runtime volumes + SSH)
222234
runs-on: ubuntu-latest
223235
timeout-minutes: 60
236+
env:
237+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
238+
DOCKER_GIT_E2E_REUSE_WORKSPACE_INSTALL: "1"
224239
steps:
225240
- uses: actions/checkout@v6
226241
with:
@@ -238,6 +253,9 @@ jobs:
238253
name: E2E (Clone auto-open SSH)
239254
runs-on: ubuntu-latest
240255
timeout-minutes: 40
256+
env:
257+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
258+
DOCKER_GIT_E2E_REUSE_WORKSPACE_INSTALL: "1"
241259
steps:
242260
- uses: actions/checkout@v6
243261
with:

docker-compose.api.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ services:
44
context: .
55
dockerfile: packages/api/Dockerfile
66
args:
7+
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
8+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: ${DOCKER_GIT_CONTROLLER_BUILD_SKILLER:-1}
79
UBUNTU_APT_MIRROR: ${UBUNTU_APT_MIRROR:-}
810
container_name: ${DOCKER_GIT_API_CONTAINER_NAME:-docker-git-api}
911
environment:
1012
DOCKER_GIT_API_PORT: ${DOCKER_GIT_API_PORT:-3334}
13+
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
1114
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-isolated}
1215
DOCKER_HOST: ${DOCKER_GIT_CONTROLLER_DOCKER_HOST:-unix:///var/run/docker.sock}
1316
DOCKER_GIT_DOCKERD_TCP_HOST: ${DOCKER_GIT_DOCKERD_TCP_HOST:-tcp://0.0.0.0:2375}

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ services:
55
dockerfile: packages/api/Dockerfile
66
args:
77
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
8+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: ${DOCKER_GIT_CONTROLLER_BUILD_SKILLER:-1}
89
UBUNTU_APT_MIRROR: ${UBUNTU_APT_MIRROR:-}
910
container_name: ${DOCKER_GIT_API_CONTAINER_NAME:-docker-git-api}
1011
environment:

packages/api/Dockerfile

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
FROM ubuntu:26.04
1+
FROM ubuntu:26.04 AS controller-base
22

3-
ARG DOCKER_GIT_CONTROLLER_REV=unknown
43
ARG UBUNTU_APT_MIRROR=
5-
LABEL io.prover-coder-ai.docker-git.controller-rev=$DOCKER_GIT_CONTROLLER_REV
64

75
ENV DEBIAN_FRONTEND=noninteractive
8-
ENV DOCKER_GIT_CONTROLLER_REV=$DOCKER_GIT_CONTROLLER_REV
96
ENV BUN_INSTALL=/opt/bun
107
ENV PATH=/opt/bun/bin:$PATH
118
WORKDIR /workspace
@@ -55,16 +52,23 @@ RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
5552
&& npm i -g node-gyp \
5653
&& rm -rf /var/lib/apt/lists/*
5754

55+
FROM controller-base AS workspace-deps
56+
5857
COPY package.json bun.lock bunfig.toml tsconfig.base.json tsconfig.json ./
59-
COPY patches ./patches
60-
COPY scripts ./scripts
61-
COPY packages ./packages
62-
COPY .gitmodules ./.gitmodules
63-
COPY third_party ./third_party
58+
RUN mkdir -p packages/api packages/app packages/docker-git-session-sync packages/lib
59+
COPY packages/api/package.json ./packages/api/package.json
60+
COPY packages/app/package.json ./packages/app/package.json
61+
COPY packages/docker-git-session-sync/package.json ./packages/docker-git-session-sync/package.json
62+
COPY packages/lib/package.json ./packages/lib/package.json
6463

6564
RUN set -eu; \
6665
for attempt in 1 2 3 4 5; do \
67-
if bun install --frozen-lockfile --silent; then \
66+
if bun install \
67+
--frozen-lockfile \
68+
--silent \
69+
--filter @effect-template/api \
70+
--filter @effect-template/lib \
71+
--filter @prover-coder-ai/docker-git-session-sync; then \
6872
exit 0; \
6973
fi; \
7074
echo "bun install attempt ${attempt} failed; retrying..." >&2; \
@@ -73,28 +77,78 @@ RUN set -eu; \
7377
done; \
7478
echo "bun install failed after retries" >&2; \
7579
exit 1
80+
81+
FROM workspace-deps AS workspace-static
82+
83+
COPY patches ./patches
84+
COPY scripts ./scripts
85+
COPY packages/docker-git-session-sync ./packages/docker-git-session-sync
86+
COPY packages/lib ./packages/lib
87+
88+
RUN bun run --cwd packages/docker-git-session-sync build
7689
RUN bun run --cwd packages/lib build
77-
RUN bun run --cwd packages/api build
78-
RUN bun scripts/skiller-apply-docker-git-patches.mjs
79-
RUN test -f third_party/skiller-desktop-skills-manager/package.json \
80-
&& cd third_party/skiller-desktop-skills-manager \
81-
&& for attempt in 1 2 3 4 5; do \
82-
if bun install --frozen-lockfile --silent; then \
83-
break; \
84-
fi; \
85-
if [ "$attempt" = "5" ]; then \
86-
echo "skiller bun install failed after retries" >&2; \
87-
exit 1; \
88-
fi; \
89-
echo "skiller bun install attempt ${attempt} failed; retrying..." >&2; \
90-
rm -rf /root/.bun/install/cache node_modules; \
91-
sleep $((attempt * 2)); \
92-
done \
93-
&& bun run build \
94-
&& touch out/.docker-git-browser-folder-picker.patch \
95-
&& mkdir -p out/preload \
96-
&& ln -sf index.mjs out/preload/index.js
9790

91+
FROM controller-base AS skiller-build
92+
93+
ARG DOCKER_GIT_CONTROLLER_BUILD_SKILLER=1
94+
95+
COPY patches ./patches
96+
COPY scripts/skiller-apply-docker-git-patches.mjs ./scripts/skiller-apply-docker-git-patches.mjs
97+
COPY .gitmodules ./.gitmodules
98+
COPY third_party ./third_party
99+
100+
RUN if [ "$DOCKER_GIT_CONTROLLER_BUILD_SKILLER" = "1" ]; then \
101+
bun scripts/skiller-apply-docker-git-patches.mjs; \
102+
else \
103+
echo "Skipping Skiller build for controller image."; \
104+
fi
105+
RUN if [ "$DOCKER_GIT_CONTROLLER_BUILD_SKILLER" = "1" ]; then \
106+
test -f third_party/skiller-desktop-skills-manager/package.json \
107+
&& cd third_party/skiller-desktop-skills-manager \
108+
&& for attempt in 1 2 3 4 5; do \
109+
if bun install --frozen-lockfile --silent; then \
110+
break; \
111+
fi; \
112+
if [ "$attempt" = "5" ]; then \
113+
echo "skiller bun install failed after retries" >&2; \
114+
exit 1; \
115+
fi; \
116+
echo "skiller bun install attempt ${attempt} failed; retrying..." >&2; \
117+
rm -rf /root/.bun/install/cache node_modules; \
118+
sleep $((attempt * 2)); \
119+
done \
120+
&& bun run build \
121+
&& touch out/.docker-git-browser-folder-picker.patch \
122+
&& mkdir -p out/preload \
123+
&& ln -sf index.mjs out/preload/index.js; \
124+
else \
125+
cd third_party/skiller-desktop-skills-manager \
126+
&& mkdir -p node_modules/electron/dist out/main out/renderer out/preload \
127+
&& printf '%s\n' '#!/usr/bin/env sh' 'echo "Skiller is not bundled in this controller image." >&2' 'exit 1' \
128+
> node_modules/electron/dist/electron \
129+
&& chmod +x node_modules/electron/dist/electron \
130+
&& printf '%s\n' 'throw new Error("Skiller is not bundled in this controller image.")' > out/main/index.js \
131+
&& printf '%s\n' '<!doctype html><title>Skiller unavailable</title>' > out/renderer/index.html \
132+
&& printf '%s\n' 'export {}' > out/preload/index.mjs \
133+
&& touch out/.docker-git-browser-folder-picker.patch \
134+
&& ln -sf index.mjs out/preload/index.js; \
135+
fi
136+
137+
FROM workspace-static AS controller-final
138+
139+
COPY .gitmodules ./.gitmodules
140+
COPY --from=skiller-build /workspace/third_party/skiller-desktop-skills-manager ./third_party/skiller-desktop-skills-manager
141+
COPY packages/api ./packages/api
142+
143+
RUN ./packages/api/node_modules/.bin/tsc -p packages/api/tsconfig.json
144+
145+
ARG DOCKER_GIT_CONTROLLER_REV=unknown
146+
ARG DOCKER_GIT_CONTROLLER_BUILD_SKILLER=1
147+
LABEL io.prover-coder-ai.docker-git.controller-rev=$DOCKER_GIT_CONTROLLER_REV
148+
LABEL io.prover-coder-ai.docker-git.controller-build-skiller=$DOCKER_GIT_CONTROLLER_BUILD_SKILLER
149+
150+
ENV DOCKER_GIT_CONTROLLER_REV=$DOCKER_GIT_CONTROLLER_REV
151+
ENV DOCKER_GIT_CONTROLLER_BUILD_SKILLER=$DOCKER_GIT_CONTROLLER_BUILD_SKILLER
98152
ENV DOCKER_GIT_API_PORT=3334
99153
ENV DOCKER_GIT_DOCKER_RUNTIME=isolated
100154
ENV DOCKER_HOST=unix:///var/run/docker.sock

packages/api/src/services/terminal-sessions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -766,11 +766,11 @@ const writePtyInput = (pty: PtyBridge | null, data: string): void => {
766766
const shellQuote = (value: string): string => `'${value.replace(/'/gu, "'\\''")}'`
767767

768768
// CHANGE: Predicate for when tmux should forward right-click pane events.
769-
// WHY: Mouse-aware apps and copy/view mode still need pane mouse events, while tmux menus must stay disabled.
769+
// WHY: Mouse-aware apps receive pane events; copy/view mode keeps tmux handling unless mouse tracking is active.
770770
// QUOTE(TZ): issue #340 right-click must not open the default tmux menu in browser terminals.
771771
// REF: PR #342 tmux right-click handling.
772772
// SOURCE: n/a
773-
// FORMAT THEOREM: mouse-aware-or-copy-mode => predicate evaluates truthy in tmux.
773+
// FORMAT THEOREM: mouse_any_flag or non-copy/view pane mode => predicate evaluates truthy in tmux.
774774
// PURITY: CORE
775775
// EFFECT: none
776776
// INVARIANT: The predicate contains only tmux format language and no shell interpolation.

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"lint:effect": "NODE_OPTIONS=--max-old-space-size=4096 PATH=../../scripts:$PATH eslint --config eslint.effect-ts-check.config.mjs .",
2828
"prebuild:docker-git": "bun install --cwd ../.. && bun run --cwd ../docker-git-session-sync build && bun run --cwd ../lib build",
2929
"build:docker-git": "vite build --config vite.docker-git.config.ts",
30+
"prebuild:docker-git:reuse-install": "bun run --cwd ../docker-git-session-sync build && bun run --cwd ../lib build",
31+
"build:docker-git:reuse-install": "vite build --config vite.docker-git.config.ts",
3032
"check": "bun run typecheck",
3133
"clone": "bun run build:docker-git && bun dist/src/docker-git/main.js clone",
3234
"open": "bun run build:docker-git && bun dist/src/docker-git/main.js open",

packages/app/src/docker-git/browser-frontend.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
resolveBrowserFrontendStatePath,
1313
shouldReuseBrowserFrontend
1414
} from "./browser-frontend-state.js"
15+
import { findReachableApiBaseUrl } from "./controller-health.js"
16+
import { resolveConfiguredApiBaseUrl, resolveExplicitApiBaseUrl } from "./controller-reachability.js"
1517
import { type ControllerRuntime, ensureControllerReady, resolveApiBaseUrl } from "./controller.js"
1618
import {
1719
runCommandCapture,
@@ -146,6 +148,49 @@ const readBrowserFrontendRuntimeState = (
146148
webState: readBrowserFrontendState(statePath)
147149
})
148150

151+
// CHANGE: prefer the host-facing controller URL for the browser web proxy.
152+
// WHY: controller bootstrap may select a Docker bridge IP before the published localhost port is reachable, but the served browser runtime must keep durable state and proxy config on the externally reachable endpoint.
153+
// QUOTE(ТЗ): "комментарии ребита надо было тоже поддержать"
154+
// REF: PR #344 E2E (Browser command) regression.
155+
// SOURCE: n/a
156+
// FORMAT THEOREM: explicit_api -> explicit_api; reachable(configured_api) -> configured_api; otherwise -> selected_api
157+
// PURITY: SHELL
158+
// EFFECT: Effect<string, never, ControllerRuntime>
159+
// INVARIANT: explicit DOCKER_GIT_API_URL is never overridden by auto-discovery.
160+
// COMPLEXITY: O(1) probes/O(1) space.
161+
/**
162+
* Resolves the API URL used by the browser frontend proxy.
163+
*
164+
* @returns Effect with the explicit API URL, the reachable configured host URL, or the selected controller URL.
165+
*
166+
* @pure false
167+
* @effect FetchHttpClient through controller health probing.
168+
* @invariant Explicit `DOCKER_GIT_API_URL` has precedence over all inferred endpoints.
169+
* @precondition `ensureControllerReady` has already completed for inferred endpoints.
170+
* @postcondition A configured host URL is used only after a successful health probe.
171+
* @complexity O(1) time and O(1) space for the bounded candidate set.
172+
* @throws Never - health probe failures fall back to the selected controller URL.
173+
*/
174+
const resolveBrowserFrontendApiBaseUrl = (): Effect.Effect<string, never, ControllerRuntime> => {
175+
const selectedApiBaseUrl = resolveApiBaseUrl()
176+
const explicitApiBaseUrl = resolveExplicitApiBaseUrl()
177+
if (explicitApiBaseUrl !== undefined) {
178+
return Effect.succeed(explicitApiBaseUrl)
179+
}
180+
181+
const configuredApiBaseUrl = resolveConfiguredApiBaseUrl()
182+
if (configuredApiBaseUrl === selectedApiBaseUrl) {
183+
return Effect.succeed(selectedApiBaseUrl)
184+
}
185+
186+
return findReachableApiBaseUrl([configuredApiBaseUrl]).pipe(
187+
Effect.match({
188+
onFailure: () => selectedApiBaseUrl,
189+
onSuccess: (apiBaseUrl) => apiBaseUrl
190+
})
191+
)
192+
}
193+
149194
const stopCurrentWebServer = (): Effect.Effect<
150195
void,
151196
ControllerBootstrapError | PlatformError,
@@ -173,7 +218,7 @@ const prepareBrowserStack = (): Effect.Effect<
173218
yield* _(Effect.log("Ensuring docker-git API controller is current."))
174219
yield* _(ensureControllerReady())
175220

176-
const apiBaseUrl = resolveApiBaseUrl()
221+
const apiBaseUrl = yield* _(resolveBrowserFrontendApiBaseUrl())
177222
const runtimeState = yield* _(readBrowserFrontendRuntimeState(statePath))
178223
const reuseInput: BrowserFrontendReuseInput = {
179224
apiBaseUrl,

0 commit comments

Comments
 (0)