Skip to content

Commit e7834df

Browse files
geekypunkclaudevenkateshsakamuri-lab
authored
fix(agent): retry upstream clones so a GitHub 429 stops failing the image build (#67)
## Symptom `docker compose build` has been failing intermittently on unrelated PRs — most recently twice today on `cursor/admin-profile-switch-c497`. Every other job passes; only the agent image fails. ## Root cause Not a code defect. Both upstream clones in `agent/Dockerfile` are **unauthenticated** requests to github.com, issued **once, with no retry**: ``` Cloning into 'runtime'... remote: This request was rate-limited due to too many requests. fatal: unable to access 'https://github.com/NousResearch/hermes-agent.git/': The requested URL returned error: 429 exit code: 128 ``` GitHub rate-limits unauthenticated traffic **per source IP**, and Actions runners share pooled egress addresses. Under load the clone is refused and the whole image build dies with it. Nothing about the PR under test is involved — which is exactly why it looked random and kept landing on innocent branches. **Failure rate: 2 of the last 15 `docker compose build` runs.** That flakiness pattern is itself the diagnosis; a real build break would fail 15 of 15. ## Fix Both clones retry up to 5 times with escalating backoff (15/30/45/60s). Retry is the right layer *because* the condition is transient — but the loop still prints `FATAL` and **exits 1** on the last attempt, so an upstream that is genuinely gone fails the build loudly rather than yielding an image with no runtime in it. This is not CI-only defensiveness: `scripts/self-host/install.sh` runs the same build, so a self-host user behind a shared NAT hits the identical refusal. Also noted in a comment: the webui clone's existing `|| git clone` fallback only ever handled a *moved ref*. Against a rate-limit it just re-issued the same refused request, so it needed the outer retry to be of any use at all. ## Verification - [x] Both edited `RUN` blocks pass `sh -n` as the shell actually receives them (continuations joined, in-`RUN` comments stripped). Baseline check caught a bug in my own extractor first — all 6 blocks including 3 untouched ones flagged, which is how I knew the harness was wrong rather than the Dockerfile. - [x] Success path: the wrapper performs a real `git clone` of hermes-agent — exit 0, 76 entries, `pyproject.toml` present. - [x] Give-up path: against an unreachable repo it retries 5×, prints `FATAL`, exits **1** (not 0). - [ ] CI `docker compose build` green on this PR — the authoritative test. ## Follow-up, deliberately not bundled here `AGENT_RUNTIME_REF=main` and `AGENT_API_REF=master` are **unpinned branches**, which contradicts the pinning rule in `CLAUDE.md` (*"PINNED for reproducibility ... Bump deliberately and re-validate"*). That's a second, independent failure mode — an upstream commit can break this build with no change on our side, which is precisely the class of bug the `mcp>=1.0,<2` pin two lines below exists to prevent. Current upstream tags are `v2026.8.18` (hermes-agent) and `v0.52.76` (hermes-webui). Worth a separate PR that pins and re-validates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent 0aa2b77 commit e7834df

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

CLAUDE.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,21 @@ Admins can **View as** a sub-user from the top-right of the home layout (`Profil
238238
`hermes_requires <0.20.0` on a "verified" 401 that came from a hand-rolled
239239
`hermes serve` run rather than `hermes webui`. 0.20.0 works. Verify against the
240240
real start path before writing a version constraint.
241+
5. **The agent image build clones two third-party repos over the public internet,
242+
unauthenticated.** `agent/Dockerfile` fetches `NousResearch/hermes-agent` and
243+
`nesquena/hermes-webui` at build time. GitHub rate-limits unauthenticated
244+
requests *per source IP*, and Actions runners share pooled egress addresses, so
245+
`docker compose build` intermittently died on `fatal: unable to access ...: The
246+
requested URL returned error: 429` (exit 128) — 2 of 15 runs, always on branches
247+
whose diff had nothing to do with the agent. Both clones now retry 5x with
248+
backoff, and still print FATAL and exit 1 on exhaustion so a genuinely dead
249+
upstream cannot yield an image with no runtime in it. Two lessons worth keeping:
250+
a CI failure that is *intermittent and unrelated to the diff* is a network or
251+
rate-limit signature, not a code defect — read the log before bisecting the
252+
branch; and the webui clone's pre-existing `|| git clone` fallback looked like
253+
resilience but only ever handled a *moved ref*, re-issuing the identical refused
254+
request against a 429. A fallback that fails the same way as the thing it backs
255+
up is not a fallback.
241256

242257
### Verification Anti-Patterns (do not repeat)
243258

agent/Dockerfile

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,24 @@ ARG AGENT_RUNTIME_REF=v2026.8.18
5252
ARG AGENT_API_REPO=https://github.com/nesquena/hermes-webui.git
5353
ARG AGENT_API_REF=v0.52.76
5454

55-
# Runtime engine (Python agent)
56-
RUN git clone --depth 1 --branch "${AGENT_RUNTIME_REF}" "${AGENT_RUNTIME_REPO}" runtime \
55+
# Runtime engine (Python agent).
56+
#
57+
# The clone retries with backoff. github.com rate-limits *unauthenticated*
58+
# requests per source IP, and CI runners share pooled egress addresses, so a
59+
# cold clone here intermittently returns HTTP 429 and took the whole image
60+
# build down with it (`fatal: unable to access ...: error: 429`, exit 128 --
61+
# 2 of 15 `docker compose build` runs). This is not CI-only defensiveness: a
62+
# self-host user behind a shared NAT hits the identical refusal. Retry is the
63+
# right layer precisely because the condition is transient; the loop still
64+
# exits non-zero and loudly on the last attempt rather than continuing with no
65+
# runtime cloned.
66+
RUN n=0; rm -rf runtime; \
67+
until git clone --depth 1 --branch "${AGENT_RUNTIME_REF}" "${AGENT_RUNTIME_REPO}" runtime; do \
68+
n=$((n+1)); \
69+
if [ "$n" -ge 5 ]; then echo "FATAL: clone of ${AGENT_RUNTIME_REPO} failed after 5 attempts" >&2; exit 1; fi; \
70+
echo "clone attempt $n failed (HTTP 429 / transient network); retrying in $((n*15))s" >&2; \
71+
rm -rf runtime; sleep $((n*15)); \
72+
done \
5773
&& cd runtime \
5874
&& UV_NO_CONFIG=1 uv sync \
5975
&& if [ -d .venv ] && [ ! -d venv ]; then ln -sfn .venv venv; fi \
@@ -64,8 +80,18 @@ RUN git clone --depth 1 --branch "${AGENT_RUNTIME_REF}" "${AGENT_RUNTIME_REPO}"
6480

6581
# HTTP API surface the frontend / backend talk to (:8787).
6682
# Default branch is master (not main). Fall back to HEAD if the ref moves.
67-
RUN (git clone --depth 1 --branch "${AGENT_API_REF}" "${AGENT_API_REPO}" api \
68-
|| git clone --depth 1 "${AGENT_API_REPO}" api) \
83+
#
84+
# Same 429 retry as the runtime clone above. Note the inner `||` fallback
85+
# handles only a *moved ref*; on a rate-limit it just re-issues the same
86+
# refused request, so it needed the outer retry to be any use at all.
87+
RUN n=0; rm -rf api; \
88+
until (git clone --depth 1 --branch "${AGENT_API_REF}" "${AGENT_API_REPO}" api \
89+
|| git clone --depth 1 "${AGENT_API_REPO}" api); do \
90+
n=$((n+1)); \
91+
if [ "$n" -ge 5 ]; then echo "FATAL: clone of ${AGENT_API_REPO} failed after 5 attempts" >&2; exit 1; fi; \
92+
echo "clone attempt $n failed (HTTP 429 / transient network); retrying in $((n*15))s" >&2; \
93+
rm -rf api; sleep $((n*15)); \
94+
done \
6995
&& cd api \
7096
&& if [ -f requirements.txt ]; then \
7197
UV_NO_CONFIG=1 uv pip install --python /opt/deepsql-agent/runtime/venv/bin/python -r requirements.txt; \

0 commit comments

Comments
 (0)